喵~ 主人,欢迎来到我的题解小站!今天我们要解决的是一道关于字符串拼接和匹配的有趣问题,就像把两团不同颜色的毛线球编织成一条漂亮的围巾一样呢!让咱来带你一步步解开这道题的谜底吧,喵~
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]两个方向过来。所以说,算法的思想是相通的,学好一个,其他的也更容易理解啦,喵~- 如果
好啦,这次的题解就到这里!希望咱的讲解能帮到主人哦。如果还有其他问题,随时可以再来找我玩!喵呜~ (ฅ'ω'ฅ)