AI辅助刷题实践 | 豆包MarsCode AI刷题

61 阅读5分钟

参加青训营这段时间体验了利用豆包MarsCode辅助刷题,这带来了不少好处,比如:

  • 提供解题思路,豆包MarsCode可以给出一些解题思路和算法步骤,
  • 及时发现问题,AI可以立即检查代码正确性,发现一些潜在的思路错误并提出改进意见
  • 代码优化,能够自动分析时间空间复杂度,提供优化建议
  • 多语言支持,可以根据需求转换成合适的语言。比如目前AI刷题平台仅支持python、java、c++提测,我就需要利用AI将JavaScript代码转换成其中的一种

image.png 下面以[字符串趋同最小代价问题](字符串趋同最小代价问题 - MarsCode)题目为例,感受一下AI带来的刷题效率提升。

问题描述
小U 和 小R 各自拥有一个长度相等的二进制字符串 A 和 B。现在,他们想要将这两个字符串修改成相同的字符串。每次修改可以选择以下两种操作:

  1. 交换同一个字符串中的任意两个字符,交换操作的成本为它们索引之差的绝对值 |i - j|
  2. 对某个字符进行取反操作,取反的成本为 2。

小U 和 小R 想知道,将字符串 A 和 B 修改为相同字符串的最小总成本是多少?

测试样例
样例1:

输入:str1 = "10001",str2 = "10000"
输出:2

样例2:

输入:str1 = "100100",str2 = "100001"
输出:2

样例3:

输入:str1 = "1010",str2 = "0111"
输出:3

样例4:

输入:str1 = "1100",str2 = "0011"
输出:4

题目分析
我最初的想法是由于交换的成本是索引之差,那么交换好两个字符的成本最低为1。而对某个字符进行取反操作,取反成本为2,那么让两个字符正确的成本是4,也就是说当两个字符索引之差小于等于4且能交换的时候,就选择交换,否则选择让两个字符都取反。根据这个思路编写了如下代码:

function solution(str1, str2) {
    // 找到不同的位
    let diss = []
    for(let i=0; i<str1.length; i++){
        if(str1[i] !== str2[i]){
            diss.push(i)
        }
    }

    // 如果相邻且不同,则交换
    // 剩下的取反
    let res = 0;
    let flag = false;  // 用于标记最后一位是否取反
    if(diss.length < 2) return diss.length*2;
    for(let i=0; i<diss.length-1; i++){
        if(str1[diss[i]] !== str1[diss[i+1]]){
            res += 1
            flag = true;
            i++;
        }else{
            res += 2
            flag = false
        }
    }

    if(!flag){
        res += 2;
    }

    return res;
}

但很遗憾的是,测试用例只通过了一个,AI给出了如下建议:

image.png 对于交换逻辑,AI提出我只考虑了相邻的不同位进行交换,检查代码发现确实如此,与我最初的想法有差异,交换条件应该改为交换成本小于等于4。为了便于找到不同位,计算交换成本,将0变1和1变0的索引分别存储到两个数组中。修改后代码如下:

function solution(str1, str2) {
    // 找到不同的位
    let diss0 = []; // 需要从0变1的索引
    let diss1 = []; // 需要从1变0的索引
    for (let i = 0; i < str1.length; i++) {
        if (str1[i] !== str2[i]) {
            if (str1[i] === '0') {
                diss0.push(i);
            } else {
                diss1.push(i);
            }
        }
    }

    // 如果不同的位数为0或1,直接返回取反的成本
    if (diss0.length + diss1.length <= 1) return (diss0.length + diss1.length) * 2;

    // 计算交换和取反的最小成本
    let res = 0;
    let i = 0, j = 0;
    while (i < diss0.length && j < diss1.length) {
        // 如果可以交换且成本小于等于4
        if (Math.abs(diss0[i] - diss1[j]) <= 4) {
            res += Math.abs(diss0[i] - diss1[j]); // 交换成本
            i++;
            j++;
        } else {
            // 否则取反
            res += 2; // 取反成本
            if (diss0[i] < diss1[j]) {
                i++;
            } else {
                j++;
            }
        }
    }

    // 处理剩余的位
    while (i < diss0.length) {
        res += 2; // 取反成本
        i++;
    }
    while (j < diss1.length) {
        res += 2; // 取反成本
        j++;
    }

    return res;
}

结果提测遇到一个测试用例不通过:

输入:str1 = "1101000000001101",str2 = "0010110111100010"
输出:17

调试代码发现,交换的判定条件有一定的局限性,比如当索引为1,3的字符需要从0变为1,索引为5,7的字符需要从1变为0。根据我们的代码逻辑应该交换索引为1和5的字符,交换索引为3和7的字符,代价为8。但实际上可以让索引为1和7的字符取反,交换索引为3和5的字符,这样代价最终为6。因此我们的实现思路存在问题,针对这种情况可以考虑使用动态规划思想解决。告诉AI我们的想法就能自动分析并解决问题。

image.png 甚至可以提问AI“可以进一步优化吗”,就能得到优化后的结果

image.png AI生成代码如下:

function solution(str1, str2) {
    // 找到不同的位
    let diff_id = [];
    for (let i = 0; i < str1.length; i++) {
        if (str1[i] !== str2[i]) {
            diff_id.push(i);
        }
    }
    let m = diff_id.length;
    if (m === 0) {
        return 0;
    }

    // 初始化dp数组
    let dp = new Array(m + 1).fill(0);
    dp[1] = 2; // 一个不同的位取反的成本

    // 动态规划状态转移
    for (let i = 2; i <= m; i++) {
        dp[i] = dp[i - 1] + 2; // 取反操作
        if (str1[diff_id[i - 2]] !== str1[diff_id[i - 1]]) {
            dp[i] = Math.min(dp[i], dp[i - 2] + Math.abs(diff_id[i - 1] - diff_id[i - 2])); // 交换操作
        }
        if (i >= 4 && str1[diff_id[i - 3]] !== str1[diff_id[i - 1]] && str1[diff_id[i - 4]] !== str1[diff_id[i - 2]]) {
            dp[i] = Math.min(dp[i], dp[i - 4] + Math.abs(diff_id[i - 1] - diff_id[i - 3]) + Math.abs(diff_id[i - 2] - diff_id[i - 4])); // 多步交换操作
        }
    }

    return dp[m];
}

然后利用AI改成python并提交,用例全部通过

image.png