什么是动态规划:动态规划中每一个状态一定是由上一个状态推导出来的,然后得出全局最优。如果某一问题有很多重叠子问题,使用动态规划是最有效的。(而贪心没有状态推导,是从局部直接选最优的。)
动态规划五部曲:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
先考虑自顶向下暴力解法(递归),得出状态转移方程(定义 dp 数组/函数的含义),加入备忘录消除重叠子问题,再将其转化为自顶向上的解法(迭代)。
509. 斐波那契数
//普通动态规划var fib = function(n) {let dp=Array(n+1).fill(0);dp[1]=1for(let i =2;i<=n;i++){dp[i]=dp[i-1]+dp[i-2];}return dp[n]};//优化空间复杂度var fib = function(n) {let p=0,q=1,r;if(n<2) return n;for(let i =2;i<=n;i++){r=p+q;p=q;q=r;}return r;};
70. 爬楼梯
//普通动态规划// var climbStairs = function(n) {// let dp=new Array(n+1).fill(0);// dp[0]=1;// let choices=[1,2]// for(let i = 1;i <= n; i++){// for(let c of choices){// if(i<c) continue;// dp[i]=dp[i]+dp[i-c];// console.log(i,dp[i])// }// }// return dp[n]// };//简单动态规划// var climbStairs = function(n) {// let dp=new Array(n+1).fill(0);// dp[1]=1;// dp[2]=2;// for(let i = 3;i <= n; i++){// dp[i]=dp[i-1]+dp[i-2];// }// return dp[n]// }//优化空间复杂度,var climbStairs = function(n) {let p = 0, q = 0, r = 1;for (let i = 1; i <= n; ++i) {p = q;q = r;r = p + q;}return r;};
放苹果
62. 不同路径
var uniquePaths = function(m, n) {let dp=Array(n).fill(1);for(let i=1;i<m;i++){for(let j=1;j<n;j++){//dp[i][j]=dp[i][j-1]+dp[i-1][j]dp[j]=dp[j-1]+dp[j] //从二维变成一维}}return dp[n-1]};
198. 打家劫舍
/*** @param {number[]} nums* @return {number}*/var rob = function(nums) {let dp=Array(nums.length+1).fill(0);dp[1]=nums[0];for(let i=2;i<nums.length+1;i++){dp[i]=Math.max(dp[i-2]+nums[i-1],dp[i-1])}return Math.max(...dp)};
213. 打家劫舍 II
/*** @param {number[]} nums* @return {number}*/var rob = function(nums) {const length=nums.length;if(length<2) return nums[0];return Math.max(dynamic(nums.slice(0,length-1)),dynamic(nums.slice(1,length)))};var dynamic =function(nums){let pre1=0,pre2=nums[0],cur;let result=pre2;for(let i=1;i<nums.length;i++){cur=Math.max(pre2,pre1+nums[i])result=Math.max(result,cur);pre1=pre2;pre2=cur;}return result;}
132. 分割回文串 II
好难啊,脑子绕绕绕
要找到状态转移方程!依赖前一个的结果
var minCut = function(s) {const length=s.length;let isPalindromic=Array.from(Array(length),()=>Array(length).fill(false))for(let i=length-1;i>=0;i--){for(let j=i;j<length;j++){if(s[i]===s[j]){if(j-i<=1){isPalindromic[i][j]=true}else if(isPalindromic[i+1][j-1]){isPalindromic[i][j]=true;}}}}let dp=Array(length) //范围是[0, i]的回文子串,最少分割次数是dp[i]。for(let i = 0; i < length; i++) dp[i] = i;for(let i=1;i<length;i++){if(isPalindromic[0][i]){dp[i]=0continue;}for(let j =0;j<i;j++){if(isPalindromic[j+1][i]){dp[i]=Math.min(dp[i],dp[j]+1)}}}return dp[length-1];};
背包问题
0-1背包
每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
for(int i = 0; i < weight.size(); i++) { // 遍历物品for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);}}
416. 分割等和子集
/*** @param {number[]} nums* @return {boolean}*/var canPartition = function(nums) {//两个子集元素相等,所以一个子集的和为总sum/2let sum=nums.reduce((pre,cur)=>pre+cur,0);if(sum%2!==0) return false;let dp=Array(sum/2+1).fill(false);dp[0]=true;for(let j=0;j<nums.length;j++){for(let i=sum/2;i>=1;i--){ //为了防止同一元素用了多次if(nums[j]>i) break;dp[i]=dp[i] || dp[i-nums[j]] //注意和dp[i]或处理}}return dp[sum/2]};
474. 一和零
/*** @param {string[]} strs* @param {number} m* @param {number} n* @return {number}*/var findMaxForm = function(strs, m, n) {const dp = Array.from(Array(m+1),()=>Array(n+1).fill(0))for(let str of strs){let zeros=0,ones=0;for (let s of str) {s==='0' ? zeros++ :ones++;}for(let i=m;i>=zeros;i--){for(let j=n;j>=ones;j--){dp[i][j]=Math.max(dp[i][j],dp[i-zeros][j-ones]+1);}}}return dp[m][n];}
494. 目标和
var findTargetSumWays = function(nums, target) {const sum=nums.reduce((pre,n)=>pre+n);//s(p)-s(n)=target//s(p)+s(n)+ s(p)-s(n)=sum + target//2 * s(p)= sum + targetconst to=(sum + target)/2;if(to < 0 || to % 1 !== 0 ) return 0;let dp=new Array(to+1).fill(0);dp[0] = 1;for(let num of nums){for(let i=to;i>=num;i--){ //本质还是01背包问题dp[i] += dp[i-num];}}return dp[to];};
最后一块石头的重量 II
将问题转换为0-1背包,与分割等和子集类似。
难点在于想到0-1背包,因为分成石头总重相近的两块结果最优
var lastStoneWeightII = function (stones) {let total = stones.reduce((pre, cur) => pre + cur, 0);let target = Math.floor(total / 2);let dp = Array(target + 1).fill(0);for (let i = 0; i < stones.length; i++) {for (let j = target; j >= 0; j--) {if (j - stones[i] >= 0) {dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i])}}}return total - dp[target] * 2};
完全背包
每件物品都有无限个(也就是可以放入背包多次)
01背包和完全背包唯一不同就是体现在遍历顺序上
// 先遍历物品,再遍历背包for(int i = 0; i < weight.size(); i++) { // 遍历物品for(int j = weight[i]; j < bagWeight ; j++) { // 遍历背包容量dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);}}
279. 完全平方数
//这题遍历顺序不重要,都能得到结果//以下为先遍历背包,再遍历物品var numSquares = function(n) {let dp=Array(n+1).fill(Infinity);dp[0]=0;for(let i=1;i<=n;i++){for(let j =1;j*j<=n;j++){if(j*j>i) break;dp[i]=Math.min(dp[i],dp[i-j*j]+1)}}return dp[n]};
139. 单词拆分
/*** @param {string} s* @param {string[]} wordDict* @return {boolean}*/var wordBreak = function(s, wordDict) {let dp=new Array(s.length).fill(false);dp[0]=true;for(let i=1;i<=s.length;i++){for(let j=0;j<wordDict.length;j++){const length=wordDict[j].lengthif(i<length) continue;dp[i]=dp[i-length] && s.substring(i-length,i)===wordDict[j]if(dp[i]) break;}}return dp[s.length];};
排列背包
组合不强调顺序,(1,5)和(5,1)是同一个组合。
排列强调顺序,(1,5)和(5,1)是两个不同的排列。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
因为如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!
如果求最小数,那么两层for循环的先后顺序就无所谓了,因为(1,5)和(5,1)都是2。
518. 零钱兑换 II
这题就是在求组合数。
/*** @param {number} amount* @param {number[]} coins* @return {number}*/var change = function(amount, coins) {let dp=Array(amount+1).fill(0)dp[0]=1;for(let i=0;i<coins.length;i++){for(let j=1;j<=amount;j++){if(coins[i]>j) continuedp[j]=dp[j]+dp[j-coins[i]]}}return dp[amount]};
377. 组合总和 Ⅳ
这道题提示 顺序不同的序列被视作不同的组合。所以是求排列数
/*** @param {number[]} nums* @param {number} target* @return {number}*/var combinationSum4 = function(nums, target) {let dp=Array(target+1).fill(0);dp[0]=1;//注意,可以理解为target为0时也算一种组合个数for(let i=1;i<=target;i++){for(let j=0;j<nums.length;j++){if(nums[j]>i) continue;dp[i]=dp[i]+dp[i-nums[j]]}}return dp[target];};
子序列问题
https://mp.weixin.qq.com/s/zNai1pzXHeB2tQE6AdOXTA
子序列是不连续的。一旦涉及到子序列和最值,那几乎可以肯定,考察的是动态规划技巧,时间复杂度一般都是 O(n^2)。遇到子序列问题,首先想到两种动态规划思路,然后根据实际问题看看哪种思路容易找到状态转移关系。找到状态转移和 base case 之后,一定要观察 DP table,看看怎么遍历才能保证通过已计算出来的结果解决新的问题。
1、第一种思路模板是一个一维的 dp 数组:
在子数组array[0..i]中,以array[i]结尾的目标子序列(最长递增子序列)的长度是dp[i]。
int n = array.length;int[] dp = new int[n];for (int i = 1; i < n; i++) {for (int j = 0; j < i; j++) {dp[i] = 最值(dp[i], dp[j] + ...)}}
2、第二种思路模板是一个二维的 dp 数组:
int n = arr.length;int[][] dp = new dp[n][n];for (int i = 0; i < n; i++) {for (int j = 1; j < n; j++) {if (arr[i] == arr[j])dp[i][j] = dp[i][j] + ...elsedp[i][j] = 最值(...)}}
本思路中 dp 数组含义又分为「只涉及一个字符串」和「涉及两个字符串」两种情况。
2.1 涉及两个字符串/数组时(比如最长公共子序列)
在子数组arr1[0..i]和子数组arr2[0..j]中,我们要求的子序列(最长公共子序列)长度为dp[i][j]。
2.2 只涉及一个字符串/数组时(比如最长回文子序列)
在子数组array[i..j]中,我们要求的子序列(最长回文子序列)的长度为dp[i][j]
1143. 最长公共子序列

var longestCommonSubsequence = function(text1, text2) {const n = text1.length, m = text2.length;let dp=Array(n+1).fill(0).map(()=> new Array(m+1).fill(0));//let dp= Array.from(new Array(n+1),() => new Array(m+1).fill(0));for(let i =1;i<=n;i++){for(let j=1;j<=m;j++){if(text1[i-1]===text2[j-1]){dp[i][j]=dp[i-1][j-1]+1;}else{dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j])}}}return dp[n][m];};
647. 回文子串
var countSubstrings = function(s) {const length=s.length;let result=0;let dp=Array.from(Array(length),()=>Array(length).fill(false))for(let i=length-1;i>=0;i--){for(let j=i;j<length;j++){if(s[i]===s[j]){if(j-i<=1){dp[i][j]=trueresult++;}else if(dp[i+1][j-1]){dp[i][j]=true;result++;}}}}return result;}//双指针var countSubstrings = function(s) {const n = s.length;var res = 0;for (let i = 0; i < 2 * n -1; i++) {let L = Math.floor( i/ 2);let R = Math.floor(i / 2) + i % 2;while(L >= 0 && R < n && s.charAt(L) == s.charAt(R)) {L--;R++;res++}}return res};
516. 最长回文子序列
与最长回文子串的区别,回文子串也可以用动态规划,但用双指针(从中心向两端扩展)更高效。
var longestPalindromeSubseq = function (s) {const length = s.length;let dp = Array.from(Array(length), () => Array(length).fill(0));for (let i = 0; i < length; i++) {dp[i][i] = 1;}for (let l = length - 1; l >= 0; l--) {for (let r = l + 1; r < length; r++) {//状态转移方程if (s[l] === s[r]) {dp[l][r] = dp[l + 1][r - 1] + 2;} else {dp[l][r] = Math.max(dp[l][r - 1], dp[l + 1][r]);}}}return dp[0][length - 1];};
300. 最长递增子序列
既然是递增子序列,我们只要找到前面那些结尾比 当前3 小的子序列,然后把 3 接到最后,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。
function lengthOfLIS(nums: number[]): number {const n = nums.length;let dp = Array(n).fill(1);for (let i = 1; i < n; i++) {for (let j = 0; j < i; j++) {if (nums[j] < nums[i]) {dp[i] = Math.max(dp[i], dp[j] + 1);}}}return Math.max(...dp);}
674. 最长连续递增序列
//easy版最长递增子序列//先用dp备忘录,然后改成O(1)变量var findLengthOfLCIS = function(nums) {let p=0,q=0,result=0;for(let i=0;i<nums.length;i++){if(nums[i]>p){q=q+1;if(q>result)result=q;}else{q=1;}p=nums[i]}return result;};
72. 编辑距离
53. 最大子数组和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 示例: 输入: [-2,1,-3,4,-1,2,1,-5,4] 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
可以使用动态规划,也可以使用贪心算法
动态规划
dp[i]:包括下标i之前的最大连续子序列和为dp[i],即 i为终点的连续最大子序列和
状态转移公式:
dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
nums[i],即:从头开始计算当前连续子序列和
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
//动态规划var maxSubArray = function (nums) {const n = nums.length;let dp = Array(n).fill(0);dp[0] = nums[0];for (let i = 1; i < n; i++) {dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);}return Math.max(...dp);};
贪心算法
var maxSubArray = function(nums) {let ans = nums[0];let sum = 0;for(const num of nums) {if(sum > 0) {sum += num;} else {sum = num;}ans = Math.max(ans, sum);}return ans;};
股票问题
https://mp.weixin.qq.com/s/4nqJMIyCKQD7IJ-HI6S3Vg
188. 买卖股票的最佳时机 IV
以这道题分析,之后的所有股票题都在这道基础上改编
var maxProfit = function (k, prices) {//dp[天数][至今最大交易数][是否持有]//dp[i][j][0]=Math.max(dp[i-1][j][1]+price[i],dp[i-1][j][0])//dp[i][j][1]=Math.max(dp[i-1][j-1][0]-price[i],dp[i-1][j][1])//初始化dp[-1][...][0] = dp[...][0][0] = 0;dp[-1][...][1] = dp[...][0][1] = -infinity//if(k>price.length/2) 则相当于无限次数,调用最佳买卖股票II方法const n = prices.lengthif (n <= 0) {return 0;}if (k > prices.length / 2) {return maxProfitInfinity(prices)}let dp = Array.from(Array(n), () => Array.from(Array(k + 1), () => Array(2)));for (let i = 0; i < n; i++) {dp[i][0][1] = -Infinity;dp[i][0][0] = 0;}for (let i = 0; i < n; i++) {for (let j = 1; j <= k; j++) {if (i === 0) {dp[i][j][0] = 0;dp[i][j][1] = -prices[i]continue;}dp[i][j][0] = Math.max(dp[i - 1][j][1] + prices[i], dp[i - 1][j][0]) //dp[1][1][0]=dp[0][1][1]+2 dp[0][1][0]dp[i][j][1] = Math.max(dp[i - 1][j - 1][0] - prices[i], dp[i - 1][j][1])}}return dp[n - 1][k][0];};var maxProfitInfinity = function (prices) {const n = prices.lengthif (n <= 0) {return 0;}let dp0, dp1;for (let i = 0; i < n; i++) {if (i === 0) {dp0 = 0;dp1 = -prices[i]continue;}dp0 = Math.max(dp1 + prices[i], dp0) //dp[1][0]=dp[0][1]+4 dp[0][0]dp1 = Math.max(dp0 - prices[i], dp1) //dp[1][1]=dp[0][0]-4 dp[0][1]}return dp0;};
121. 买卖股票的最佳时机
动态规划
var maxProfit = function (prices) {const n = prices.lengthif (n <= 0) {return 0;}let dp = Array.from(Array(n), () => Array(2));for (let i = 0; i < n; i++) {if (i - 1 < 0) {dp[i][1] = -prices[i];dp[i][0] = 0;continue;}dp[i][0] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][0])dp[i][1] = Math.max(- prices[i], dp[i - 1][1])}return dp[n - 1][0];};//空间优化var maxProfit = function (prices) {const n = prices.lengthif (n <= 0) {return 0;}let dp0 = 0, dp1 = -prices[0];for (let i = 0; i < n; i++) {dp0 = Math.max(dp1 + prices[i], dp0)dp1 = Math.max(- prices[i], dp1)}return dp0;};
贪心
//局部最优:最高-最低let maxProfit = function (prices) {let maxSum = 0, minprice = prices[0]for (let i = 1; i < prices.length; i++) {minprice = Math.min(prices[i], minprice)maxSum = Math.max(maxSum, prices[i] - minprice)}return maxSum}
122. 买卖股票的最佳时机 II
动态规划
//根据买卖股票的最佳时机 ④改编代码,因为k无限所以可以忽略var maxProfit = function (prices) {const n = prices.lengthif (n <= 0) {return 0;}let dp = Array.from(Array(n), () => Array(2));for (let i = 0; i < n; i++) {if (i === 0) {dp[i][0] = 0;dp[i][1] = -prices[i]continue;}dp[i][0] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][0]) //dp[1][0]=dp[0][1]+4 dp[0][0]dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]) //dp[1][1]=dp[0][0]-4 dp[0][1]}return dp[n - 1][0];};//发现每次都是根据前一次得到,空间优化var maxProfit = function (prices) {const n = prices.lengthif (n <= 0) {return 0;}let dp0=0, dp1=-prices[i];for (let i = 0; i < n; i++) {let temp=dp0;dp0 = Math.max(dp1 + prices[i], dp0)dp1 = Math.max(temp - prices[i], dp1)}return dp0;};
贪心
//局部最优:每一次交易都是正收益var maxProfit = function(prices) {let sum=0;for(let i=prices.length;i>0;i--){const profit=prices[i]-prices[i-1];if(profit>0){sum+=profit}}return sum;};
714. 买卖股票的最佳时机含手续费
无限次,含手续费,即在买卖股票的最佳时机 II的基础上改编。
//在卖出时付手续费var maxProfit = function (prices, fee) {const n = prices.lengthif (n <= 0) {return 0;}let dp0 = 0, dp1 = -prices[0];for (let i = 0; i < n; i++) {let temp = dp0;dp0 = Math.max(dp1 + prices[i] - fee, dp0)//在卖出时付手续费dp1 = Math.max(temp - prices[i], dp1)}return dp0;};//在买入时付手续费var maxProfit = function (prices, fee) {const n = prices.lengthif (n <= 0) {return 0;}let dp0 = 0, dp1 = -prices[0] - fee; //若在买入时付,需要注意买入初始值for (let i = 0; i < n; i++) {let temp = dp0;dp0 = Math.max(dp1 + prices[i], dp0)dp1 = Math.max(temp - prices[i] - fee, dp1)//在买入时付手续费}return dp0;};
309. 最佳买卖股票时机含冷冻期
无限交易次数,但有冷冻期的要求:卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
var maxProfit = function (prices) {const n = prices.lengthif (n <= 0) {return 0;}let dp = Array.from(Array(n), () => Array(2));for (let i = 0; i < n; i++) {if (i - 1 < 0) {dp[i][0] = 0;dp[i][1] = -prices[i]continue;}if (i - 2 < 0) { //特殊处理i<2的情况dp[i][0] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][0]);dp[i][1] = Math.max(- prices[i], dp[i - 1][1])continue; 2}dp[i][0] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][0])dp[i][1] = Math.max(dp[i - 2][0] - prices[i], dp[i - 1][1])//第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。}return dp[n - 1][0];};//优化空间复杂度var maxProfit = function (prices) {const n = prices.lengthif (n <= 0) {return 0;}let dp0 = 0, dp1 = -prices[0];let dp_pre = 0;for (let i = 0; i < n; i++) {let temp = dp0;dp0 = Math.max(dp1 + prices[i], dp0)dp1 = Math.max(dp_pre - prices[i], dp1)dp_pre = temp;}return dp0;};
123. 买卖股票的最佳时机 III
能进行两笔交易,即在买卖股票的最佳时机的基础上改编,k=2
var maxProfit = function (prices) {const n = prices.lengthif (n <= 0) {return 0;}let dp = Array.from(Array(n), () => Array.from(Array(3), () => Array(2)));for (let i = 0; i < n; i++) {dp[i][0][1] = -Infinity;dp[i][0][0] = 0;}for (let i = 0; i < n; i++) {for (let j = 1; j <= 2; j++) {if (i === 0) {dp[i][j][0] = 0;dp[i][j][1] = -prices[i]continue;}dp[i][j][0] = Math.max(dp[i - 1][j][1] + prices[i], dp[i - 1][j][0]) //dp[1][1][0]=dp[0][1][1]+2 dp[0][1][0]dp[i][j][1] = Math.max(dp[i - 1][j - 1][0] - prices[i], dp[i - 1][j][1])}}return dp[n - 1][2][0];};//因为k不大就1和2的情况,所以可能优化空间复杂度var maxProfit = function (prices) {const n = prices.lengthif (n <= 0) {return 0;}let dpK1H0 = dpK2H0 = 0,dpK1H1 = dpK2H1 = -Infinity;for (let i = 0; i < n; i++) {const price = prices[i]dpK1H0 = Math.max(dpK1H1 + price, dpK1H0)dpK1H1 = Math.max(- price, dpK1H1)dpK2H0 = Math.max(dpK2H1 + price, dpK2H0)dpK2H1 = Math.max(dpK1H0 - price, dpK2H1)}return dpK2H0;};

