题目描述

给定两个单词 xxxyyy ,以及一个有限的单词序列 {w1,w2,…,wk}\{w_1, w_2, \ldots, w_k\}{w1,w2,,wk} 。我们可以在 xxxyyy 的右侧追加序列中的单词,每次追加算一次操作。问是否能够通过若干次操作,使得 xxxyyy 最终变成完全相同的单词?如果可以,求出最少操作次数;如果不可能,输出 −1-11

样例分析

样例输入:

2
abba
ab
4
baaabad
aa
badccaa
cc
a
ab
4
bb
ab
ba
aa

样例输出:

5
-1

第一个样例中,初始单词 x="abba"x = \texttt{"abba"}x="abba"y="ab"y = \texttt{"ab"}y="ab" 。通过如下操作可以得到相同字符串:

  • xxx 追加 "aa"\texttt{"aa"}"aa""badccaa"\texttt{"badccaa"}"badccaa" ,得到 "abbaaabadccaa"\texttt{"abbaaabadccaa"}"abbaaabadccaa"
  • yyy 追加 "baaabad"\texttt{"baaabad"}"baaabad""cc"\texttt{"cc"}"cc""aa"\texttt{"aa"}"aa" ,得到 "abbaaabadccaa"\texttt{"abbaaabadccaa"}"abbaaabadccaa"

555 次操作。

第二个样例中,初始单词 x="a"x = \texttt{"a"}x="a"y="ab"y = \texttt{"ab"}y="ab" ,虽然互为前缀,但无法通过给定单词序列调整成功,输出 −1-11

题目分析

