目录
不选择i:dp[i][v]=dp[i-1][v]同列不受影响
最值:dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i])
不选择i:dp[i][v]=...dp[i-1][v]同列不受影响
子序列:dp[i]以下标i - 1/i为结尾(保持相对顺序)
(Dynamic Programming,DP)
递归或者递推的写法来实现动态规划,其中递归写法在此处又称作记忆化搜索
- function F(n){
- if(n= 0||n== 1) return 1;
- else return F(n-1)+F(n-2);
- }
dp[n]=-1表示F(n)当前还没有被计算过
- function F(n) {
- if(n == 0||n==1) return 1;//递归边界
- if(dp[n] != -1) return dp[n]; //已经计算过,直接返回结果,不再重复计算else {
- else dp[n] = F(n-1) + F(n-2); //计算F(n),并保存至dp[n]
- return dp [n];//返回F(n)的结果
- }
第i层有i个数字。现在要从第一层走到第n层,最后将路径上所有数字相加后得到的和最大是多少?
dp[i][j]表示从第i行第j个数字出发到达最底层的所有路径中能得到的最大和
dp[i][i]=max(dp[i-1][j],dp[i-1][j+1])+f[i][j]
mxn矩阵 a,从左上角开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,输出所有的路径中最小的路径和。
dp[i][j]代表i到j的最短路径
求解子问题时的状态转移方程:从「上一状态」到「下一状态」的递推式。
dp[i, j] = min(dp[i - 1][j], dp[i][j - 1]) + matrix[i][j]
JavaScript中没有二维数组的概念,但是可以设置 数组元素的值 等于 数组
key:
- function minPathSum(matrix) {
- var row = matrix.length,
- col = matrix[0].length;
- var dp = new Array(row).fill(null).map(() => new Array(col).fill(0));
- dp[0][0] = matrix[0][0]; // 初始化左上角元素
- // 初始化第一行
- for (var i = 1; i < col; i++) dp[0][i] = dp[0][i - 1] + matrix[0][i];
- // 初始化第一列
- for (var j = 1; j < row; j++) dp[j][0] = dp[j - 1][0] + matrix[j][0];
- // 动态规划
- for (var i = 1; i < row; i++) {
- for (var j = 1; j < col; j++) {
- dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + matrix[i][j];
- }
- }
- return dp[row - 1][col - 1]; // 右下角元素结果即为答案
- }
其实可以从1遍历j,然后有两种渠道得到dp[i].
拆分为2个数:是j * (i - j) 直接相乘。
拆分为3个及以上个数:j * dp[i - j],相当于是拆分(i - j)
dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]
- var integerBreak = function(n) {
- let dp = new Array(n + 1).fill(0)
- dp[2] = 1
-
- for(let i = 3; i <= n; i++) {
- for(let j = 1; j <= i / 2; j++) {
- dp[i] = Math.max(dp[i], dp[i - j] * j, (i - j) * j)
- }
- }
- return dp[n]
- };
- for 状态1 in 状态1的所有取值:
- for 状态2 in 状态2的所有取值:
- for ...
- dp[状态1][状态2][...] = 择优(选择1,选择2...)
-
- int dp[N+1][W+1]
- dp[0][..] = 0
- dp[..][0] = 0
-
- for i in [1..N]:
- for w in [1..W]:
- dp[i][w] = max(
- 把物品 i 装进背包,
- 不把物品 i 装进背包
- )
- return dp[N][W]
-
- ...加上边界条件,装不下的时候,只能选择不装
n件物品,重量为w[i],价值为c[j],容量为V的,其中每种物品都只有1件。
dp[i][v]表示前i件物品(1≤i≤n, 0≤v≤V)恰好装入容量为v的背包中所能获得的最大价值。
- function testWeightBagProblem (weight, value, size) {
- // 定义 dp 数组
- const len = weight.length,
- dp = Array(len).fill().map(() => Array(size + 1).fill(0));
-
- // 初始化
- for(let j = weight[0]; j <= size; j++) {
- dp[0][j] = value[0];
- }
-
- // weight 数组的长度len 就是物品个数
- for(let i = 1; i < len; i++) { // 遍历物品
- for(let j = 0; j <= size; j++) { // 遍历背包容量
- if(j < weight[i]) dp[i][j] = dp[i - 1][j];
- else dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
- }
- }
-
- console.table(dp)
-
- return dp[len - 1][size];
- }
-
- function test () {
- console.log(testWeightBagProblem([1, 3, 4, 5], [15, 20, 30, 55], 6));
- }
-
- test();
本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。
- 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]);
- }
- }
如果使用一维dp数组,物品(value)遍历的for循环放在外层,遍历背包(weight)的for循环放在内层,且内层for循环倒序遍历!滚动数组
- var canPartition = function(nums) {
- const sum = (nums.reduce((p, v) => p + v));
- //奇数
- if (sum & 1) return false;
- const dp = Array(sum / 2 + 1).fill(0);
- for(let i = 0; i < nums.length; i++) {
- for(let j = sum / 2; j >= nums[i]; j--) {
- dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
- if (dp[j] === sum / 2) {
- return true;
- }
- }
- }
- return dp[sum / 2] === sum / 2;
- };
- /**
- * @param {number[]} stones
- * @return {number}
- */
- var lastStoneWeightII = function (stones) {
- let sum = stones.reduce((s, n) => s + n);
-
- let dpLen = Math.floor(sum / 2);
- let dp = new Array(dpLen + 1).fill(0);
-
- for (let i = 0; i < stones.length; ++i) {
- for (let j = dpLen; j >= stones[i]; --j) {
- dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
- }
- }
-
- return sum - dp[dpLen] - dp[dpLen];
- };
left组合 - right组合 = target。
left + right = sum,而sum是固定的。right = sum - left
公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2 。
target是固定的,sum是固定的,left就可以求出来。
此时问题就是在集合nums中找出和为left的组合
dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。
例如:dp[j],j 为5,
那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。
所以求组合类问题的公式,都是类似这种:
dp[j] += dp[j - nums[i]]
- const findTargetSumWays = (nums, target) => {
-
- const sum = nums.reduce((a, b) => a+b);
-
- if(Math.abs(target) > sum) {
- return 0;
- }
-
- if((target + sum) % 2) {
- return 0;
- }
-
- const halfSum = (target + sum) / 2;
-
- let dp = new Array(halfSum+1).fill(0);
- dp[0] = 1;
-
- for(let i = 0; i < nums.length; i++) {
- for(let j = halfSum; j >= nums[i]; j--) {
- dp[j] += dp[j - nums[i]];
- }
- }
-
- return dp[halfSum];
- };
与01背包问题不同的是其中每种物品都有无数件。
- // 先遍历物品,再遍历背包容量
- function test_completePack1() {
- let weight = [1, 3, 5]
- let value = [15, 20, 30]
- let bagWeight = 4
- let dp = new Array(bagWeight + 1).fill(0)
- for(let i = 0; i <= weight.length; i++) {
- for(let j = weight[i]; j <= bagWeight; j++) {
- dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])
- }
- }
- console.log(dp)
- }
-
- // 先遍历背包容量,再遍历物品
- function test_completePack2() {
- let weight = [1, 3, 5]
- let value = [15, 20, 30]
- let bagWeight = 4
- let dp = new Array(bagWeight + 1).fill(0)
- for(let j = 0; j <= bagWeight; j++) {
- for(let i = 0; i < weight.length; i++) {
- if (j >= weight[i]) {
- dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])
- }
- }
- }
- console.log(2, dp);
- }
完全背包的两个for循环的先后顺序都是可以的。
- for (int i = 0; i < coins.size(); i++) { // 遍历物品
- for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
- dp[j] += dp[j - coins[i]];
- }
- }
假设:coins[0] = 1,coins[1] = 5。
先把1加入计算,再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况
- for (int j = 0; j <= amount; j++) { // 遍历背包容量
- for (int i = 0; i < coins.size(); i++) { // 遍历物品
- if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
- }
- }
背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。
dp[j]在外层,遇到1时更新了一次5,遇到5时更新了一次1
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
解释: 有四种方式可以凑成总金额:
- const change = (amount, coins) => {
- let dp = Array(amount + 1).fill(0);
- dp[0] = 1;
-
- for(let i =0; i < coins.length; i++) {
- for(let j = coins[i]; j <= amount; j++) {
- dp[j] += dp[j - coins[i]];
- }
- }
-
- return dp[amount];
- }
- const combinationSum4 = (nums, target) => {
-
- let dp = Array(target + 1).fill(0);
- dp[0] = 1;
-
- for(let i = 0; i <= target; i++) {
- for(let j = 0; j < nums.length; j++) {
- if (i >= nums[j]) {
- dp[i] += dp[i - nums[j]];
- }
- }
- }
-
- return dp[target];
- };
纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行!
- // 遍历物品
- const coinChange = (coins, amount) => {
- if(!amount) {
- return 0;
- }
-
- let dp = Array(amount + 1).fill(Infinity);
- dp[0] = 0;
-
- for(let i = 0; i < coins.length; i++) {
- for(let j = coins[i]; j <= amount; j++) {
- dp[j] = Math.min(dp[j - coins[i]] + 1, dp[j]);
- }
- }
-
- return dp[amount] === Infinity ? -1 : dp[amount];
- }
完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?
- for (int j = 1; j * j <= i; j++) { // 遍历物品
- dp[i] = min(dp[i - j * j] + 1, dp[i]);
- }
单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。
拆分时可以重复使用字典中的单词,说明就是一个完全背包!
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。
所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
- const wordBreak = (s, wordDict) => {
-
- let dp = Array(s.length + 1).fill(false);
- dp[0] = true;
-
- for(let i = 0; i <= s.length; i++){
- for(let j = 0; j < wordDict.length; j++) {
- if(i >= wordDict[j].length) {
- if(s.slice(i - wordDict[j].length, i) === wordDict[j] && dp[i - wordDict[j].length]) {
- dp[i] = true
- }
- }
- }
- }
-
- return dp[s.length];
- }
找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值
dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。
确定递推公式
dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。
dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。
然后我们在遍历的过程中,取dp[i][j]的最大值。
所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
此时大家可以回想一下01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i])
- const findMaxForm = (strs, m, n) => {
- const dp = Array.from(Array(m+1), () => Array(n+1).fill(0));
- let numOfZeros, numOfOnes;
-
- for(let str of strs) {
- numOfZeros = 0;
- numOfOnes = 0;
-
- for(let c of str) {
- if (c === '0') {
- numOfZeros++;
- } else {
- numOfOnes++;
- }
- }
-
- for(let i = m; i >= numOfZeros; i--) {
- for(let j = n; j >= numOfOnes; j--) {
- dp[i][j] = Math.max(dp[i][j], dp[i - numOfZeros][j - numOfOnes] + 1);
- }
- }
- }
-
- return dp[m][n];
- };
for(let i = 1; i < nums.length; i++)
for(let j = 0; j < i; j++)
dp[i][j]表示S[i]至S[j]所表示的子串是否是回文子串,是则为1,不是为0
dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。
当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
动态规划
- const findLength = (A, B) => {
- // A、B数组的长度
- const [m, n] = [A.length, B.length];
- // dp数组初始化,都初始化为0
- const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));
- // 初始化最大长度为0
- let res = 0;
- for (let i = 1; i <= m; i++) {
- for (let j = 1; j <= n; j++) {
- // 遇到A[i - 1] === B[j - 1],则更新dp数组
- if (A[i - 1] === B[j - 1]) {
- dp[i][j] = dp[i - 1][j - 1] + 1;
- }
- // 更新res
- res = dp[i][j] > res ? dp[i][j] : res;
- }
- }
- // 遍历完成,返回res
- return res;
- };
滚动数组
- const findLength = (nums1, nums2) => {
- let len1 = nums1.length, len2 = nums2.length;
- // dp[i][j]: 以nums1[i-1]、nums2[j-1]为结尾的最长公共子数组的长度
- let dp = new Array(len2+1).fill(0);
- let res = 0;
- for (let i = 1; i <= len1; i++) {
- for (let j = len2; j > 0; j--) {
- if (nums1[i-1] === nums2[j-1]) {
- dp[j] = dp[j-1] + 1;
- } else {
- dp[j] = 0;
- }
- res = Math.max(res, dp[j]);
- }
- }
- return res;
- }
Longest Common Subsequence:子序列可以不连续
“sadstory”与“adminsorry”最长公共子序列为“adsory”
dp[i][j]:strA[i]和strB[j]之前的LCS 长度,下标从1开始
直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
- const numDistinct = (s, t) => {
- let dp = Array.from(Array(s.length + 1), () => Array(t.length +1).fill(0));
-
- for(let i = 0; i <=s.length; i++) {
- dp[i][0] = 1;
- }
-
- for(let i = 1; i <= s.length; i++) {
- for(let j = 1; j<= t.length; j++) {
- if(s[i-1] === t[j-1]) {
- //不用s[i - 1]来匹配,个数为dp[i - 1][j]
- dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
- } else {
- dp[i][j] = dp[i-1][j]
- }
- }
- }
-
- return dp[s.length][t.length];
- };
dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。
所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
- const lengthOfLIS = (nums) => {
- let dp = Array(nums.length).fill(1);
- let result = 1;
-
- for(let i = 1; 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);
- }
- }
- result = Math.max(result, dp[i]);
- }
-
- return result;
- };
- const longestPalindromeSubseq = (s) => {
- const strLen = s.length;
- let dp = Array.from(Array(strLen), () => Array(strLen).fill(0));
-
- for(let i = 0; i < strLen; i++) {
- dp[i][i] = 1;
- }
-
- for(let i = strLen - 1; i >= 0; i--) {
- for(let j = i + 1; j < strLen; j++) {
- if(s[i] === s[j]) {
- dp[i][j] = dp[i+1][j-1] + 2;
- } else {
- dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
- }
- }
- }
-
- return dp[0][strLen - 1];
- };
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。
决定dp[i]的因素就是第i房间偷还是不偷。
如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。
如果不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房,(注意这里是考虑,并不是一定要偷i-1房,这是很多同学容易混淆的点)
然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
受影响的是首尾元素
对于一个数组,成环的话主要有如下三种情况:
注意我这里用的是"考虑",例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。
而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了。
- var rob = function(nums) {
- const n = nums.length
- if (n === 0) return 0
- if (n === 1) return nums[0]
- const result1 = robRange(nums, 0, n - 2)
- const result2 = robRange(nums, 1, n - 1)
- return Math.max(result1, result2)
- };
-
- const robRange = (nums, start, end) => {
- if (end === start) return nums[start]
- const dp = Array(nums.length).fill(0)
- dp[start] = nums[start]
- dp[start + 1] = Math.max(nums[start], nums[start + 1])
- for (let i = start + 2; i <= end; i++) {
- dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1])
- }
- return dp[end]
- }
下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
所以本题dp数组就是一个长度为2的数组!
那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢?
别忘了在递归的过程中,系统栈会保存每一层递归的参数。
后序遍历:通过递归函数的返回值来做下一步计算
- const rob = root => {
- // 后序遍历函数
- const postOrder = node => {
- // 递归出口
- if (!node) return [0, 0];
- // 遍历左子树
- const left = postOrder(node.left);
- // 遍历右子树
- const right = postOrder(node.right);
- // 不偷当前节点,左右子节点都可以偷或不偷,取最大值
- const DoNot = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
- // 偷当前节点,左右子节点只能不偷
- const Do = node.val + left[0] + right[0];
- // [不偷,偷]
- return [DoNot, Do];
- };
- const res = postOrder(root);
- // 返回最大值
- return Math.max(...res);
- };
首先设定是现金为0,买股票会花钱(变成负数),卖股票会赚钱
每一天的股票都有两种状态分别是持有和不持有
对于递推公式dp【i】【0】 = max(dp【i - 1】【0】, -price【i】)来说,
其实就是买不买第i天这支股票,买的话金钱减少,不买就维持前一天的状态,在买和不买两种情况中取剩钱最多的那个
对于dp【i】【1】 = max(dp【i - 1】【1】, dp【i - 1】【0】 + price【i】)
就是卖不卖第i天这支股票,卖的话赚钱,不卖就维持前一天的状态,在卖和不卖中取剩钱最多的那个,如果想要卖股票的话,之前必须要买过股票,所以是dp【i - 1】【0】 + price【i】
- int maxProfit(vector<int>& prices) {
- int low = INT_MAX;
- int result = 0;
- for (int i = 0; i < prices.size(); i++) {
- low = min(low, prices[i]); // 取最左最小价格
- result = max(result, prices[i] - low); // 直接取最大区间利润
- }
- return result;
- }
- const maxProfit = (prices) => {
- let dp = Array.from(Array(prices.length), () => Array(2).fill(0));
- // dp[i][0] 表示第i天持有股票所得现金。
- // dp[i][1] 表示第i天不持有股票所得最多现金
- dp[0][0] = 0 - prices[0];
- dp[0][1] = 0;
- for (let i = 1; i < prices.length; i++) {
- // 如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来
- // 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
- // 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]
- dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
-
- // 在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来
- // 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
- // 第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]
- dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
- }
-
- return dp[prices.length - 1][1];
- };
假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。
相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。
此时就是把利润分解为每天为单位的维度,而不是从 0 天到第 3 天整体去考虑
- var maxProfit = function(prices) {
- let result = 0
- for(let i = 1; i < prices.length; i++) {
- result += Math.max(prices[i] - prices[i - 1], 0)
- }
- return result
- };
一天一共就有五个状态,
dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金
达到dp[i][1]状态,有两个具体操作:
那么dp[i][1]究竟选 dp[i-1][0] - prices[i],还是dp[i - 1][1]呢?
一定是选最大的,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);
同理dp[i][2]也有两个操作:
所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
同理可推出剩下状态部分:
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0;
第0天做第一次买入的操作,dp[0][1] = -prices[0];
第0天做第一次卖出的操作,这个初始值应该是多少呢?
此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入,当天卖出,所以dp[0][2] = 0;
第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢?
第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后再买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。
所以第二次买入操作,初始化为:dp[0][3] = -prices[0];
同理第二次卖出初始化dp[0][4] = 0;
除了0以外,偶数就是卖出,奇数就是买入。
题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了
- // 方法一:动态规划
- const maxProfit = (k,prices) => {
- if (prices == null || prices.length < 2 || k == 0) {
- return 0;
- }
-
- let dp = Array.from(Array(prices.length), () => Array(2*k+1).fill(0));
-
- for (let j = 1; j < 2 * k; j += 2) {
- dp[0][j] = 0 - prices[0];
- }
-
- for(let i = 1; i < prices.length; i++) {
- for (let j = 0; j < 2 * k; j += 2) {
- dp[i][j+1] = Math.max(dp[i-1][j+1], dp[i-1][j] - prices[i]);
- dp[i][j+2] = Math.max(dp[i-1][j+2], dp[i-1][j+1] + prices[i]);
- }
- }
-
- return dp[prices.length - 1][2 * k];
- };
-
- // 方法二:动态规划+空间优化
- var maxProfit = function(k, prices) {
- let n = prices.length;
- let dp = new Array(2*k+1).fill(0);
- // dp 买入状态初始化
- for (let i = 1; i <= 2*k; i += 2) {
- dp[i] = - prices[0];
- }
-
- for (let i = 1; i < n; i++) {
- for (let j = 1; j < 2*k+1; j++) {
- // j 为奇数:买入状态
- if (j % 2) {
- dp[j] = Math.max(dp[j], dp[j-1] - prices[i]);
- } else {
- // j为偶数:卖出状态
- dp[j] = Math.max(dp[j], dp[j-1] + prices[i]);
- }
- }
- }
-
- return dp[2*k];
- };
四个状态:
达到买入股票状态(状态一)即:dp[i][0],有两个具体操作:
那么dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);
达到保持卖出股票状态(状态二)即:dp[i][1],有两个具体操作:
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有一个操作:
昨天一定是持有股票状态(状态一),今天卖出
即:dp[i][2] = dp[i - 1][0] + prices[i];
达到冷冻期状态(状态四),即:dp[i][3],只有一个操作:
昨天卖出了股票(状态三)
dp[i][3] = dp[i - 1][2];
综上分析,递推代码如下:
- dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
- dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
- dp[i][2] = dp[i - 1][0] + prices[i];
- dp[i][3] = dp[i - 1][2];
- // 贪心思路
- var maxProfit = function(prices, fee) {
- let result = 0
- let minPrice = prices[0]
- for(let i = 1; i < prices.length; i++) {
- if(prices[i] < minPrice) {
- minPrice = prices[i]
- }
- if(prices[i] >= minPrice && prices[i] <= minPrice + fee) {
- continue
- }
-
- if(prices[i] > minPrice + fee) {
- result += prices[i] - minPrice - fee
- // 买入和卖出只需要支付一次手续费
- minPrice = prices[i] -fee
- }
- }
- return result
- };
-
- // 动态规划
- /**
- * @param {number[]} prices
- * @param {number} fee
- * @return {number}
- */
- var maxProfit = function(prices, fee) {
- // 滚动数组
- // have表示当天持有股票的最大收益
- // notHave表示当天不持有股票的最大收益
- // 把手续费算在买入价格中
- let n = prices.length,
- have = -prices[0]-fee, // 第0天持有股票的最大收益
- notHave = 0; // 第0天不持有股票的最大收益
- for (let i = 1; i < n; i++) {
- // 第i天持有股票的最大收益由两种情况组成
- // 1、第i-1天就已经持有股票,第i天什么也没做
- // 2、第i-1天不持有股票,第i天刚买入
- have = Math.max(have, notHave - prices[i] - fee);
- // 第i天不持有股票的最大收益由两种情况组成
- // 1、第i-1天就已经不持有股票,第i天什么也没做
- // 2、第i-1天持有股票,第i天刚卖出
- notHave = Math.max(notHave, have + prices[i]);
- }
- // 最后手中不持有股票,收益才能最大化
- return notHave;
- };