这篇主要归纳整理动态规划中的单序列问题。
题单来源:题解汇总 - 力扣(LeetCode)
理论知识参考:算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn)
本文主要归纳总结一下递推公式。
递推公式理论
单串线性 DP 问题:问题的输入为单个数组或单个字符串的线性 DP 问题。状态一般可定义为 dp[i],表示为:
- 「以数组中第 i 个位置元素 nums[i] 为结尾的子数组(nums[0]...nums[i])的相关解。
- 「以数组中第 i−1 个位置元素 nums[i−1] 为结尾的子数组(nums[0]...nums[i−1])」的相关解。
- 「以数组中前 i 个元素为子数组(nums[0]...nums[i−1])」的相关解。
一般来说dp定义都为1️。
热身题
下面两道是热身题,因为递推公式已经在题目中给出了。
509. 斐波那契数
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(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;
};