动态规划 | 豆包MarsCode AI 刷题

113 阅读7分钟

>>动态规划 (用空间去换时间)

状态转移 : 穷举每个状态,把对所有状态的组合穷举出来,然后对每个状态的组合求最值

(能画递归树的前提是 满足最优子结构) :什么是最优子结构? 比如 ,我要统计全校的最高分,我只要知道每个班的最高分再做比较即可 ; 但是如果我要统计全校的最大分差,我只知道每个班的最高分差是无法判断全校的分差

1.斐波那契数列(不是正宗的dp)

递归算法的时间复杂度 = 递归函数调用的次数 x * 递归函数本身的次数

此递归树的时间复杂度 为 2 的 k 次方. f(18 ) , f (17 ) 存在两次冗余计算

以下为动态规划求斐波那契数列

//动态规划求斐波那契数列
function fib(n){
    //启用备忘录
    let arr = new Array(n + 1).fill(0);
    return helper(arr,n);
}
​
function helper(arr,n){
    if(n == 1 || n== 0){
        return n
    }
​
    if(arr[n] != 0){
        return arr[n];
    }
    else{
        arr[n] = helper(arr,n - 1) + helper(arr,n - 2);
        return arr[n];
    }
}
console.log(fib(5))
//时间复杂度为 O(n)//这是自顶向下的递归函数

接了下来我们来看自底向上的函数来实现斐波那契

function fib(n){
    if(n == 0){
        return 0
    }
    let dp = new Array( n + 1).fill(0);
    dp[0] = 0;
    dp[1] = 1;
    let m;
    //这里的for循环相当于上面算法的递归,不过这是自底向上的函数
    for( m= 2 ;m <= n ; m++){
        //状态转移
        dp[m] = dp[m - 1] + dp[m - 2];
    }
    return dp[n]
}
console.log(fib(5))
​
//优化!!!!!!!
​
function fib(n){
    if(n == 0){
        return 0
    }
    let dp = new Array( n + 1).fill(0);
    dp[0] = 0;
    dp[1] = 1;
    let m;
    //这里的for循环相当于上面算法的递归,不过这是自底向下的函数
    // for( m= 2 ;m <= n; m++){
    //     //其实我们这里发现dp数组有两个长度就够了 !!!!!
    //     dp[m] = dp[m - 1] + dp[m - 2];
    // }
​
    let pre = 0;
    let cur = 1;
    // 优化如下
    for( m= 2 ;m <= n; m++){
        //其实我们这里发现dp数组有两个长度就够了
        dp[m] = pre + cur;
        pre = cur;
        cur = dp[m];
    }
​
    return dp[n]
}
console.log(fib(5))

2.动态规划特点:

  1. 重叠子问题
  2. 状态转移方程
  3. 最优子结构

题型:求最值

核心:穷举

解题思路:

  1. 明确状态
  2. 明确选择
  3. 明确dp函数/数组的定义
  4. 明确 base case

好,接下来我们来看下一道例题:

3.数字分组求偶数和

问题描述:

小M面对一组从 1 到 9 的数字,这些数字被分成多个小组,并从每个小组中选择一个数字组成一个新的数。目标是使得这个新数的各位数字之和为偶数。任务是计算出有多少种不同的分组和选择方法可以达到这一目标。

  • numbers: 一个由多个整数字符串组成的列表,每个字符串可以视为一个数字组。小M需要从每个数字组中选择一个数字。

例如对于[123, 456, 789],14个符合条件的数为:147 149 158 167 169 248 257 259 268 347 349 358 367 369

测试样例

样例1:

输入:numbers = [123, 456, 789] 输出:14

样例2:

输入:numbers = [123456789] 输出:4

样例3:

输入:numbers = [14329, 7568]

输出:10

OK,让我来解释一下为什么要用动态规划,为什么会想到动态规划

1.问题的性质:

  • 组合问题:这是一个组合问题,涉及到从多个选项中选择并计算结果。
  • 最优子结构:问题的解可以分为子问题的解。 例如: 选择前i 个数字组中的数字,使得和为偶数,可以分解为选择前 i - 1个数字组中的数字 ,并考虑第i 个数字组中的选择

2.状态定义:

  • 状态表示: 定义状态dpi 表示前i 个数组 中选择数字,使得和为偶数的组合数;

    dpi表示前i 个数组中选择数字,使得和为奇数的组合数

  • 状态转移 :根据当前数字组 的选择,更新状态,

  • 如果前i - 1个数字组中的和为偶数,选择当前数字组中的偶数会使得和保持为偶数,选择奇数使得和变为奇数。反之亦然

