文章目录
斐波那契数列和爬楼梯问题
两者区别在于:dp[0]边界的处理,斐波那契dp[0]=0,爬楼梯问题中dp[0]=1
// 关于:% (1e9 +7)对质数取模的话,能尽可能地避免模数相同的数之间具备公因数,来达到减少冲突的目的
// 傻递归 f(n) = f(n-1) + f(n-2), f(1)=1, f(0)=0
const fib=(n)=>{
if(n<=0) return 0 // 爬楼梯可以改为 1
if(n==1) return 1
return fib(n) = fib(i-1) + fib(i-2)
}
/* 优化1:记忆存储(记忆化搜索,自顶向下解决问题)
在缓存的时候,需要用到f(4)再去想办法缓存f(4)的值,需要用到f(3)再去想办法缓存f(3)的值 </br>
当我要用它的时候再去检查它有没有被缓存,如果没有的话,就计算缓存
*/
const fib = function(n) {
if(n<=1) return n // 把0和1两种状态都处理了
const cache = []
cache[0]=0 // 爬楼梯问题可以改为1
cache[1]=1
/* 检查创建的cacahe里面有没有momoize里要找的数,
如果有就直接return里面的值,如果没有就再用f(n-1) + f(n-2)
*/
function momeize(num){
if(cache[num]!==undefined){
return cache[num]
}
cache[num] =( momeize(num-1)+momeize(num-2)) % (1e9 +7)
return cache[num]
}
return momeize(n)
}
// 优化2:把递归转化成顺推形式 O(n)
const fib=(n)=>{
if(n<=1) return n
let cache= [0, 1] // 爬楼梯可以改为 [1, 1]
for(let i=2;i<=n;i++){
cache[i] = (cache[i - 1] + cache[i - 2]) % (1e9 +7);
}
return cache[n]
}
// 优化3:降维度, 空间复杂度的优化(降维变成了两个变量)
const fib = function(n) {
if(n<=1) return n
let prev2 = 0 // 倒数第二个数 爬楼梯问题可以改为1
let prev1 = 1 // 倒数第一个数
let result = 0
for(let i=2;i<=n;i++){
// result等于倒数第一个数+倒数第二个数
result = prev1 + prev2
// 让prev1、prev2都往前进一个值
prev2 = prev1 // 旧的prev2等于新的prev1
prev1 = result // 旧的prev1等于新的result, 即下一个result=上一个result+prve1
}
return result
}
198.打家劫舍
// 一维dp
dp[i]的状态定义:从0到i个房子最多能打劫多少钱,i可能被打劫也可能没 ==> nums[i]
dp[i]表示前i个房间的最大金额,nums[i]表示第i个房间的金额
dp[i] = max(dp[i-1], dp[i-2]+nums[i]) // 第3个房屋 = f(3-1)第2个房屋钱,f(3-2)第1个房屋钱 + 第3个房屋的钱
// 二维dp
dp[i][0]表示第i天没有偷房子 0没偷 1偷了
dp[i][0] = max(dp[i-1][0], dp[i-1][1]) // 当天偷了/没偷 取最大值
dp[i][1] = dp[i-1][0] + nums[i] // 昨天没偷,nums[i]表示今天偷的金额
/** 时间复杂度:O(n) for循环
* 空间复杂度:O(n) 用了数组,数组也是线性增长(还可以降维度优化)
*/
var rob = function(nums) {
if(nums.length === 0) return 0;
const dp= [0, nums[0]] // nums[0] 为第一个房屋存放的金额
for(let i=2;i<=nums.length;i+=1){
// nums[i-1] 数组下标从0开始,想求第一个房间金额就传0进去,求第二个金额传1进去
dp[i] = Math.max(dp[i-1], dp[i-2]+nums[i-1])
}
return dp[nums.length]
}
零钱兑换题
硬币都是无限的,所以嵌套循环
1. 零钱兑换(求最少硬币个数)
不同面额硬币coins=[1, 2, 5],总金额amount 11
11 = 5+5+1 ;结果输出3
可以分解为求单个dp[i]的最少硬币个数
状态转移:
f(x) 1≤x ≤s(总金额)
f(11) = min{f(10), f(9), f(6)} +1
// f(10)\f(9)\(f6) 表示凑成10元、9元、6元分别的硬币最少个数
// 1 表示(10+1=9+2=6+5)只需要1枚1的硬币、1枚2的硬币、1枚5的硬币
dp[i] = min{ dp[i-1], dp[i-2], dp[i-5] } +1
// dp[i] 组成金额i的最少硬币个数
var coinChange = function(coins, amount) {
const dp=[] // 也可以直接把dp定义成[12, 12, .....],就不需要每次初始化为正无穷
dp[0] = 0
for(let i=1;i<=amount;i++){
dp[i] = amount+1 // 每次进来都初始化为正无穷,便于后续取最小值
for(let j=0; j<coins.length; j++){
if(i - coins[j]<0) continue // 假设硬币面值20比11大,跳出
dp[i] = Math.min(dp[i], dp[i-coins[j]]+1 )
}
}
// console.log(dp) 每个金额对应的组成金额的最小次数 [0, 1, 1, 2, 2, 1, 2, 2, 3, 3, 2, 3]
return dp[amount] > amount ? -1:dp[amount]
}
零钱兑换2(求方法种类)
零钱兑换 II:每种面额的硬币有无限个,比如总额为5,面额coins=[1, 2, 5]
5=5;5=2+2+1;5=2+1+1+1;5=1+1+1+1+1;结果输出4
可以分解为求单个dp[i]的方法种类
分析: amount = 7 coins = [1, 3] 自上而下递推
凑成7 7-1= 6 // 动态转移为求6
6-1=5 // 动态转移为求5
5-1=4
5-3=2
6-3=3(3-1=2;3-3=0)
7-3=4 // 动态转移为求4
4-1=3
4-3=1
所以dp[7]种类 = dp[6]种类+dp[4]种类
dp[n] += dp[n-coins[i]]
条件,如果conis里面的比amount大,那我就不管
let amount = 5, coins = [1, 2, 5]
var change = function(amount, coins) {
var dp = new Array(amount + 1).fill(0) // [0, 0, 0, 0, 0, 0] 表示取0、1、2、3、4、5等金额对应的多少种方式
dp[0] = 1
for (let i = 0; i < coins.length; i++) { // 循环coins[i]的每一种情况
// for(let j=1; j<amount; i++){ if(j>=coins[i]){// 才执行...}} // 比如coins[i]为20,j等于5,就不管,即j要大于coins
for (let j = coins[i]; j < amount + 1; j++) { // 优化成这种,让j必须等于大于coins[i]
dp[j] += dp[j - coins[i]] // 3 - 1, 3-2
}
}
// console.log(dp) // [1, 1, 2, 2, 3, 4] 金额为5时有4种情况,为4有3种情况
return dp[amount]
}
console.log(change(amount, coins))
股票问题
121 (只有一次买卖)
1)记录最小值
2)遍历,让当天值-最小值 = 收益,然后更新记录最大收益
[7,1,5,3,6,4] ,比如用Math,min找出最小值1,再遍历让每个值减1取最大收益(但是前面值比1大的不能减1)
122 可以买和买无数次(即今天买,明天卖,后天买,大后天卖)
[7,1,5,3,6,4] 在7处买,1处卖;5处买,3处卖;6处买,4处卖,即(7-1)+(5-3)+(6-4),
每次买卖值再加起来,但是要判断前面的值比我小就不买(比如 1,5),前面的值比我还大我就不卖(比如 7,1)
123和188 最多只能交易k次
dp[i] 表示第i天的最大收益,i天有三个状态:不动、买入、卖出;k表示交易多少次;0当天没股票,1当天有股票
// 结果是0,即今天没有股票 = 前一天不动,前一天卖出,所以+prices[i]
dp[i][k][0] = Math.max(dp[i-1][k][0],dp[i-1][k][1] + prices[i])
// 结果是1,即今天有股票 = 前一天不动,前一天买入,所以-prices[i]
dp[i][k][1] = Math.max(dp[i-1][k][1],dp[i-1][k-1][0] - prices[i])
309 冷冻期
状态: [买入, 卖出, 冷冻期, 买入, 卖出]
714 手续费
买入-卖出 就产生一次手续费,所以买入-手续费或者卖出-手续费都可以
// 找出最小值,遍历所有值-最小值,得到最大利润
var maxProfit = function(prices) {
const len = prices.length
let min = prices[0]
let maxprofit = 0
for(let i=0;i<len;i++){
min = Math.min(min, prices[i])
maxprofit = Math.max(maxprofit, prices[i]-min)
}
return maxprofit
};
/** 时间复杂度:O(n) for循环
* 空间复杂度:O(1) 没有线性增长,是常量
*/
// 定义变量做累加,如果今天比昨天高就昨天买入,今天卖,累加利润
var maxProfit = function(prices) {
let profit = 0
for(let i=1;i<prices.length; i+=1){
// 如果今天比昨天高,就昨天买,今天卖
if(prices[i]>prices[i-1]){
profit += prices[i]-prices[i-1]
}
}
return profit
};
/* 每一个都有三种状态:不动、买、卖
dp[i][k][0] i表示第几天、k表示第几笔交易、0表示没股票、1表示有股票
定义一个三维数组,dp[i][k][0]
*/
var maxProfit = function(prices) {
let n = prices.length;
if(n == 0){
return 0;
}
let maxTime = 2;
let dp = Array.from(new Array(n),() => new Array(maxTime+1));
for(let i = 0;i < n;i++){
for(let r = 0;r <= maxTime;r++){
dp[i][r] = new Array(2).fill(0);
}
}
console.log(dp)
for(let i = 0;i < n;i++){
for(let k = maxTime;k >= 1;k--){
// 处理边界,0表示没股票,1表示有股票,即第0天卖出
if(i == 0){
dp[i][k][0] = 0;
dp[i][k][1] = -prices[i];
continue;
}
// 结果是0,即没有股票 = 前一天不动,前一天卖出,所以+prices[i]
dp[i][k][0] = Math.max(dp[i-1][k][0],dp[i-1][k][1] + prices[i])
// 结果是1,即有股票 = 前一天不动,前一天买入,所以-prices[i]
dp[i][k][1] = Math.max(dp[i-1][k][1],dp[i-1][k-1][0] - prices[i])
}
}
return dp[n-1][maxTime][0]
}
console.log(maxProfit([3,3,5,0,0,3,1,4]))
var maxProfit = function(k, prices) {
let n = prices.length;
let maxTime = k;
if(n == 0){
return 0;
}
let dp = Array.from(new Array(n),() => new Array(maxTime+1));
for(let i = 0;i < n;i++){
for(let r = 0;r <= maxTime;r++){
dp[i][r] = new Array(2);
}
}
for(let i = 0;i < n;i++){
for(let k = maxTime;k >= 1;k--){
if(i == 0){
dp[i][k][0] = 0;
dp[i][k][1] = - prices[i];
continue;
}
dp[i][k][0] = Math.max(dp[i-1][k][0],dp[i-1][k][1] + prices[i]);
dp[i][k][1] = Math.max(dp[i-1][k][1],dp[i-1][k-1][0] - prices[i]);
}
}
return dp[n-1][maxTime][0];
}
var maxProfit = function(prices) {
let n = prices.length;
if(n == 0){
return 0;
}
let dp = Array.from(new Array(n),() => new Array(2));
for(var i = 0;i < n;i++){
if(i == 0){
dp[0][0] = 0;
dp[0][1] = -prices[i];
continue;
}else if(i == 1){
dp[1][0] = Math.max(dp[0][0],dp[0][1]+prices[i]);
dp[1][1] = Math.max(dp[0][1], - prices[i]);
continue;
}
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-2][0] - prices[i]);
}
return dp[n-1][0]
}
var maxProfit = function(prices, fee) {
let n = prices.length;
if(n == 0){
return 0;
}
let dp = Array.from(new Array(n),() => new Array(2));
for(let i = 0;i < n;i++){
if(i == 0){
dp[0][0] = Math.max(0,-Infinity+prices[0]);
dp[0][1] = Math.max(-Infinity,0 - prices[0] - fee);
continue;
}
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0] - prices[i] - fee);
}
return dp[n-1][0]
}
路径问题
合并子问题 = 向下子问题 + 向右子问题 ⇒ 向上子问题 + 向左子问题
dp[i][j] = dp[i-1][j] + dp [i][j-1]
var uniquePaths = function(m, n) {
const dp = Array.from({length:m}, ()=> new Array(n).fill(0))
// 边界条件是第一行和第一列都是1
for (let i = 0; i < m; i++) dp[i][0] = 1
for (let j = 0; j < n; j++) dp[0][j] = 1
for(let i=1;i<m;i++){
for(let j=1;j<n;j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1]
}
}
return dp[m-1][n-1]
}
- 63. 不同路径 II
每一步状态:有障碍物 为1(不能通过)和 没障碍物 为0(能通过)
var uniquePathsWithObstacles = function(obstacleGrid) {
if(obstacleGrid[0][0]==1) return 0 // 出发点就被障碍堵住
const m = obstacleGrid.length
const n = obstacleGrid[0].length
const dp = Array.from({length:m},()=>new Array(n))
// 处理边界条件
dp[0][0] = 1 // 终点就是出发点
for(let i=1;i<m;i++) dp[i][0] = obstacleGrid[i][0] == 1 || dp[i - 1][0] == 0 ? 0 : 1
for (let i = 1; i < n; i++) dp[0][i] = obstacleGrid[0][i] == 1 || dp[0][i - 1] == 0 ? 0 : 1
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[i][j] = obstacleGrid[i][j] == 1 ? 0 : dp[i - 1][j] + dp[i][j - 1]
}
}
return dp[m - 1][n - 1]
}
[
[2], => [11] // 2选9和10里的9
[3,4], => [9, 10] // 3选7和6里的6;4选6和10里的6
[6,5,7], => [7, 6, 10] // 6选4和1里的1;5选1和8里的1;7选8和3里的3
[4,1,8,3] => 选最小1
]
分析:2只能加3或加4,选最小当然是3,到3的时候的时候选最小的5,到5的时候选最小的1
所以自顶向下:2+3+5+1 = 11
可以逆向从底层往上推,最底层选最小的
定义状态:dp[i][j] 到ij的最小值
状态方程:dp[i][j] = min(dp[i+1][j], dp[i+1][j+1])+t[i][j] // 它两之间的最小值再加上我自己本身(比如2)
var minimumTotal = function(triangle) {
let dp=triangle
//最后一行已经初始化了,所以从第二行开始
for(let i = dp.length-2;i>=0 ;i--){ // 从倒数第二行[6, 5, 7]开始
for(let j=0;j<dp[i].length;j++){ // 遍历当前行的数值个数6、5、6,3个
dp[i][j]= Math.min(dp[i+1][j], dp[i+1][j+1]) + dp[i][j] // 选下一个的最小的,比如6选[4,1]里最小的1,再加上自己6
}
}
return dp[0][0]
}
// 压缩内存空间
var minimumTotal = function(triangle) {
var dp = new Array(triangle.length+1).fill(0) // 初始化为[0, 0, 0, 0, 0]
for(var i = triangle.length-1;i >= 0;i--){
for(var j = 0;j < triangle[i].length;j++){
/* dp[4,1,8,3,0]中,triangle[i][j]为[6,5,7] 4和1比较 + 6 1和8比较 +5 8和3比较+7 => 新dp[7, 6, 10, 3, 0]
dp[7, 6, 10, 3, 0]中,triangle[i][j]为[3, 4] 7和6比较 + 3 6和10比较 +4 => 新dp[9, 10, 10, 3, 0]
dp[9, 10, 10, 3, 0]中,triangle[i][j]为[2] 9和10比较 +2 [11, 10, 10, 3, 0] 最少路径dp[0]
*/
dp[j] = Math.min(dp[j],dp[j+1]) + triangle[i][j]
}
}
return dp[0]
}
剪绳问题
var cuttingRope = function(n) {
let dp = Array.from({length: n+1},()=>1);
if (n == 2) return 1
if (n == 3) return 2 // 分为1*2 或者1*1*1断,取最大乘积为2
dp[1] = 1
dp[2] = 2
dp[3] = 3 // 分为3断
for (let k = 1; k <= n; k++) {
for (let i = 1; i <= k / 2; i++) { // k/2是为了裁剪小段,复用缓存
dp[k] = Math.max(dp[k], dp[i] * dp[k - i]) // f(8) = max(f(8), f(2)f(6))
}
}
return dp[n]
}
- 剑指 Offer 14- II. 剪绳子 II
大数运算不能使用动态规划,可以使用贪心算法
var cuttingRope = function(n) {
if (n == 2) return 1
if (n == 3) return 2
if (n == 4) return 4
let res=1
while(n>4){
res*=3 // 每段剪为3,所以这里累积乘以3
res=res%1000000007
n-=3
}
return res*n%1000000007
}
最大最小
// 因为有复数有,又是取最大和(累加自身)
// 所以遍历每个数值,跟0作比较取最大数值,再累加当前的数值
var maxSubArray = function(nums) {
if(nums == null) return 0
let dp=[]
dp[0]=nums[0]
let max=dp[0]
for(let i=1;i<nums.length;i++){
dp[i] = Math.max(0, dp[i-1]) + nums[i] // 找出前面大于0的,和自身累加
/* dp[0] = max(0, dp[0-1]) + nums[0] 0+-2 dp=[-2]
dp[1] = max(0, dp[0]) + nums[1] 0+1 dp=[-2, 1]
dp[2] = max(0, dp[1]) + nums[2] 1+-3 dp = [-2, 1, -2]
dp[3] = max(0, dp[2]) + nums[3] 0+4 dp = [-2, 1, -2, 4]
dp[4] = max(0, dp[3]) + nums[4] 4+-1 dp = [-2, 1, -2, 4, 3]
// ...
*/
max = Math.max(dp[i], max)
console.log(dp)
}
return max
}
// 若遇到了一个比当前元素小的值就累加1
var lengthOfLIS = function(nums) {
const len = nums.length
if(len==0) return 0
let maxLen=1
let dp = new Array(len).fill(1)
for(let i=1;i<len;i++) {
for(let j=0;j<i;j++) {
if(nums[i]>nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1)
}
}
if(dp[i]>maxLen){
maxLen = dp[i]
}
}
return maxLen
};
var longestCommonSubsequence = function(text1, text2) {
let m=text1.length
let n=text2.length
const dp=Array.from({length:m+1},()=>new Array(n+1).fill(0))
dp[0][0]=0
for(let i=1;i<=m;i++){
for(let j=1;j<=n;j++){
if(text1[i-1]==text2[j-1]){ // 它们是从下标0开始,所以要-1
dp[i][j] = dp[i-1][j-1]+1 // 如果有,就找到前一个值+1
}else{
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) // 没有就找到上一个或者前一个中最大的
}
}
}
return dp[m][n]
}
/* 状态转移方:dp[i]=dp[i−1]∗nums[i]
注意:负负得正
dp[i][0]: 从第0项到第i项范围内的子数组的最小乘积
dp[i][1]: 从第0项到第i项范围内的子数组的最大乘积
对于以i项为末尾项的子数组能产生的最小积,它有3种情况:
不和别人乘,就它自己
自己是负数,希望乘上前面的最大积
自己是正数,希望乘上前面的最小积
dp[i][0]取三种情况中的最小值:
dp[i][0] = min(dp[i−1][0]∗nums[i], dp[i−1][1]∗nums[i], nums[i])
类似的,dp[i][1]值取三种情况中的最大值
dp[i][1] = max(dp[i−1][0]∗nums[i], dp[i−1][1]∗nums[i], nums[i])
*/
var maxProduct = function(nums) {
let res = nums[0]
let preMin = nums[0]
let preMax = nums[0]
let temp1=0, temp2 = 0
for(let i=1;i<nums.length;i++){
temp1 = preMin*nums[i]
temp2 = preMax*nums[i]
preMin = Math.min(temp1, temp2, nums[i])
preMax = Math.max(temp1, temp2, nums[i])
res = Math.max(preMax, res)
}
return res
}
//dp[i][j] = max{dp[i - 1][j], dp[i][j - 1]} + data[i][j]
// 自顶向下
var maxValue = function(grid) {
const m = grid.length;
if(!m) return 0
const n = grid[0].length;
let dp = Array.from({length:m+1}, () => new Array(n+1).fill(0));
for(let i = 0; i < m; i ++) {
for(let j = 0; j < n; j ++) {
dp[i + 1][j + 1] = Math.max(dp[i][j + 1], dp[i + 1][j]) + grid[i][j] // 取右边或者下边最大值,再加上当前自身
}
}
return dp[m][n];
}
// 自底向上
var maxValue = function(grid) {
const m = grid.length;
if(!m) return 0
const n = grid[0].length;
let dp = Array.from({length:m+1}, () => new Array(n+1).fill(0));
for(let i=0; i<m; i++){
for(let j=0; j<n; j++){
if (i==0 && j==0){
dp[i][j] = grid[i][j];
}else if(i==0){
dp[i][j] = dp[i][j-1] + grid[i][j]
}else if(j==0){
dp[i][j] = dp[i-1][j] + grid[i][j]
}else{
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + grid[i][j] // 取右边或者下边最大值,再加上当前自身
}
}
}
return dp[m-1][n-1];
}
背包问题
有 n 件物品,物品体积用一个名为 w 的数组存起来,物品的价值用一个名为 value 的数组存起来;每件物品的体积用 w[i] 来表示,每件物品的价值用 value[i] 来表示。现在有一个容量为 c 的背包,问你如何选取物品放入背包,才能使得背包内的物品总价值最大? 注意:每种物品都只有1件
for(let i=1;i<=n;i++) {
for(let v=w[i]; v<=c;v++) {
// 状态:拿到的物品没有放到包里,有放到包里
dp[i][v] = Math.max(dp[i-1][v], dp[i-1][v-w[i]]+value[i])
}
}
// 降维:i只是一个索引,可以省略dp[i][v]更新维dp[v],把用不到的i去掉
// dp[v]是当前背包的最大价值
dp[v] = Math.max(dp[v], dp[v-w[i]] + value[i])
// n物品的个数 c容量 w物品的重量 value价值数组
function knapsack(n, c, w, value) {
// dp是动态规划的状态保存数组
const dp = (new Array(c+1)).fill(0)
// res 用来记录所有组合方案中的最大值
let res = -Infinity
for(let i=1;i<=n;i++) {
for(let v=c;v>=w[i];v--) {
dp[v] = Math.max(dp[v], dp[v-w[i]] + value[i]) // 写出状态转移方程:取到没放入背包,去到的有放入背包
if(dp[v] > res) {
res = dp[v] // 即时更新最大值
}
}
}
return res
}