算法整理|动态规划之单序

138 阅读8分钟

这篇主要归纳整理动态规划中的单序列问题。

题单来源:题解汇总 - 力扣(LeetCode)

理论知识参考:算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn)

本文主要归纳总结一下递推公式。

递推公式理论

单串线性 DP 问题:问题的输入为单个数组或单个字符串的线性 DP 问题。状态一般可定义为 dp[i],表示为:

  1. 「以数组中第 i 个位置元素 nums[i] 为结尾的子数组(nums[0]...nums[i])的相关解。
  2. 「以数组中第 i−1 个位置元素 nums[i−1] 为结尾的子数组(nums[0]...nums[i−1])」的相关解。
  3. 「以数组中前 i 个元素为子数组(nums[0]...nums[i−1])」的相关解。

一般来说dp定义都为1️。

热身题

下面两道是热身题,因为递推公式已经在题目中给出了。

509. 斐波那契数

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

递推公式:F(n) = F(n - 1) + F(n - 2)

题解:

由于F(n)每次只和前两个状态,也就是F(n - 1)和F(n - 2)有关,又可以简化空间只记录这两个状态。

var fib = function (n) {
  if (n == 0 || n == 1) return n;
  let l = 0;
  let m = 1;
  for (i = 2; i < n + 1; i++) {
    let temp = m;
    m = m + l;
    l = temp;
  }
  return m;
};

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

题解:

设dp为爬到第i个台阶的方法数。则dp[i]=dp[i-1]+dp[i-2]。也就是斐波那契。区别是初始值:dp[0]是1。

var climbStairs = function(n) {
 if (n == 0 || n == 1) return n;
  let l = 1;
  let m = 1;
  for (i = 2; i < n + 1; i++) {
    let temp = m;
    m = m + l;
    l = temp;
  }
  return m;
};

746. 使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

题解:

dp[i]为爬到台阶i的最小花费。动态规划中的递推公式实际上是指状态如何转移。我们可以找到当前状态可以由哪些状态转移而来。第i个台阶可以从i-1个台阶爬一个阶梯而来;也可以从i-2个台阶爬2个阶梯而来。所以状态只有两个,也可以优化只记录这两个状态:


dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
var minCostClimbingStairs = function(cost) {
    n=cost.length
 if (n == 0 || n == 1) return n;
  let l = 0;
  let m = 0;
  for (i = 2; i < n+1; i++) {
    let temp = m;
    m = Math.min(m+cost[i-1],l+cost[i-2]);
    l = temp;
  }
  return m;
};

连续子区间问题

由于子区间连续,dp定义必须是以nums[i]为结尾的子序列,然后结合题目要求的最值。上一状态满足要求时更新;不满足时由于连续,则状态清零,从当前值开始;结果为dp中的最值。

dp[i]=max(nums[i],dp[i-1]+nums[i]),结果: max(dp)

如果题目要求总个数,即为sum(dp)。以nums[i]为结尾的所有子序列的和即为总个数。

53. 最大子数组和

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

子数组 是数组中的一个连续部分。

题解: 设dp[i]为以nums[i]为结尾的连续子数组的最大和。dp[i-1]+nums[i]>nums[i]则扩充。

dp[i]=max(dp[i-1]+nums[i],nums[i]),只需要记录上一个状态:dp[i-1]。

var maxSubArray = function(nums) {
    let pre=0
    let max=-Infinity
    for(let i=0;i<nums.length;i++){
        pre=Math.max(nums[i],pre+nums[i])
        if(pre>max) max=pre
    }
    return max
};

2606. 找到最大开销的子字符串

给你一个字符串 s ,一个字符 互不相同 的字符串 chars 和一个长度与 chars 相同的整数数组 vals 。

子字符串的开销 是一个子字符串中所有字符对应价值之和。空字符串的开销是 0 。

字符的价值 定义如下:

  • 如果字符不在字符串 chars 中,那么它的价值是它在字母表中的位置(下标从 1 开始)。

    • 比方说,'a' 的价值为 1 ,'b' 的价值为 2 ,以此类推,'z' 的价值为 26 。
  • 否则,如果这个字符在 chars 中的位置为 i ,那么它的价值就是 vals[i] 。

请你返回字符串 s 的所有子字符串中的最大开销。

题解:

就是最大子数组和,只不过每个字符的价值还需要计算。