3.状态转移方程

  • 偶数状态转移

    • dp[i][0] = dp[i-1][0] * new_even + dp[i-1][1] * new_odd
    • 解释:前 i-1 个数字组的和为偶数时,选择当前数字组中的偶数会使和保持为偶数;前 i-1 个数字组的和为奇数时,选择当前数字组中的奇数会使和变为偶数。
  • 奇数状态转移

    • dp[i][1] = dp[i-1][0] * new_odd + dp[i-1][1] * new_even
    • 解释:前 i-1 个数字组的和为偶数时,选择当前数字组中的奇数会使和变为奇数;前 i-1 个数字组的和为奇数时,选择当前数字组中的偶数会使和保持为奇数。

4.初始状态

  • dp0 = 1,表示没有选择任何数字时,和为0的组合数为1。

5.最终结果

  • dpn,表示前n` 个数字组中选择数字,使得和为偶数的组合数。
function solution(numbers) {
  const n = numbers.length;
​
  // 初始化奇数和偶数计数数组
  const oddCounts = numbers.map(num => num.split('').filter(digit => digit % 2 !== 0).length);
  const evenCounts = numbers.map(num => num.split('').filter(digit => digit % 2 === 0).length);
​
  // 初始化 dp 数组
  const dp = new Array(n + 1).fill(0).map(() => [0, 0]);
  dp[0][0] = 1; // 初始状态,没有选择任何数字时,和为0的组合数为1
​
  for (let i = 1; i <= n; i++) {
    const [prevEven, prevOdd] = dp[i - 1];
    const [newEven, newOdd] = [evenCounts[i - 1], oddCounts[i - 1]];
​
    // 选择偶数
    dp[i][0] = prevEven * newEven + prevOdd * newOdd;
    dp[i][1] = prevEven * newOdd + prevOdd * newEven;
  }
​
  return dp[n][0];
}
​
function main() {
  console.log(solution(["123", "456", "789"])); // 输出 14
  console.log(solution(["123456789"])); // 输出 4
  console.log(solution(["14329", "7568"])); // 输出 10
}
​
main();

4.DNA序列编辑问题

题目如下:

小R正在研究DNA序列,他需要一个函数来计算将一个受损DNA序列(dna1)转换成一个未受损序列(dna2)所需的最少编辑步骤。编辑步骤包括:增加一个碱基、删除一个碱基或替换一个碱基。


测试样例

样例1:

输入:dna1 = "AGT",dna2 = "AGCT" 输出:1

样例2:

输入:dna1 = "AACCGGTT",dna2 = "AACCTTGG" 输出:4

样例3:

输入:dna1 = "ACGT",dna2 = "TGC" 输出:3

样例4:

输入:dna1 = "A",dna2 = "T" 输出:1

样例5:

输入:dna1 = "GGGG",dna2 = "TTTT" 输出:4

现在我来假定一个测试用例: dna1 = "AGCT",dna2 = "ACGT"

OK,我们来❀一个图:

OK,我们来看代码:

function solution(dna1, dna2) {
  const m = dna1.length;
  const n = dna2.length;
  
  // 创建一个 (m+1) x (n+1) 的二维数组 dp
  const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
  
  // 初始化 dp 数组
  for (let i = 0; i <= m; i++) {
    dp[i][0] = i;
  }
  for (let j = 0; j <= n; j++) {
    dp[0][j] = j;
  }
  
  // 填充 dp 数组
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (dna1[i - 1] === dna2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1];
      } else {
        dp[i][j] = Math.min(
          dp[i - 1][j] + 1, // 删除
          dp[i][j - 1] + 1, // 插入                                                                         
          dp[i - 1][j - 1] + 1 // 替换
        );
      }
    }
  }
  
  // 返回最终结果
  return dp[m][n];
}
​
​
​
function main() {
  //  You can add more test cases here
  // console.log(solution("AGCTTAGC", "AGCTAGCT") === 2);
  console.log(solution("AGCCGAGC", "GCTAGCT") === 4);
}
​
main();
额外学到的

使用 &运算检查 state 的第 i 位置 是否为 1

比如 现在 的 state 为 0b101,我们想要检查 state 的第一位是否为 1,我们可以试图这样做

state & (1 << 0)

规律即为 想要证明state 的第i 位(从右往左) 是否为 1 ,做此运算

state & (1 << (i - 1))

使用 | 运算 使 state 的第 i 位变为 1

依旧是state 0b101 ,我们想要让第二位变成 1 ,做运算 |

state | (1 << 1)  //测试state111,十进制7

规律即为 想要证明state 的第i 位(从右往左) 是否为 1 ,做此运算

state & (1 << (i - 1))