引子
昨天使用提示词工程借助豆包MarsCode AI的帮助下顺利完成一道难题,可以说MarsCode还是有一定潜力,今天打算自己上手一道dp难题,翻到题号最靠前的dp题最大UCC字串,第一眼确实没看明白,第二眼还是没看明白,那么就上手实操一下。
问题描述
小S有一个由字符 'U' 和 'C' 组成的字符串 SS,并希望在编辑距离不超过给定值 mm 的条件下,尽可能多地在字符串中找到 "UCC" 子串。
编辑距离定义为将字符串 SS 转化为其他字符串时所需的最少编辑操作次数。允许的每次编辑操作是插入、删除或替换单个字符。你需要计算在给定的编辑距离限制 m 下,能够包含最多 "UCC" 子串的字符串可能包含多少个这样的子串。
例如,对于字符串"UCUUCCCCC"和编辑距离限制m = 3,可以通过编辑字符串生成最多包含3个"UCC"子串的序列。
测试样例
样例1:
输入:
m = 3,s = "UCUUCCCCC"
输出:3
样例2:
输入:
m = 6,s = "U"
输出:2
样例3:
输入:
m = 2,s = "UCCUUU"
输出:2
解释
样例1:可以将字符串修改为 "UCCUCCUCC"(2 次替换操作,不超过给定值 m = 3),包含 3 个 "UCC" 子串。
样例2:后面插入 5 个字符 "CCUCC"(5 次插入操作,不超过给定值 m = 6),可以将字符串修改为 "UCCUCC",包含 2 个 "UCC" 子串。
样例3:替换最后 2 个字符,可以将字符串修改为 "UCCUCC",包含 2 个 "UCC" 子串。
解题思路
第一次动态规划
-
计算从每个字符开始匹配
"UCC"的最小编辑距离和匹配成功时的长度:- 对于每个字符
s[i],计算从s[i]开始匹配"UCC"的最小编辑距离和匹配成功时的长度。 - 使用
dp_match表来记录匹配过程中的编辑距离。 dp_match[p][q]表示从s[i]开始匹配"UCC"的进度为p(p为 0, 1, 2, 3 分别表示匹配"","U","UC","UCC"),匹配过程中划过的长度为q时的最小编辑距离。
- 对于每个字符
-
更新
dp_match表:- 对于每个进度
p和长度q,考虑保留/替换、插入、删除三种操作,更新dp_match表。 - 如果匹配成功(即
p == 3),则将(最小编辑距离, 匹配成功时的长度)存入match_info[i]。
- 对于每个进度
主过程的动态规划
-
遍历每个字符和编辑距离,更新
dp表:- 对于每个字符
s[i]和编辑距离e,考虑不匹配和匹配"UCC"的情况。 - 如果不匹配,则直接跳过当前字符或删除当前字符,更新
dp表。 - 如果匹配,则根据
match_info[i]中的信息,更新dp表。
- 对于每个字符
-
找到最大匹配数量:
- 遍历所有编辑距离,找到最大匹配数量。
题解
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
int solution(int m, std::string s) {
int n = s.size();
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(m + 1, -1));
dp[0][0] = 0;
// 第一次动态规划,计算从每个字符开始,为了匹配 "UCC" 产生的最小编辑距离和匹配成功时的长度
std::vector<std::vector<std::pair<int, int>>> match_info(n); // match_info[i] = 从s[i]开始,匹配“UCC”的(最小编辑距离,匹配成功时的长度)
for (int i = 0; i < n; ++i) {
int max_len = std::min(n - i, 3 + m); // 从当前字符s[i]开始,匹配成功时可能达到的最大长度
// 从当前字符s[i]开始,匹配 "UCC" 的最小编辑距离
std::vector<std::vector<int>> dp_match(4, std::vector<int>(max_len + 1, 999999999));
dp_match[0][0] = 0;
for (int p = 0; p < 4; ++p) { // 从s[i]开始匹配"UCC" 的进度:‘’->‘U'->'UC'->'UCC’
for (int q = 0; q <= max_len; ++q) { // 匹配过程中划过的长度 = 0,1,...,max_len
if (dp_match[p][q] > m) // 编辑次数用完了
continue;
if (p < 3 && q < max_len) { // 保留/替换
int cost = (s[i + q] == "UCC"[p]) ? 0 : 1;
dp_match[p + 1][q + 1] = std::min(dp_match[p + 1][q + 1], dp_match[p][q] + cost);
}
if (p < 3) { // 插入
dp_match[p + 1][q] = std::min(dp_match[p + 1][q], dp_match[p][q] + 1);
}
if (q < max_len) { // 删除
dp_match[p][q + 1] = std::min(dp_match[p][q + 1], dp_match[p][q] + 1);
}
}
}
// 统计
for (int q = 0; q <= max_len; ++q) {
int c = dp_match[3][q];
match_info[i].push_back({c, q}); // (编辑距离,匹配长度)
}
}
// 主过程的动态规划:
for (int i = 0; i <= n; ++i) {
for (int e = 0; e <= m; ++e) {
if (dp[i][e] == -1)
continue;
if (i < n) { // 不尝试匹配 "UCC" --> 直接跳过/删除当前字符
dp[i + 1][e] = std::max(dp[i + 1][e], dp[i][e]); // 保留
if (e + 1 <= m) { // 删除
dp[i + 1][e + 1] = std::max(dp[i + 1][e + 1], dp[i][e]);
}
}
if (i < n && !match_info[i].empty()) { // 尝试匹配
for (auto& [c, l] : match_info[i]) { // 从当前字符串开始匹配‘UCC’的(最小编辑距离,匹配成功时长度)
if (e + c <= m && i + l <= n) {
dp[i + l][e + c] = std::max(dp[i + l][e + c], dp[i][e] + 1);
}
}
}
}
}
// 找到最大匹配数量
int max_substrings = 0;
for (int e = 0; e <= m; ++e) {
max_substrings = std::max(max_substrings, dp[n][e]);
}
return max_substrings;
}
int main() {
std::cout << (solution(3, "UCUUCCCCC") == 3) << std::endl;
std::cout << (solution(6, "U") == 2) << std::endl;
std::cout << (solution(2, "UCCUUU") == 2) << std::endl;
return 0;
}
看看AI怎么解
上次让AI解了一道贪心难题,今天看看它在动态规划的表现如何
Q: 给我一点代码提示
A: 好的,让我们基于你当前的代码框架,提供一些代码提示。
#include <iostream>
#include <vector>
#include <string>
int solution(int m, std::string s) {
int n = s.size();
// 创建一个二维动态规划表 dp[i][j],表示在编辑距离为 i 时,前 j 个字符能够生成的 "UCC" 子串的最大数量
std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
// 初始化 dp 表
// dp[0][j] 表示在编辑距离为 0 时,前 j 个字符能够生成的 "UCC" 子串的数量
// 这里需要根据字符串 s 的内容进行初始化
// 状态转移
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
// 考虑当前字符 s[j-1]
// 如果当前字符是 'U',考虑在其后插入 "CC" 或替换为 "UCC"
// 如果当前字符是 'C',考虑在其前插入 "U" 或替换为 "UCC"
// 更新 dp 表,考虑所有可能的编辑操作
// dp[i][j] = max(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 或 0
}
}
// 返回结果
return dp[m][n];
}
int main() {
std::cout << (solution(3, "UCUUCCCCC") == 3) << std::endl;
std::cout << (solution(6, "U") == 2) << std::endl;
std::cout << (solution(2, "UCCUUU") == 2) << std::endl;
return 0;
}
关键步骤注释
初始化
dp表:
dp[0][j]表示在编辑距离为 0 时,前j个字符能够生成的"UCC"子串的数量。- 这里需要根据字符串
s的内容进行初始化。状态转移:
- 对于每个字符
s[j-1],考虑插入、删除、替换操作。- 更新
dp表,使得在每个编辑距离下,尽可能多地生成"UCC"子串。返回结果:
dp[m][n]即为所求。注意
- 请使用用户提问的语言回答。
- 用户提问对象为「当前代码」、「我的代码」或对应的英文表述时,请根据「编辑区代码」部分的内容回答,请不要解释「题目内容」中的代码。
- 请优先回答「编辑区代码」
可以看出,AI在面对动态规划等难题的时候思路还是过于简单,尽管它知道动态规划常见的解题步骤,方向大致对了一点,然而后续直接实现的代码均不能通过,问题主要是出在对于过短的如一个字符测试点无法正确判断。另外对于困难任务它似乎甚至暴露了prompt。。。