Skip to content

喵~ 主人,欢迎来到我的题解小站!今天我们要解决的是一道关于字符串拼接和匹配的有趣问题,就像把两团不同颜色的毛线球编织成一条漂亮的围巾一样呢!让咱来带你一步步解开这道题的谜底吧,喵~

E. Three Strings


题目大意

主人,听我给你描述一下这个任务哦。

我们有三个字符串 abc。其中,c 是通过一种特殊的方式由 ab 混合而成的。

生成 c 的过程是这样的:

  1. 一开始,c 是一个空字符串。
  2. 在每一步,我们随机从 a 或者 b 的开头取出一个字符,然后把它加到 c 的末尾。
  3. 这个过程一直持续,直到 a 或者 b 中有一个字符串被取空了。
  4. 然后,把另一个还没取完的字符串剩下的所有字符,按顺序全部加到 c 的末尾。
  5. 最后,c 中有一些字符被随机替换成了别的字符。

我们的任务是,给定最终的 abc,找出在第 5 步中,最少有多少个字符被替换了。

举个例子,如果 a = "abra"b = "cada",那么一个可能的、没有经过字符替换的 c 可以是 abracada。如果给定的 cabracade,那么最少就替换了 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 个字符的最大匹配数。现在我们想知道下一步该怎么办。

我们有两种选择:

  1. 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]))
  2. 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]))

初始状态: 当 ab 都还没取任何字符时,即 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++ 代码,咱给主人加上了详细的注释哦!

cpp
#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;
}

知识点介绍

主人,这个问题里用到的核心思想其实和一些经典算法很像哦,了解它们能让你变得更强!

  1. 动态规划 (Dynamic Programming) 动态规划是一种将复杂问题分解成更小、更简单的子问题来解决的策略。就像猫咪玩毛线球,不会一口气把整个球解开,而是一点一点地抽丝剥茧。DP 的关键在于:

    • 定义状态dp[i][j] 是什么意思?
    • 找到状态转移方程:如何从已知的子问题答案推导出更大问题的答案?
    • 确定边界/初始条件:最小的子问题答案是什么? 只要掌握了这三点,很多看起来很棘手的问题都会变得温顺起来呢!
  2. 最长公共子序列 (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 和 “所有可能的 ab 混合字符串” 之间的“最长公共子序列”。我们的 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] 两个方向过来。所以说,算法的思想是相通的,学好一个,其他的也更容易理解啦,喵~

好啦,这次的题解就到这里!希望咱的讲解能帮到主人哦。如果还有其他问题,随时可以再来找我玩!喵呜~ (ฅ'ω'ฅ)

Released under the MIT License.