题目描述
给定一个后缀替换文法,包含一个起始字符串 SSS 和一个目标字符串 TTT,以及 NRNRNR 条规则,每条规则形如 X→YX \\to YX→Y,其中 XXX 和 YYY 是等长的字母数字字符串。应用规则时,若当前字符串的后缀恰好等于 XXX,则可将该后缀替换为 YYY。规则可以任意多次使用。要求判断是否能从 SSS 变换到 TTT,若能,则输出所需的最少规则应用次数。
输入格式
输入包含多个测试用例。每个测试用例以一行开始,包含两个等长的字母数字字符串 SSS 和 TTT(长度 1≤∣S∣=∣T∣≤201 \\le |S| = |T| \\le 201≤∣S∣=∣T∣≤20,由空格分隔),以及一个整数 NRNRNR(0≤NR≤1000 \\le NR \\le 1000≤NR≤100),表示规则条数。随后 NRNRNR 行,每行包含两个等长的字母数字字符串 XXX 和 YYY(长度 1≤∣X∣=∣Y∣≤201 \\le |X| = |Y| \\le 201≤∣X∣=∣Y∣≤20),表示规则 X→YX \\to YX→Y。所有字符串区分大小写。输入以一行单独的句点 . 结束。
输出格式
对于每个测试用例,输出一行 Case x: 后接最少规则应用次数;如果无法变换,则输出 No solution。
样例
输入
AA BB 4
A B
AB BA
AA CC
CC BB
A B 3
A C
B C
C B
.
输出
Case 1: 2
Case 2: No solution
题目分析
本题的核心是在一个状态空间(所有与 SSS 等长的字符串)中,以规则为有向边,求从 SSS 到 TTT 的最短路径。然而,由于字母数字字符集大小为 626262,长度最大为 202020,完整状态空间理论上有 622062^{20}6220 个状态,不可能直接搜索。
但规则只能修改后缀,这一特性大大限制了状态间的转换关系。我们可以将字符串按照长度分层,仅关注可能出现的后缀。任意规则 X→YX \\to YX→Y 以及初始串 SSS 和目标串 TTT,它们的所有后缀都有可能出现在某次变换中。因此,所有可达字符串的后缀必定在这些集合内。这样,状态数量被限制在 O(NR⋅L+L2)O(NR \\cdot L + L^2)O(NR⋅L+L2) 级别,其中 L≤20L \\le 20L≤20,因此可以接受。
进一步观察,长度为 kkk 的字符串之间,如果两个串的首字符相同,则它们的转换可以分解为:先处理首字符后的长度为 k−1k-1k−1 的后缀(利用长度 k−1k-1k−1 的最短路径),然后再考虑是否需替换首字符(直接规则或间接规则)。这启发我们采用动态规划思想,从长度 111 开始,逐步向长度 LLL 扩展,利用 Floyd-Warshall\\texttt{Floyd-Warshall}Floyd-Warshall 算法计算每一层内所有节点之间的最短距离。
解题思路
状态定义与编号
对于每个长度 kkk(1≤k≤L1 \\le k \\le L1≤k≤L),将所有可能出现的后缀(即 SSS 和 TTT 的所有后缀、所有规则左右两侧的所有后缀)收集起来,去重后存入数组 suffixList[k],并用哈希映射 idMap[k] 将每个字符串映射到其编号。这样,长度为 kkk 的节点个数 nkn_knk 非常有限(通常不超过几百个)。
构建长度 kkk 的转移图
对于长度为 kkk 的任意两个节点 iii 和 jjj(对应字符串 uuu 和 vvv),我们定义从 uuu 到 vvv 的最短距离 distk[i][j]\\textit{dist}_k[i][j]distk[i][j]。其初始值设为无穷大,对角线为 000。
转移分为两类:
直接规则:如果存在一条规则 u→vu \\to vu→v(即 X=uX = uX=u 且 Y=vY = vY=v),则从 uuu 到 vvv 有边,权值为 111。
间接转移:如果 k>1k > 1k>1 且 u[0]=v[0]u[0] = v[0]u[0]=v[0](首字符相同),那么可以先通过长度 k−1k-1k−1 的路径将 u[1..k−1]u[1..k-1]u[1..k−1] 变换为 v[1..k−1]v[1..k-1]v[1..k−1],而首字符保持不变。此时从 uuu 到 vvv 的代价就等于长度 k−1k-1k−1 层中对应的两个后缀之间的最短距离。该值已在上一层计算好(存储在 preDist 中)。
将这些初始边加入后,对长度 kkk 的节点集合运行 Floyd-Warshall\\texttt{Floyd-Warshall}Floyd-Warshall 算法,求得该层所有节点之间的最短闭包。这个闭包就表示在长度保持为 kkk 的前提下,任意两串之间变换所需的最少规则数。
逐层递推
我们从 k=1k=1k=1 开始,逐步计算到 k=Lk=Lk=L。在计算第 kkk 层时,使用第 k−1k-1k−1 层的结果(preDist)作为间接转移的依据。计算完成后,将该层的 dist 复制给 preDist,用于下一层的计算。
最终,当 k=Lk=Lk=L 时,preDist 就是长度为 LLL 的所有可达状态之间的最短距离矩阵。直接取出 SSS 和 TTT 对应的编号,其距离即为答案(若为无穷大则表示不可达)。
正确性说明
该算法的正确性基于以下观察:
- 任何对长度为 kkk 的字符串进行操作,若操作改变了首字符,则必然是直接应用了一条规则,且该规则左右两侧长度均为 kkk(因为规则只能替换整个后缀,若替换长度小于 kkk,首字符不受影响;若替换长度为 kkk,则整个字符串被替换,首字符可能改变)。
- 若首字符不变,则只可能通过修改剩余长度为 k−1k-1k−1 的后缀来实现,这正好对应间接转移。
- 通过 Floyd-Warshall\\texttt{Floyd-Warshall}Floyd-Warshall 计算闭包,可以组合多条直接或间接边,得到任意两点间的最短路径。
由于长度上限很小(L≤20L \\le 20L≤20),各层节点总数也小,Floyd-Warshall\\texttt{Floyd-Warshall}Floyd-Warshall 的 O(nk3)O(n_k^3)O(nk3) 是可接受的。
复杂度分析
- 收集节点:每条规则产生至多 ∣X∣+∣Y∣|X| + |Y|∣X∣+∣Y∣ 个后缀,总共 O(NR⋅L)O(NR \\cdot L)O(NR⋅L) 个字符串,每个字符串插入哈希表 O(L)O(L)O(L),故 O(NR⋅L2)O(NR \\cdot L^2)O(NR⋅L2)。
- 每层节点数 nkn_knk 不超过 NR⋅L+2LNR \\cdot L + 2LNR⋅L+2L,实际很小。
- 每层 Floyd-Warshall\\texttt{Floyd-Warshall}Floyd-Warshall 算法的时间复杂度 O(nk3)O(n_k^3)O(nk3),总复杂度为 O(∑k=1Lnk3)O(\\sum_{k=1}^L n_k^3)O(∑k=1Lnk3),在最坏情况下 L=20L=20L=20,NR=100NR=100NR=100,nkn_knk 约为 200200200,计算量约为 20×2003=1.6×10820 \\times 200^3 = 1.6 \\times 10^820×2003=1.6×108,在可接受范围内(实际运行很快)。
- 空间复杂度 O(maxnk2)O(\\max n_k^2)O(maxnk2)。
代码实现
// Suffix-Replacement Grammars
// UVa ID: 1089
// Verdict: Accepted
// Submission Date: 2026-06-21
// UVa Run Time: 0.560s
//
// 版权所有(C)2026,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
const int MAXL = 25;
const long long INF = (1LL << 60);
vector<string> suffixList[MAXL];
map<string, int> idMap[MAXL];
void addNode(const string& s) {
int len = s.size();
if (len >= MAXL) return;
if (idMap[len].count(s)) return;
int label = idMap[len].size();
idMap[len][s] = label;
suffixList[len].push_back(s);
}
long long solveCase(const string& S, const string& T, const vector<pair<string, string>>& rules) {
int L = S.size();
// 清空之前的数据
for (int i = 0; i < MAXL; ++i) {
suffixList[i].clear();
idMap[i].clear();
}
// 收集所有可能出现的后缀:规则 X、Y 的所有后缀,以及 S、T 的所有后缀
for (const auto& rule : rules) {
const string& X = rule.first;
const string& Y = rule.second;
for (int i = 0; i < (int)X.size(); ++i)
addNode(X.substr(i));
for (int i = 0; i < (int)Y.size(); ++i)
addNode(Y.substr(i));
}
for (int i = 0; i < L; ++i) {
addNode(S.substr(i));
addNode(T.substr(i));
}
vector<vector<long long>> preDist; // 长度 k-1 的距离矩阵
for (int k = 1; k <= L; ++k) {
int n = suffixList[k].size();
vector<vector<long long>> dist(n, vector<long long>(n, INF));
for (int i = 0; i < n; ++i) dist[i][i] = 0;
// 填充初始边
for (int i = 0; i < n; ++i) {
const string& si = suffixList[k][i];
for (int j = 0; j < n; ++j) {
const string& sj = suffixList[k][j];
// 直接规则
for (const auto& rule : rules) {
if (rule.first == si && rule.second == sj) {
dist[i][j] = min(dist[i][j], 1LL);
break;
}
}
// 间接转移:首字符相同
if (k > 1 && si[0] == sj[0]) {
string subI = si.substr(1);
string subJ = sj.substr(1);
auto itI = idMap[k – 1].find(subI);
auto itJ = idMap[k – 1].find(subJ);
if (itI != idMap[k – 1].end() && itJ != idMap[k – 1].end()) {
int pi = itI->second;
int pj = itJ->second;
dist[i][j] = min(dist[i][j], preDist[pi][pj]);
}
}
}
}
// Floyd 求传递闭包
for (int mid = 0; mid < n; ++mid)
for (int i = 0; i < n; ++i) {
if (dist[i][mid] >= INF) continue;
for (int j = 0; j < n; ++j)
if (dist[mid][j] < INF)
dist[i][j] = min(dist[i][j], dist[i][mid] + dist[mid][j]);
}
preDist = move(dist); // 作为下一长度的前驱矩阵
}
int st = idMap[L][S];
int ed = idMap[L][T];
long long ans = preDist[st][ed];
return (ans >= INF ? –1 : ans);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string S, T;
int NR;
int caseNo = 1;
while (cin >> S) {
if (S == ".") break;
cin >> T >> NR;
vector<pair<string, string>> rules;
for (int i = 0; i < NR; ++i) {
string X, Y;
cin >> X >> Y;
rules.emplace_back(X, Y);
}
long long ans = solveCase(S, T, rules);
cout << "Case " << caseNo++ << ": ";
if (ans == –1) cout << "No solution\\n";
else cout << ans << "\\n";
}
return 0;
}
总结
本题的关键在于利用规则仅作用于后缀这一限制,将状态空间压缩到所有可能出现的后缀集合。通过长度分层和动态规划,将任意长度的字符串之间的最短路径问题,分解为同一长度下的闭包计算和长度间的递推。Floyd-Warshall\\texttt{Floyd-Warshall}Floyd-Warshall 算法在每层内求解全源最短路径,而层间则通过首字符相同的条件进行转移。该方法有效避免了直接搜索的巨大状态空间,且实现简洁,性能优异。此类“后缀替换”问题往往可以借鉴字符串分层 DP\\texttt{DP}DP 的思路,是解决规则驱动型字符串变换问题的常用技巧。


