题解:洛谷 P9013 Find and Replace
【题目来源】
洛谷:P9013 [USACO23JAN] Find and Replace S - 洛谷
【题目描述】
Bessie 正在使用世界上最先进最伟大的文本编辑器:miV!她想将一个仅由大写和小写英文字母组成的字符串转换为一个新的字符串。每一次操作,miV 可以将字符串中所有的字母 \(c_1\) 替换成另一种字母 \(c_2\)。例:对于字符串aAbBa,如果将其中的 a 替换成 B,那么字符串会变为BAbBB。
Bessie 非常地忙碌,所以对于给出的 \(T(1\le T\le 10)\) 组测试数据,请输出她至少需要多少次操作才能把原字符串转换为新字符串。
【输入】
第一行是一个整数 \(T\),表示测试数据的数量。
接下来有 \(T\) 对长度相等的字符串。字符串中所有的字符都是大写或小写的字母。字符串的长度不会超过 \(10^5\)。
【输出】
对于每组测试数据,输出转换字符串需要的最小操作数。
如果这不可能做到,输出 \(−1\)。
【输入样例】
4
abc
abc
BBC
ABC
abc
bbc
ABCD
BACD
【输出样例】
0
-1
1
3
【核心思想】
-
问题分析:给定源字符串 \(s\) 和目标字符串 \(t\),每次操作可将字符串中所有某个字符 \(c_1\) 替换为 \(c_2\)。求最少操作次数将 \(s\) 变为 \(t\),或判断不可能。本质上是字符映射图分析问题:建立从源字符到目标字符的映射关系,分析映射图中的链、环和冲突。
-
算法选择:
- 映射图构建:每个源字符 \(s_i\) 映射到目标字符 \(t_i\),构建有向图
- 冲突检测:同一源字符不能映射到不同目标字符;若所有 \(52\) 个字母都参与映射且无不动点,则无法转换(无临时字符中转)
- 环与链分析:统计映射图中的边数,检测环并判断是否需要额外操作
-
关键步骤:
- 读取数据:读入 \(T\) 组 \((s, t)\)
- 构建映射(\(i = 0\) 到 \(len-1\)):
- $si = $
ch2i(s[i]),$ti = $ch2i(t[i]) - 若
to[si]已存在且不等于 \(ti\),ok = false(冲突) to[si] = ti,st.insert(ti)- 若 \(s[i] \neq t[i]\),
same = false
- $si = $
- 不可能性检测:
- 若
!ok,输出 \(-1\) - 若
st.size() == 52 && !same,输出 \(-1\)(全映射无临时字符) - 若
same,输出 \(0\)
- 若
- 统计操作数:
- \(ans \leftarrow\) 非自环边数(
to[i] && to[i] != i) - 计算每个目标字符的入度
inDeg
- \(ans \leftarrow\) 非自环边数(
- 环检测:遍历 \(1\) 到 \(52\):
- 用
start数组标记访问状态,检测环 - 若环上所有节点入度均为 \(1\)(无外部入边),\(ans \leftarrow ans + 1\)(需要临时字符中转)
- 用
- 输出 \(ans\)
-
时间/空间复杂度:
- 时间复杂度:\(O(T \cdot (|s| + 52))\),线性扫描 + 固定字符集处理
- 空间复杂度:\(O(52)\),固定大小的映射数组
-
字符映射图的核心思想:
- 函数映射约束:每个源字符只能映射到一个目标字符,但多个源字符可映射到同一目标字符
- 环的处理:映射图中若存在环(如 \(A \to B \to C \to A\)),直接替换会导致循环依赖,需要临时字符中转,额外消耗 \(1\) 次操作
- 全映射死锁:若所有 \(52\) 个字母都参与映射且无不动点,没有临时字符可用,无法完成转换
- 入度分析:环上某节点入度 \(> 1\) 时,可通过其他路径间接中转,无需额外操作
- 适用于字符串变换、字符替换、函数图分析类问题
【解题思路】

【算法标签】
提高+ #字符串入门
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int maxN = 55;
int to[maxN], inDeg[maxN], start[maxN];
int ch2i(char x) // 将字符串转为数字,便于作为数组下标存储数据
{
return isupper(x) ? x-'A'+1 : x-'a'+27; // 如果是大写A-Z,就是1-26,如果是小写a-z,就是27-52
}
int main()
{
int T;
cin >> T; // 输入T
while (T--) { // 遍历T组数据
memset(to, 0, sizeof(to)); // 每次清空to、inDeg、start数组
memset(inDeg, 0, sizeof(to));
memset(start, 0, sizeof(to));
string s, t;
cin >> s >> t; // 输入源字符串s和目标字符串t
set<int> st; // 定义集合(无重复元素)
bool ok = true, same = true; // 定义标记位标记是否有同一个源字符指向不同的目标字符、是否所有位置都相同
int len = s.size(); // 获取字符串长度
for (int i=0; i<len; i++) { // 遍历字符串
int si=ch2i(s[i]), ti = ch2i(t[i]); // 获取每个位置的字符,并转为数字
if (to[si] && to[si]!=ti) { // 如果曾经赋过值,且与当前的目标字符不相等
ok = false; // 说明同一个源字符指向不同的目标字符,修改ok标记为false
break; // 退出循环
}
to[si] = ti; // si位置的字符需要转为ti
st.insert(ti); // 目标字符插入集合中
if (s[i]!=t[i]) { // 如果源字符与目标字符不相等
same = false; // 标记为false
}
}
if (52==st.size() && !same) { // 如果集合大小为52,即所有大小写字母都出现过,且有位置存在不相同
ok = false; // 那说明所有字符都不相等,且找不到个临时字符来保存值
}
if (!ok) { // 如果不能转换
cout << "-1" << endl; // 输出-1
continue;
}
if (same) { // 如果所有字符都相同
cout << "0" << endl; // 输出0
continue;
}
int ans = 0;
for (int i=1; i<=52; i++) { // 遍历52个字符
if (to[i] && to[i]!=i) { // 如果有to[i]有值,且不与i相等
ans++; // 操作次数自增1
inDeg[to[i]]++; // to[i]的入度自增1
}
}
for (int i=1; i<=52; i++) { // 遍历52个字符
int u = i; // 定义u保存i
if (start[u]) { // 如果start[u]已经赋过值则继续循环
continue;
}
while (u && !start[u]) { // 判断是否属于环,如s=123,t=231
start[u] = i; // 同一个环的指向同一个值,如start[1]=1,start[2]=1,start[3]=1
u = to[u]; // 这个环以节点1开头
}
if (u && u!=to[u] && start[u]==i) { //构成环
bool multiIn = false; // 判断是否有多个入度,如s=ABCD,t=BCAB
int v = u; // 临时变量保存u
do {
if (inDeg[v] > 1) { // 如B就有多个入点
multiIn = true; // multiIn就修改为true
}
v = to[v]; // 遍历环上的所有点
} while (v!=u); // 一直到v与u相同退出循环
if (!multiIn) { // 如果环上的点没有多个入度
ans++; // 答案要自增1
}
}
}
cout << ans << endl; // 输出方案数
}
return 0;
}
【运行结果】
4
abc
abc
0
BBC
ABC
-1
abc
bbc
1
ABCD
BACD
3
浙公网安备 33010602011771号