前言
某个男人 动静布局, 而我作为一个致力称为厨师界最会写算法的前端,总得刷上一部分题,有那么一点发现吧,当初咱们就来聊聊,菜鸡如我,发现了什么。
注释
汇总这周学习的当初,我脑壳疼的慌,啥货色都想不到,然而为了不破本人每周都总结一下下的指标,只能水几句了;
dp 如同是所有高级菜鸟的一个瓶颈,反正每次说到算法难在哪里,第一个想到的就是 dp,去吹牛皮说面试他人装逼,就说顺手甩几道 dp 去难为面试者的老哥层出不穷,所以 dp 有一种失望之巅的赶脚,只有翻过它,就能进阶了;
其实学习算法到面试遇到的算法,用到 dp 的时候其实没有,树,链表这些反而是超火的热题,所以 dp 对于前端以面试为目标的算法学习,属于一种积攒吧,就是经典题型学一学,遇到了原题搞一搞,遇到难题间接 pass,就是高考数学倒数两题的感觉,所以呢,所以把最经典的温习一遍,而后。。我就一菜鸡,我也不晓得, 反正后续应该会持续编吧,会学更深的 dp,毕竟这个货色是真的有意思,有趣味的能够到这里学习一下大神的总结
以上,切实吹不上来了,下线,睡觉;下周做,位运算吧;加油呀
题目汇总
股票买卖
-
- 交易股票的最佳时机
-
- 交易股票的最佳时机 II
-
- 交易股票的最佳时机 III
-
- 交易股票的最佳时机 IV
-
- 最佳交易股票机会含冷冻期
-
- 交易股票的最佳时机含手续费
打家劫舍
-
- 打家劫舍
-
- 打家劫舍 II
-
- 打家劫舍 III
记忆化递归
-
- 杨辉三角
-
- 爬楼梯
- 面试题 08.06. 汉诺塔问题
其余
-
- 零钱兑换
-
- 零钱兑换 II — 凑计划
-
- 最长回文子串
题目
121. 交易股票的最佳时机
剖析:
- 交易 1 次,不须要距离, 不收手续费
- dp_0 示意没有持有的状态的最大利润,dp_1 示意持有状态下的最小老本
2.5. 因为只进行一次交易,所以 dp_1 要尽可能大的取,确保买入的老本最小,而 dp_0 也要放弃最高,保障卖出最大 - 状态转移方程 dp_0 = Math.max(dp_0,dp_1+nums[i]), dp_1 = Math.max(dp_1,-nums[i])
- base case dp_0 =0 dp_1 = -nums[i]
var maxProfit = function(prices) {let dp_0 = 0,dp_1 = prices[0]
for(let i = 0;i<prices.length;i++){dp_0 = Math.max(dp_0,dp_1+prices[i])
dp_1 = Math.max(dp_1,-prices[i]) // 因为只会交易一次,这里
}
return dp_0
}
122. 交易股票的最佳时机 II
剖析
- 交易 n 次,不须要距离, 不收手续费
- dpi 示意在第 i+1 天(i 是下标值,对应的天数要 +1),状态为 j 时,最大利润,其中 i 就是 [0,len-1],j 为 0 示意没有持有,为 1 示意持有
- 状态转移方程: dpi = Math.max(dpi-1+temp, dpi-1), dpi =Math.max(dpi-1, dpi-1-temp)
- base case: dp0 = 0, dp0 = -temp
- 工夫复杂度 O(n), 空间复杂度 O(n)
var maxProfit = function(prices) {const dp = new Array(prices).map(() => [])
dp[0][0] = 0,dp[0][1] =prices[0]
for (let i = 1; i < prices.length; i++) {dp[i][0] = Math.max(dp[i-1][1]+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][0]
};
剖析 — 压缩空间变量
- 压缩一下,将二维数组转成两个变量 dp_0,dp_1
- 工夫复杂度 O(n), 空间复杂度 O(1)
var maxProfit = function(prices) {let dp_0 = 0,dp_1 = -prices[0]
for (let i = 1; i < prices.length; i++) {
const temp_0 = dp_0
dp_0 = Math.max(dp_0,dp_1+prices[i])
dp_1 = Math.max(temp_0-prices[i],dp_1)
}
return dp_0
}
123. 交易股票的最佳时机 III
剖析
- 交易 2 次,不须要距离, 不收手续费
- dpi[k] 示意在第 i+1 天(i 是下标值,对应的天数要 +1),持有状态为 j , 交易次数为 k 次时的最大利润,其中 i 就是 [0,len-1],j 为 0 示意没有持有,为 1 示意持有
- 买入记作 1 次,卖出不记作一次交易
- 状态转移方程: dpi[k] = Math.max(dpi-1[k],dpi-1[k]+prices[i]), dpi[k] = Math.max(dpi-1[k],dpi-1[k-1]-prices[i])
- base case 1 — 交易次数为 0 次时的全副状态,如果此时是持有 1,即和交易次数为 0 产生抵触,只有交易次数没涨下来,任何交易都是耍流氓,所以相当于没有交易 0;如果是没有持有 0,那么初始化状态就是 0 了;
- base case 2 — 因为交易次数为 0 的状态曾经解决完了,这里交易次数起码为 1 次,也就是说持有属于失常操作,不持有属于异样,给个 0 占个地位;
- 这里最难的中央是 base case 的确定,总之如果交易次数和持有状态产生异样,则代表交易失败,只有用了交易次数,持有状态同步更新,才算是一次失常的交易;
参考视频:传送门
var maxProfit = function (prices) {const dp = Array.from(prices).map(() => Array.from({length:2}).map(() => new Array(3)))
const len = prices.length
for(let i = 0;i<len;i++){for(let k = 1;k<=2;k++){
// base case 1 -- 当 k 为 0 时
dp[i][0][0] = 0
dp[i][1][0] = 0 // 只有交易次数没涨下来,任何交易都是耍流氓
if(i === 0){
// base case 2 -- 第一天时的状态
dp[0][0][k] = 0 //
dp[0][1][k] = -prices[0]
continue
}
dp[i][0][k] = Math.max(dp[i-1][0][k],dp[i-1][1][k]+prices[i])
dp[i][1][k] = Math.max(dp[i-1][1][k],dp[i-1][0][k-1]-prices[i])
}
}
return dp[len-1][0][2]
}
188. 交易股票的最佳时机 IV
- 和 123. 交易股票的最佳时机 III 是一样的操作和剖析
- 值得注意的是,这题的入参,prices.length 的范畴是 0,1000],所以须要进行判空解决;而 [123. 交易股票的最佳时机 III 中的 prices.length 的范畴是 [1,pow(10,5)], 所以能够间接应用
var maxProfit = function(k, prices) {
const len = prices.length
if(len<2) return 0
// dp[i][j][k] -- i 为 [0,len-1] j:0,1 ; k 为 [0,k]
const dp =Array.from(prices).map(() => Array.from({length:2}).map(() => new Array(k+1).fill(0)))
for (let i = 0; i < prices.length; i++) {
// base case1 -- 交易次数为 0 的时候
dp[i][0][0] = 0
dp[i][1][0] = 0
for (let j = 1; j <= k; j++) {if(i === 0){
// base case2 -- 第一天交易
dp[0][0][j] = 0
dp[0][1][j] = -prices[0]
continue
}
dp[i][0][j] = Math.max(dp[i-1][0][j],dp[i-1][1][j]+prices[i])
dp[i][1][j] = Math.max(dp[i-1][1][j],dp[i-1][0][j-1]-prices[i])
}
}
return dp[prices.length-1][0][k]
};
309. 最佳交易股票机会含冷冻期
剖析
- 交易 n 次,须要距离 1 天, 不收手续费
- dp_0_0 示意没有持有,不处于冷却期,dp_0_1 示意没有持有,处于冷却期, dp_1_0 失常状况,持有的时候没有冷却期,dp_1_1 属于矛盾的状态,疏忽
-
状态转移方程:
- dp_0_0 = Math.max(dp_0_0, dp_0_1);
- dp_0_1 = dp_1_0 + prices[i];
- dp_1_0 = Math.max(dp_1_0,temp – prices[i]);
- base case: dp_0_0 = 0, dp_0_1 = 0, dp_1_0 = -prices[0];
- 次要留神有冷却期的是没有持有才会触发,所以总共只有三种状态,dp_0_0 = 0, dp_0_1, dp_1_0;所以能够间接将空间复杂度压缩到 O(1)
var maxProfit = function (prices) {
const len = prices.length;
if (len < 2) return 0;
let dp_0_0 = 0,
dp_0_1 = 0,
dp_1_0 = -prices[0];
for (let i = 1; i < len; i++) {
const temp = dp_0_0;
// 不在冷却中的没持有状态,证实上一次只可能是没有持有的状态
dp_0_0 = Math.max(dp_0_0, dp_0_1);
dp_0_1 = dp_1_0 + prices[i];
dp_1_0 = Math.max(dp_1_0,temp - prices[i]);
}
return Math.max(dp_0_0, dp_0_1);
};
console.log(maxProfit([1, 2, 4]));
714. 交易股票的最佳时机含手续费
剖析
- 交易 n 次,无距离, 收手续费
- 因为要收手续费,那么就不是怎么交易都是好的了,必须将老本算上去,之前都是无本例如,只须要低买高卖就成,当初必须思考可能会亏的可能
- 整体剖析和 122. 交易股票的最佳时机 II 一样,只是在卖出的时候,加上 fee 即可
var maxProfit = function(prices, fee) {let dp_0 = 0, dp_1 = -prices[0]
for (let i = 1; i < prices.length; i++) {
const temp =dp_0
dp_0 = Math.max(dp_0,dp_1 + prices[i] - fee)
dp_1 = Math.max(dp_1,temp - prices[i])
}
return dp_0
};
198. 打家劫舍
剖析
- dpi 示意在第 i 个房子没有偷东西时的最高金额,dpi 示意偷东西了的最高金额
- 状态转移方程: dpi = Math.max(dpi-1,dpi-1) dpi = dpi-1+nums[i]
- base case dp0 = 0 dp0= numns[0]
- 工夫复杂度 O(n), 空间复杂度 O(n)
var rob = function (nums) {
const len = nums.length;
const dp = new Array(len).fill(null).map(() => new Array(2));
dp[0][0] = 0;
dp[0][1] = nums[0];
for (let i = 1; i < len; i++) {dp[i][0] = Math.max(dp[i - 1][1], dp[i - 1][0]);
dp[i][1] = dp[i - 1][0] + nums[i];
}
return Math.max(dp[len - 1][0], dp[len - 1][1]);
};
剖析
- 降维,每次的值只和前一个无关,且状态值只有两个,所以能够间接用两个变量 dp_0,dp_1 示意
- 工夫复杂度 O(n), 空间复杂度 O(n)
var rob = function (nums) {let dp_0 = 0,dp_1 = nums[0]
for (let i = 1; i < len; i++) {
const temp = dp_0 // 保留一下上一个 dp_0
dp_0 = Math.max(dp_0,dp_1)
dp_1 = temp+nums[i]
}
return Math.max(dp_0,dp_1)
}
213. 打家劫舍 II
剖析
- dpi[k] 其中 i 示意第 i 个值,j 示意第一个房子是否偷了,k 示意以后这个房子有没有偷
- 状态转移方程: dpi[k] = dpi-1[k]
- base case: dp0[0] = 0, dp0[1] = nums[0],dp0[1] = -1,dp0[0] = -1
- 工夫复杂度 O(n) 空间复杂度 O(n)
var rob = function (nums) {
const len = nums.length;
const dp = new Array(len)
.fill(0)
.map(() => new Array(2).fill(0).map(() => new Array(2).fill(-1)));
(dp[0][0][0] = 0), (dp[0][1][1] = nums[0]);
for (let i = 1; i < len; i++) {dp[i][0][0] = Math.max(dp[i - 1][0][0], dp[i - 1][0][1]);
dp[i][0][1] = dp[i - 1][0][0] + nums[i];
dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][1][1]);
dp[i][1][1] = i === len - 1 ? dp[i - 1][1][1] : dp[i - 1][1][0] + nums[i];
}
return Math.max(dp[len - 1][0][0], dp[len - 1][0][1], dp[len - 1][1][0],dp[len - 1][1][1]);
};
剖析
- 降维,每次的值只和前一个无关,且两个状态值别离只有两个 item,所以能够间接用四个变量 dp_0_0,dp_1_1,dp_1_0,dp_0_1 放弃迭代过程中的状态
- 工夫复杂度 O(n), 空间复杂度 O(n)
var rob = function (nums) {let dp_0_0 = 0, dp_1_1 = nums[0], dp_1_0 = -1,dp_0_1 = -1
for (let i = 1; i < nums.length; i++) {
const temp_0_0 = dp_0_0
const temp_1_0 = dp_1_0
dp_0_0 = Math.max(dp_0_0,dp_0_1)
dp_0_1 = temp_0_0 + nums[i]
dp_1_0 = Math.max(dp_1_0,dp_1_1)
dp_1_1 = i === nums.length - 1 ? dp_1_1 : temp_1_0+nums[i]
}
return Math.max(dp_0_0,dp_1_1,dp_1_0,dp_0_1);
};
337. 打家劫舍 III
剖析
- 自低向上求最大的状态值,每一个节点都返回两个状态值[dp_0,dp_1] , 其中 dp_0 示意没有取挡墙 root.val 的值,dp_1 示意取了以后 val 的最大值
- 最开始也尝试应用自顶向下放弃状态去取,然而能够获得单条门路的最大状态值,累加的话须要找出反复的状态节点,所以不太适合
- 自底向上取能够保障所有连线上的节点的状态都一一进行解决,最初汇总到根节点上,失去最大值,而自顶向下,最初其实是扩散的,最大值在哪里不好确定,这里也不行个别的 dp 用数组保留所有状态值,所以为了就一个聚合值,也应该思考是自底向上,求根节点的状态值
- 工夫复杂度 O(n), 空间复杂度 O(1)
var rob = function (root) {const recursion = (root) => {if (!root) return [0, 0];
const [ldp_0, ldp_1] = recursion(root.left);
const [rdp_0, rdp_1] = recursion(root.right);
const dp_0 = Math.max(
ldp_0 + rdp_0,
ldp_0 + rdp_1,
ldp_1 + rdp_0,
ldp_1 + rdp_1
);
const dp_1 = ldp_0 + rdp_0 + root.val;
return [dp_0, dp_1];
};
const [dp_0, dp_1] = recursion(root);
return Math.max(dp_0, dp_1);
};
118. 杨辉三角
剖析
- 杨辉三角中,两条边都是 1
- 三角里的值 numrow = numrow-1+numrow-1
- 只须要设置好边界,能够间接用 dp 缓存自顶向下遍历过的 numrow
var generate = function(numRows) {const dp = new Array(numRows)
for (let i = 0; i < numRows; i++) {dp[i] = new Array() ;
for(let j = 0;j<=i;j++){
// 杨辉三角的两边都是 1
if(j === 0 || j === i) {dp[i][j] = 1
continue
}
dp[i][j] = dp[i-1][j-1]+dp[i-1][j]
}
}
return dp
};
console.log(generate(1))
console.log(generate(2))
console.log(generate(5))
70. 爬楼梯
剖析 — 记忆化递归 — 自顶向下缓存对应的值
- 用 map 存储递归过程中存储不同楼梯长度 key, 对应的走法 value
- 每一次递归都先查 map 是否曾经有答案,没有的时候再递归往下持续走
- base case,[0->1,1->1], 应用 map 缓存值,能够缩小反复的两头操作,升高工夫复杂度
- 工夫复杂度 O(n), 空间复杂度 O(n)
var climbStairs = function(n) {const map = new Map()
map.set(1,1)
map.set(0,1)
const recursion= (n) => {if(map.has(n)) return map.get(n)
const sum = recursion(n-1)+recursion(n-2)
map.set(n,sum)
return sum
}
return recursion(n)
};
@剖析 — dp
- 应用 dp[i] 示意楼梯为 i 时,有多少种走法
- 状态转移方程: dp[i] = dp[i-1]+dp[i-2]
- base case: dp[0] + dp[1] = 1 — 啥也不走也是一种走法
- 工夫复杂度 O(n), 空间复杂度 O(n)
var climbStairs = function(n) {const dp = []
dp[1] = 1
dp[0] = 1
for (let i = 2; i <= n; i++) {dp[i] = dp[i-1]+dp[i-2]
}
return dp[n]
}
面试题 08.06. 汉诺塔问题
剖析
- 这个一个典型的递归问题, 每一次都要将 n 个值从 first 启动,通过 second,最初达到 end
- 拆解一下,如果 n === 1,间接从 first -> end 即可
- 如果有 n > 1, 那么须要先将 n-1 个值从 first -> end -> second 存储起来,将剩下的一个间接从 first -> end
- 通过步骤 3,以后 first 是空的,second 有 n-1 个值,end 有 1 个最大值,当初这种状况等价于将 n-1 个值从 second -> first -> end,这样最初就能在 end 中找到一个残缺的 n 个值的数组了
- 工夫复杂度 O(n2)
var hanota = function(A, B, C) {
const len = A.length;
const recursion = (n,first,second,end) => {if(n === 1) {const a = first.pop()
end.push(a)
return
}
// 将 n-1 个值从 first -> end -> second, 而后将最初的值从 first 挪动到 end
recursion(n-1,first,end,second)
end.push(first.pop())
// 当初 end 有一个值,second 有 n-1 个值,first 为空
// 再将 n-1 个值从 second -> first -> end
recursion(n-1,second,first,end)
}
recursion(len,A,B,C)
};
322. 零钱兑换
剖析 — 硬币数有限
- 状态定义:dp[i] 示意凑成总金额为 i 时所需的起码的硬币数
- 状态转移方程: dp[i] = Math.min(dp[i-coins[x]])+1 — 别离计算出上一次取一枚硬币时,最小的数,而后 + 1 即可
- base case: dp[0] = 0
- 工夫复杂度 O(n∗m) 其中 n 是硬币数量,m 是金额总数,空间复杂度为 O(m)
var coinChange = function (coins, amount) {const dp = new Array(amount + 1)
// base case
dp[0] = 0; // 如果总金额为 0,则不须要取了
for (let i = 1; i <= amount; i++) {dp[i] = Infinity // 初始化
for (coin of coins) {if (i >= coin) {dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
};
// 两层遍历是无所谓 的,排列组合都 ok,因为求的是一个极值
var coinChange = function (coins, amount) {const dp = new Array(amount + 1).fill(Infinity);
// base case
dp[0] = 0; // 如果总金额为 0,则不须要取了
for (coin of coins) {for (let i = 1; i <= amount; i++) {if (i >= coin) {dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
};
518. 零钱兑换 II — 凑计划
剖析
- 始终不了解为什么必须是外层写 coins 的循环,内层写 amount 循环,因为这个 322. 零钱兑换 还是有点相似的,都是凑钱,一个是凑最小的数,而我这里凑的计划
- 也正因为凑的是计划数,所以 1-2-1 和 1-1-2 是一样的计划,这里要的只是组合,而不须要排列;
- 所以如果外层是 amount,就思考到了每一次取硬币的程序,失去的是一个排列数,与本题不合乎,所以把 coins 放外层,那么整个方案设计就只和硬币的类型无关,和取的程序无关了 — 有感于此,在下面那题加一个 coins 外层嵌套的写法也是 ok 的;
- 状态定义:dp[i] 示意凑成总金额为 i 时存在的计划 — 给定 amount 硬币数组,硬币品种不一样,失去的计划数不一样
- 状态转移方程: dp[i] = sum(dp[i-coin]) — 别离计算出取走一枚硬币时,可能凑出的计划数
- base case: dp[0] = 1 // 不取也是一种计划
- 工夫复杂度 O(n∗m) 其中 n 是硬币数量,m 是金额总数,空间复杂度为 O(m)
var change = function (amount, coins) {const dp = new Array(amount + 1).fill(0); // 凑不齐就是 0 个计划了
dp[0] = 1; // 不取也是一种计划
// 相当于背包问题中,背包里的货色有哪些,每多一样货色,就能够更新计划
for (coin of coins) {for (let i = coin; i <= amount; i++) {
// 只有比以后币值高的,才须要更新计划数
dp[i] = dp[i] + dp[i - coin];
}
}
return dp[amount];
};
5. 最长回文子串
剖析
- 状态定义 — dpi 示意 在 [i,j] 的子串是一个回文子串
- 状态转移方程: dpi = s[i] === s[j] && dpi+1
- base case: j-i<1 && s[i] === s[j] 时,dpi = true, 他们就是回文子串
- dp 是依据已知值去推断无后续性的值,所以要求 dpi 的时候,要不 dpi+1 曾经晓得,要不就是一个 base case
- 所以外层包裹的是一个起点值,而后 i 处于内层向前扩大
var longestPalindrome = function(s) {const dp =Array.from(s).map(() => new Array())
let ret = ''
for(let j = 0;j <s.length;j++){for(let i =j;i>=0;i--){if( j-i<2 && s[i] === s[j]){
// base case : 单个字符
dp[i][j] = true
}else if(s[i] === s[j] && dp[i+1][j-1]){dp[i][j] = true
}
if(dp[i][j] && j-i+1>ret.length){ret = s.slice(i,j+1)
}
}
}
return ret
};