喵~ 主人,欢迎来到我的题解小站!今天我们要解决的是一道关于字符串拼接和匹配的有趣问题,就像把两团不同颜色的毛线球编织成一条漂亮的围巾一样呢!让咱来带你一步步解开这道题的谜底吧,喵~
E. Three Strings
题目大意
主人,听我给你描述一下这个任务哦。
我们有三个字符串 a
、b
和 c
。其中,c
是通过一种特殊的方式由 a
和 b
混合而成的。
生成 c
的过程是这样的:
- 一开始,
c
是一个空字符串。 - 在每一步,我们随机从
a
或者b
的开头取出一个字符,然后把它加到c
的末尾。 - 这个过程一直持续,直到
a
或者b
中有一个字符串被取空了。 - 然后,把另一个还没取完的字符串剩下的所有字符,按顺序全部加到
c
的末尾。 - 最后,
c
中有一些字符被随机替换成了别的字符。
我们的任务是,给定最终的 a
、b
和 c
,找出在第 5 步中,最少有多少个字符被替换了。
举个例子,如果 a = "abra"
,b = "cada"
,那么一个可能的、没有经过字符替换的 c
可以是 abracada
。如果给定的 c
是 abracade
,那么最少就替换了 1 个字符 (a
-> e
)。
解题思路
嘿嘿,这个问题看起来有点像是在一大堆可能性中找最优解,对吧?一遇到这种“最优解”问题,咱的猫猫直觉就告诉咱,动态规划 (Dynamic Programming, DP) 可能是个好办法哦!
我们可以把生成 c
的过程想象成一条路径。每一步,我们都要决定是从 a
拿一个字符还是从 b
拿一个字符。我们的目标是找到一条路径,使得这条路径生成的字符串与给定的 c
有最多的相同字符。匹配的字符越多,需要修改的就越少嘛!
于是,我们可以定义一个 DP 状态:
dp[i][j]
表示:从字符串 a
中取了前 i
个字符,从字符串 b
中取了前 j
个字符,混合后构成了 c
的前 i+j
个字符,所能达到的 最大匹配字符数。
接下来就是思考状态要怎么转移啦,喵~
假设我们已经知道了 dp[i][j]
的值,也就是用 a
的前 i
个字符和 b
的前 j
个字符构成了 c
的前 i+j
个字符的最大匹配数。现在我们想知道下一步该怎么办。
我们有两种选择:
从
a
中取下一个字符:- 这个选择只有在
a
还没被取完(即i < |a|
)时才可行。 - 我们要取的字符是
a[i]
(0-indexed)。 - 它将成为
c
中的第i+j+1
个字符,也就是c[i+j]
。 - 那么,我们就可以从
dp[i][j]
这个状态转移到dp[i+1][j]
。新的匹配数是dp[i][j]
加上这次新取出的字符是否匹配。如果a[i] == c[i+j]
,匹配数就加 1,否则不变。 - 所以,我们可以更新
dp[i+1][j]
的值为:max(dp[i+1][j], dp[i][j] + (a[i] == c[i+j]))
。
- 这个选择只有在
从
b
中取下一个字符:- 这个选择只有在
b
还没被取完(即j < |b|
)时才可行。 - 我们要取的字符是
b[j]
。 - 它同样将成为
c
中的第i+j+1
个字符,也就是c[i+j]
。 - 于是,我们从
dp[i][j]
转移到dp[i][j+1]
。如果b[j] == c[i+j]
,匹配数就加 1。 - 所以,我们可以更新
dp[i][j+1]
的值为:max(dp[i][j+1], dp[i][j] + (b[j] == c[i+j]))
。
- 这个选择只有在
初始状态: 当 a
和 b
都还没取任何字符时,即 i=0, j=0
,匹配数当然是 0 啦。所以 dp[0][0] = 0
。 为了防止未访问过的状态影响计算,我们可以把 dp
数组的其他值初始化为一个很小的负数。
最终答案: 当我们把 a
的所有 n
个字符和 b
的所有 m
个字符都取完后,对应的状态就是 dp[n][m]
。这个值就代表了在所有可能的混合方式中,能与 c
产生的最大匹配字符数。
那么,最少的修改次数就是 c
的总长度减去这个最大匹配数,也就是 (|a| + |b|) - dp[|a|][|b|]
。
这样一来,思路是不是就清晰多啦?喵~
题解代码
这是根据上面的思路写出的 C++ 代码,咱给主人加上了详细的注释哦!
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
void solve() {
std::string a, b, c;
std::cin >> a >> b >> c;
int n = a.length();
int m = b.length();
// dp[i][j] 将存储:
// 使用 a 的前 i 个字符和 b 的前 j 个字符进行混合,
// 与 c 的前 i+j 个字符所能达到的最大匹配数。
// 为了方便取 max,我们初始化为一个很小的负数。
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(m + 1, -1e9));
// 初始状态:不从 a 和 b 取任何字符,匹配数为 0。
dp[0][0] = 0;
// 开始填充 dp 表格
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
// 如果当前状态 dp[i][j] 是可达的(不是初始的负无穷),
// 就从这里向下一个状态转移。
// 选择 1:从 a 中取下一个字符 a[i]
// 这要求 a 中还有字符没被取走 (i < n)
if (i < n) {
// 新字符 a[i] 将会对应 c 中的 c[i+j]
int match = (a[i] == c[i + j]);
// 更新 dp[i+1][j] 的值
dp[i + 1][j] = std::max(dp[i + 1][j], dp[i][j] + match);
}
// 选择 2:从 b 中取下一个字符 b[j]
// 这要求 b 中还有字符没被取走 (j < m)
if (j < m) {
// 新字符 b[j] 将会对应 c 中的 c[i+j]
int match = (b[j] == c[i + j]);
// 更新 dp[i][j+1] 的值
dp[i][j + 1] = std::max(dp[i][j + 1], dp[i][j] + match);
}
}
}
// 最终,dp[n][m] 就是取完 a 和 b 所有字符后的最大匹配数
int max_matches = dp[n][m];
int total_len = n + m;
// 最少修改次数 = 总长度 - 最大匹配数
int min_changes = total_len - max_matches;
std::cout << min_changes << std::endl;
}
int main() {
// 加速输入输出,让程序跑得更快一点~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
知识点介绍
主人,这个问题里用到的核心思想其实和一些经典算法很像哦,了解它们能让你变得更强!
动态规划 (Dynamic Programming) 动态规划是一种将复杂问题分解成更小、更简单的子问题来解决的策略。就像猫咪玩毛线球,不会一口气把整个球解开,而是一点一点地抽丝剥茧。DP 的关键在于:
- 定义状态:
dp[i][j]
是什么意思? - 找到状态转移方程:如何从已知的子问题答案推导出更大问题的答案?
- 确定边界/初始条件:最小的子问题答案是什么? 只要掌握了这三点,很多看起来很棘手的问题都会变得温顺起来呢!
- 定义状态:
最长公共子序列 (Longest Common Subsequence, LCS) 你有没有觉得我们这个 DP 的过程和“最长公共子序列”问题特别像?LCS 问题是寻找两个字符串中都以相同顺序出现的、最长的字符序列。
经典的 LCS 解法也是用一个
dp[i][j]
表,表示第一个字符串的前i
个字符和第二个字符串的前j
个字符的 LCS 长度。它的转移方程是:- 如果
str1[i] == str2[j]
,则dp[i][j] = dp[i-1][j-1] + 1
。 - 否则,
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
。
我们这道题,可以看作是寻找
c
和 “所有可能的a
、b
混合字符串” 之间的“最长公共子序列”。我们的 DP 状态dp[i][j]
就代表了c
的前缀c[0...i+j-1]
和a
的前缀a[0...i-1]
与b
的前缀b[0...j-1]
的某种混合的最大匹配。转移方式也极其相似,都是从dp[i-1][j]
和dp[i][j-1]
两个方向过来。所以说,算法的思想是相通的,学好一个,其他的也更容易理解啦,喵~- 如果
好啦,这次的题解就到这里!希望咱的讲解能帮到主人哦。如果还有其他问题,随时可以再来找我玩!喵呜~ (ฅ'ω'ฅ)