var maximumCostSubstring = function(s, chars, vals) {
    const getValue = (ch) => {
    let index = chars.indexOf(ch);
    if (index !== -1) {
      return vals[index];
    } else {
      return ch.charCodeAt() - "a".charCodeAt() + 1;
    }
  };
   let pre=0
   let max=0
  for (let i = 0; i < s.length; i++) {
    let v = getValue(s[i]);
    pre = Math.max(v, pre + v);
    if(max<pre) max=pre
  }
  return max;
};

413. 等差数列划分

如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。

  • 例如,[1,3,5,7,9][7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。

给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。

子数组 是数组中的一个连续序列。

题解:

设dp[i]为以nums[i]为结尾的等差子数组个数。

子序列要求连续,所以满足等差数列条件,即nums[i]-nums[i-1]==nums[i-1]-nums[i-2]时,找dp[i-1]的状态,该子序列扩增1个,dp[i]=dp[i-1]+1,即以nums[i]结尾的等差数列个数。如果不满足要求,则子序列从i 处断开,dp[i]=0。最终结果为sum(dp)

var numberOfArithmeticSlices = function (nums) { 
    let dp = new Array(nums.length).fill(0); 
    let sum = 0; for (let i = 2; i < nums.length; i++) { 
        if (nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]) { 
            dp[i] = dp[i - 1] + 1; 
            sum += dp[i]; 
        } 
    } 
    return sum; 
};

673. 最长递增子序列的个数

给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。

注意 这个数列必须是 严格 递增的。

题解:

只能返回最长的子序列的个数。在300.最长递增子序列的基础上还需要构造一个count数组记录最长子序列的个数,当dp[i]为当前最长子序列的长度时,count[i]就为这些数组的个数和。

递推:

dp[i]=max(dp[j]+1,dp[i]) ,就是300.最长递增子序列

count[i]=+count[j] ,当j满足dp[j]+1==dp[i]的条件时,否则,count[i]=count[j]。

var findNumberOfLIS = function(nums) {
    let n = nums.length, maxLen = 0, ans = 0;
    const dp = new Array(n).fill(0);
    const cnt = new Array(n).fill(0);
    for (let i = 0; i < n; ++i) {
        dp[i] = 1;
        cnt[i] = 1;
        for (let j = 0; j < i; ++j) {
            if (nums[i] > nums[j]) {
                if (dp[j] + 1 > dp[i]) {
                    dp[i] = dp[j] + 1;
                    cnt[i] = cnt[j]; // 重置计数
                } else if (dp[j] + 1 === dp[i]) {
                    cnt[i] += cnt[j];
                }
            }
        }
        if (dp[i] > maxLen) {
            maxLen = dp[i];
            ans = cnt[i]; // 重置计数
        } else if (dp[i] === maxLen) {
            ans += cnt[i];
        }
    }
    return ans;
};

最长子序列

不需要连续,一般需要遍历[0,i-1]找到满足需求的上一个状态。

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

1218. 最长定差子序列

给你一个整数数组 arr 和一个整数 difference,请你找出并返回 arr 中最长等差子序列的长度,该子序列中相邻元素之间的差等于 difference 。

子序列 是指在不改变其余元素顺序的情况下,通过删除一些元素或不删除任何元素而从 arr 派生出来的序列。 和

题解:

这两题都是求最长子序列,区别是子序列要求的条件不一样。

递增子序列:满足nums[i]>nums[j],dp[i]=max(dp[j]+1,dp[i])

var lengthOfLIS = function (nums) {
  let dp = new Array(nums.length).fill(1);
  for (let i = 0; i < nums.length; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[i] > nums[j]) {
        dp[i] = Math.max(dp[i], dp[j] + 1);
      }
    }
  }
  return Math.max(...dp);
};

定差子序列:满足nums[i]-nums[j]==difference,dp[i]=max(dp[j]+1,dp[i])。由于定差,可以用哈希表,数作为键,子序列长度作为值,通过nums[i]-difference查询上一个满足要求的数。

var longestSubsequence = function (arr, difference) {
  let n = arr.length;
  let dp = new Map();
  let ans = 0;
  for (let i = 0; i < n; i++) {
    dp.set(arr[i], (dp.get(arr[i] - difference) || 0) + 1);
    ans = Math.max(ans, dp.get(arr[i]));
  }
  return ans;
};