关键观察

  1. 前缀关系是必要条件
    如果 xxxyyy 能够通过追加单词变成相同的字符串 ZZZ ,那么 ZZZ 必然同时以 xxxyyy 为前缀。因此,xxxyyy 中较短的那个必须是较长那个的前缀。否则直接输出 −1-11

  2. 问题转化
    假设 xxx 较短, yyy 较长,且 yyyxxx 为前缀。设它们的共同前缀长度为 lll ,则:

    • xxx 已经完全匹配
    • yyy 还剩余 y[l:]y[l:]y[l:] 需要匹配

    但仔细思考会发现,实际情况比这复杂:当向 xxx 追加单词时,xxx 变长,可能会超过 yyy 的当前长度;同理,向 yyy 追加单词时,yyy 变长,也可能会超过 xxx 的当前长度。因此,不能简单地认为只有一方有剩余。

  3. 进一步转化
    考虑任意时刻两个字符串的状态。由于它们始终互为前缀(这是操作过程保持的性质),我们可以用较长字符串减去共同前缀后的剩余部分来表示当前状态。例如:

    • 初始状态:x="abba"x = \texttt{"abba"}x="abba"y="ab"y = \texttt{"ab"}y="ab" ,共同前缀 "ab"\texttt{"ab"}"ab" ,剩余部分 "ba"\texttt{"ba"}"ba"(来自 xxx
    • xxx 追加 "aa"\texttt{"aa"}"aa" 后:x="abbaaa"x = \texttt{"abbaaa"}x="abbaaa"y="ab"y = \texttt{"ab"}y="ab" ,共同前缀 "ab"\texttt{"ab"}"ab" ,剩余部分 "baaa"\texttt{"baaa"}"baaa"(来自 xxx
    • yyy 追加 "baaabad"\texttt{"baaabad"}"baaabad" 后:x="abbaaa"x = \texttt{"abbaaa"}x="abbaaa"y="abbaaabad"y = \texttt{"abbaaabad"}y="abbaaabad" ,共同前缀 "abbaa"\texttt{"abbaa"}"abbaa" ,剩余部分 "abad"\texttt{"abad"}"abad"(来自 yyy

    这样,我们只需要追踪一个字符串——当前需要匹配的剩余部分。

  4. 状态转移
    设当前剩余部分为 sss 。当我们选择一个单词 www 时,有两种情况:

    • 如果 ssswww 为前缀,说明 www 可以追加到当前较短的字符串上,使得较短的变长,剩余部分变为 sss 去掉 www 前缀后的部分
    • 如果 wwwsss 为前缀,说明 www 可以追加到当前较长的字符串上,使得较长的变长,剩余部分变为 www 去掉 sss 前缀后的部分

    这两种情况可以统一处理:检查 ssswww 是否互为前缀,如果是,则新剩余部分为较长者去掉共同前缀后的子串。

  5. 最少操作次数
    问题转化为:从初始剩余部分出发,每次选择一个单词,根据上述规则转移到新的剩余部分,求到达空字符串的最少步数。这是一个典型的最短路径问题,由于每次操作代价为 111 ,可以用 BFS\texttt{BFS}BFS 求解。

解题思路

算法步骤

  1. 预处理
    读入测试用例数 TTT

  2. 对于每个测试用例

    • 读入 xxxyyy
    • 检查 xxxyyy 是否互为前缀(使用 isPrefix\texttt{isPrefix}isPrefix 函数)
    • 如果不是,输出 −1-11 并继续下一个测试用例
    • 如果是,计算初始剩余部分 remain=getRemainder(x,y)\texttt{remain} = \texttt{getRemainder}(x, y)remain=getRemainder(x,y)
    • 如果 remain\texttt{remain}remain 为空,说明已经相同,输出 000 并继续
  3. BFS\texttt{BFS}BFS 搜索

    • 使用队列 q\texttt{q}q 存储待处理的剩余部分
    • 使用哈希表 dist\texttt{dist}dist 记录到达每个剩余部分的最少步数
    • 初始状态:dist[remain]=0\texttt{dist[remain]} = 0dist[remain]=0q.push(remain)\texttt{q.push(remain)}q.push(remain)
    • 当队列非空:
      • 取出队首 cur\texttt{cur}cur 及其步数 steps\texttt{steps}steps
      • 如果 cur\texttt{cur}cur 为空,返回 steps\texttt{steps}steps
      • 遍历每个单词 w\texttt{w}w
        • 如果 isPrefix(cur, w)\texttt{isPrefix(cur, w)}isPrefix(cur, w) 为真(即互为前缀)
        • 计算新剩余部分 next=getRemainder(cur, w)\texttt{next} = \texttt{getRemainder(cur, w)}next=getRemainder(cur, w)
        • 如果 next\texttt{next}next 未被访问过,更新距离并入队
    • 如果队列为空仍未找到空字符串,返回 −1-11
  4. 输出结果

正确性证明

  • 状态表示的完备性:任意时刻,两个字符串的状态可以用剩余部分唯一表示,因为共同前缀已经匹配,只需要关心后续需要匹配的部分。
  • 状态转移的正确性:每次操作对应一次单词追加,必然导致剩余部分按照上述规则变化。反之,任何合法的剩余部分变化都对应一次有效操作。
  • 最优性BFS\texttt{BFS}BFS 保证首次到达空字符串时的步数最小。

复杂度分析

  • 时间复杂度:O(L⋅k⋅∣w∣)O(L \cdot k \cdot |w|)O(Lkw) ,其中 LLL 是可能出现的剩余部分的最大长度,kkk 是单词个数,∣w∣|w|w 是单词平均长度。实际运行中状态数远小于理论最大值。
  • 空间复杂度:O(L⋅k)O(L \cdot k)O(Lk) ,用于存储 dist\texttt{dist}dist 映射和队列。

注意事项

  1. 单词可以重复使用,不需要记录使用状态。
  2. 剩余部分可能很长,需要使用 string\texttt{string}string 类型存储。
  3. isPrefix\texttt{isPrefix}isPrefix 函数需要正确处理两个字符串长度不同的情况。
  4. 使用 unordered_map\texttt{unordered\_map}unordered_map 而不是 map\texttt{map}map 以获得更好的平均性能。

参考代码

// Words Adjustment
// UVa ID: 10941
// Verdict: Accepted
// Submission Date: 2026-03-01
// UVa Run Time: 0.060s
//
// 版权所有(C)2026,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

// 检查 a 和 b 是否互为前缀
bool isPrefix(const string& a, const string& b) {
    int n = min(a.size(), b.size());
    for (int i = 0; i < n; i++)
        if (a[i] != b[i]) return false;
    return true;
}

// 返回较长字符串去掉共同前缀后的剩余部分
string getRemainder(const string& a, const string& b) {
    int n = min(a.size(), b.size());
    return (a.size() > b.size() ? a : b).substr(n);
}

int solve(string x, string y, vector<string>& words) {
    // 检查是否互为前缀
    if (!isPrefix(x, y)) return -1;
    
    // 获取需要匹配的剩余部分
    string remain = getRemainder(x, y);
    if (remain.empty()) return 0;
    
    // BFS:状态为当前需要匹配的剩余字符串
    unordered_map<string, int> dist;
    queue<string> q;
    
    dist[remain] = 0;
    q.push(remain);
    
    while (!q.empty()) {
        string cur = q.front();
        q.pop();
        int steps = dist[cur];
        
        if (cur.empty()) return steps;
        
        for (string& w : words) {
            // 检查 w 和 cur 是否互为前缀
            if (isPrefix(cur, w)) {
                string next = getRemainder(cur, w);
                if (!dist.count(next)) {
                    dist[next] = steps + 1;
                    q.push(next);
                }
            }
        }
    }
    
    return -1;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int caseNum;
    cin >> caseNum;
    
    while (caseNum--) {
        string x, y;
        cin >> x >> y;
        
        int k;
        cin >> k;
        
        vector<string> words(k);
        for (int i = 0; i < k; i++) cin >> words[i];
        
        int result = solve(x, y, words);
        cout << result << "\n";
    }
    
    return 0;
}

总结

本题的关键在于将两个字符串的状态巧妙地转化为单个剩余字符串,从而将问题简化为单源最短路径问题。这种转化思路在字符串处理问题中非常常见,值得学习掌握。BFS\texttt{BFS}BFS 的使用保证了在无权图中找到最优解,而 unordered_map\texttt{unordered\_map}unordered_map 则提供了高效的狀態查詢。

更多推荐