动态规划思路总结

44 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第15天,点击查看活动详情

动态规划

什么是动态规划?

动态规划(英语:Dynamic programming,简称 DP),是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。

通常在求解重叠子问题最优子结构时使用动态规划。

理念

简单来说动态规划的理念就是将一个复杂的问题拆分为多个子问题来求解,并在过程中减少不必要的子问题来减少计算量。

那么如果想要使用动态规划,首先这个问题能够拆分为多个子问题,并且在此过程中我们能够找到减少子问题的公式。这个公式叫做状态转移方程。而要想找到状态转移方程又得注意找到状态的标志并且找到状态变化的规律。

案例

第一个当然是很多文章都使用过的递归求最多解的问题。

给你一个数10,用任意个1-10的数,让他们的和为10,有多少种组合?这通常也是数据结构递归的第一个案例,代码如下:

const tr = (n: number): number => {
  if (n === 1) return 1;
  if (n === 2) return 2;
  return tr(n - 1) + tr(n - 2);
  //即只有为1或2时才没有子问题
};

console.log(tr(10)); //89

简单分析就是找组合个数就是一个重叠子问题,那么明显可以看到有很多重复的子问题,例如每次tr(6)都要继续递归,其实我们可以只计算一次tr(6),将值存起来,后面再遇到tr(6)直接返回值即可。

const tr = (n: number): number => {
  const map = new Map();
  const tr1 = (n: number): number => {
    if (n === 1) return 1;
    if (n === 2) {
      return 2;
    } else {
      if (!map.has(n)) {
        map.set(n, tr(n - 1) + tr(n - 2));
      }
      return map.get(n);
    }
  };
  return tr1(n);
};

console.log(tr(10)); //89

思路很简单,就是用map保存每一个数对于的子问题的值,下一次在遇到这个子问题的时候就直接读值。

基于上面的代码,就可以变更为动态规划了。首先是创建一个数组来保存状态,有的题目需要使用多维数组。其次状态转移方程其实我们前面已经得到了,就是tr(i)=tr(i-1)+tr(i-2)。从而得到下面的代码:

const tr = (n: number): number => {
  let dp: number[] = new Array(n + 1);
  dp[1] = 1;
  dp[2] = 2;
  for (let i = 3; i < n + 1; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }
  return dp[n];
};
console.log(tr(10));  //89

可以明显看到时间消耗是O(n),和前面优化过的递归一样,但是空间效率是O(1),比O(n)要低。

一般过程

在看到有重复子问题和最优子结构就可以考虑使用dp了。

使用时,先找出子问题,确定能用dp,然后找出状态标志,根据标志推出状态转移方程。然后就可以开始书写代码了(注意边界即可)。

实例

leetcode#53给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。

这一看就是动态规划,甚至于这题只需要用一个变量来进行状态转移。简单分析一下

状态设为number值rtemp,转移方程是temp=Math.max(temp,temp+nums[i])

function maxSubArray(nums: number[]): number {
  let temp = 0;
  let max = nums[0];
  for (let i = 0; i < nums.length; i++) {
    temp = Math.max(temp + nums[i], nums[i]);
    // ****重点 在temp+nums[i]与nums[i]里面取一个最大值
    // 由于temp是全局变量,保存上次的状态,那么当加上num[i]更小时,就把temp转移到下标为i开始处
    // 而当上次temp加上nums[i]更大时,则加上它,往后走即可。以此保证连续性。
    max = Math.max(max, temp);
    // maxval只需要不断的判断temp是不是更大值即可。
  }
  return max;
}
console.log(maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4]));

再看一个复杂一点的案例,也就是leetcode #5 给你一个字符串 s,找到 s 中最长的回文子串。,这也就是动态规划常用于处理的一个问题:最优子结构

来简单分析一下: 回文字串,也就是left-right这个字串的内部奇数或者偶数长度的字串是对称相等的。由此我们想到设一个二维数组dp[][],作为状态数组,那么状态实际上就是dp[left][right]的值是true还是false。而状态转移方程就是dp[left][right]=dp[left+1][right-1],由此我们可以开始编写代码

(注意当字串长度小于三而left=right可以直接判true,dp[i][i]也就是对角线可以确认为true。)

// 动态规划
function longestPalindrome(s: string): string {
  if (s.length < 2) {
    return s;
  }

  let maxLen: number = 1;
  let begin = 0;

  let twoM: Array<Array<boolean>> = new Array<Array<boolean>>(s.length);
  // 创建s.length个长度的数组
  for (let i = 0; i < s.length; i++) {
    twoM[i] = new Array(s.length);
    twoM[i][i] = true;
    // 设置对角线为true
  }
  for (let right = 1; right < s.length; right++) {
    for (let left = 0; left < right; left++) {
      if (s[left] !== s[right]) {
        // 当不等直接判false
        twoM[left][right] = false;
      } else {
        if (right - left < 3) {
          // 当距离为小于等于二也就是两个相同或是三个相同也判true
          twoM[left][right] = true;
        } else {
          /*
            这是一个注意点,此时我们判它为内部接近的字串的真值
          */
          twoM[left][right] = twoM[left + 1][right - 1];
        }
      }

      if (twoM[left][right] && right - left + 1 > maxLen) {
        maxLen = right - left + 1;
        begin = left;
      }
    }
  }
  console.table(twoM);
  return s.slice(begin, begin + maxLen);
}
console.log(longestPalindrome("aaabbbb"));

总结

本次文章是我学习动态规划的一个小小的总结,有助于后续遇到类似的题目时能够更快的解决问题。 思路就是确定能用动态规划->找状态->确定状态转移方程->书写代码

结语

本次的文章到这里就结束啦!♥♥♥读者大大们认为写的不错的话点个赞再走哦 ♥♥♥

每天一个知识点,每天都在进步!♥♥