3.动态规划

154 阅读4分钟

1.动态规划三个要素

动态规划,即Dynamic Programming,简称DP,解题由三个要素构成:

状态dp[i]或dp[i][j],沿升递归到第i个界限所得到的目标值

状态初始化dp[1] = c1, dp[2] = c2,只有1或2个界限时的目标值

状态转移方程dp[i] = a*dp[i-1] + b*dp[i-2],第i个界限的目标值依赖于第i-1和第i-2界限时的目标值

2.背包DP

时间复杂度O(N*W),空间复杂度O(N)

2.1 经典背包

每个物品种类下的物品数量只有一个

w[i]:物品重量,v[i]:物品价值,N:物品种类数量,W:背包可容纳重量

状态转移方程(二维)dp[i][j] = max{ dp[i-1][j - k*w[i]] + k*v[i], 0 <= k <= 1 }

状态转移方程(一维)dp[j] = max{ dp[j - k*w[i]] + k*v[i], 0<=k<=1 } for j=W,...,w[i]

// 状态定义
dp[i][j] => dp[j] 

// 状态初始化
dp[0][0,...,w]=0 => dp[0,..,w]=0

// 状态转移方程
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-w[j]]+v[i]) => 
dp[j] = Math.max(dp[j], dp[j-w[i]]+v[i]) // W >= j >= w[i],i<=N


// 求解伪代码
for(i=1,...,N)
  for(j=W,...,w[i]) // 逆向
    dp[j] = max(dp[j], dp[j-w[i]]+v[i])

2.2 完全背包

每个物品总类下的物品数量不限

w[i]:物品重量,v[i]:物品价值,N:物品种类数量,W:背包可容纳重量

状态转移方程(二维)dp[i][j] = max{ dp[i-1][j - k*w[i]] + k*v[i], 0 <= k <= j/w[i] }

状态转移方程(一维)dp[j] = max{ dp[j - k*w[i]] + k*v[i], 0<=k<=1 } for j=w[i],...,W

// 状态定义
dp[i][j]

// 状态初始化
dp[0][0,...,w]=0

// 状态转移方程
dp[i][j] = Math.max(dp[i−1][j], dp[i][j−w[i]]+v[i]) // 0<= j <= w[i],1<= i <=N,由于数量不限,故第二个为dp[i]而非dp[i-1]
dp[j] = Math.max(dp[j], dp[j-w[i]]+v[i]) // W >= j >= w[i],i<=N

// 求解伪代码
for(i=1,...,N)
  for(j=w[i],...,W) // 正向
    dp[j] = max(dp[j], dp[j-w[i]]+v[i])

2.3 多重背包

每个物品种类下的物品数量大于等于1

w[i]:物品重量,v[i]:物品价值,N:物品种类数量,W:背包可容纳重量,n[i]:同种类物品数量

状态转移方程(二维)dp[i][j] = max{ dp[i-1][j - k*w[i]] + k*v[i], 0 <= k <= n[i] }

状态转移方程(一维)dp[j] = max{ dp[j - k*w[i]] + k*v[i], 0 <= k <= min{n[i], j/w[i]} } for j=W,...,w[i]

// 状态定义
dp[i][j]

// 状态初始化
dp[0][0,...,w]=0


// 求解伪代码
for(i=1,...,N)
  for(j=W,...,w[i]) // 逆向
      for(k=1,...,min(n[i], floor(j/w[i])))
        dp[j] = max(dp[j], dp[j-w[i]]+v[i])

2.4 求解具体实现

1.经典背包求解

// w[i]:物品重量,v[i]:物品价值,W:背包可容纳重量
function (w,v,W){
  let N = w.length // 物品种类
  let dp = new Array(N+1).fill(0)
  for(let i=0; i<N; i++){
    for(let j=W; j>=w[i]; j--) dp[j] = Math.max(dp[j], dp[j-w[i]] + v[i])
  }
  return dp[N]
}

2.完全背包求解

// w[i]:物品重量,v[i]:物品价值,W:背包可容纳重量,n[1,...,i] = Infinity:每种物品数量无限
function (w,v,W){
  let N = w.length // 物品种类
  let dp = new Array(N+1).fill(0)
  for(let i=0; i<N; i++){
    for(let j=w[i]; j<=W; j++) dp[j] = Math.max(dp[j], dp[j-w[i]] + v[i])
  }
  return dp[N]
}

3.多重背包求解

// w[i]:物品重量,v[i]:物品价值,W:背包可容纳重量,n[i]:物品数量
function(w,v,n,W){
  let N = w.length // 物品种类
  let dp = new Array(N+1).fill(0)
  for(let i=0; i<N; i++){
    for(let k =1;k<=Math.min(Math.floor(W/w[i]), n[i]);k++){
      for(let j=W; j>=k*w[i]; j--) dp[j] = Math.max(dp[j], dp[j-w[i]] + v[i])
    }
  }
  return dp[N]
}

4.混合背包求解

混合背包由经典背包、多重背包和完全背包构成,即物品种类下的物品数量可能有一个、多个、无限个,即n=[1, ..., k, ..., Infinity, ...]

function(w,v,n,W){
  // 先处理边界问题
  let N = w.length // 物品种类

  // 初始化状态
  let dp = new Array(N+1).fill(0)
  
  // 接下来很简单,分三者情况处理即可
  for(let i=0; i<N; i++){
    if(n[i]==Infinity){ 
      for(let j=w[i]; j<=W; j++) dp[j] = Math.max(dp[j], dp[j - w[i]]+v[i])
    }
    else{ // 二合一
      for(let k = 1;k<=Math.min(Math.floor(W/w[i]),n[i]),k++){
        for(let j=W;j>=k*w[i];j--) dp[j] = Math.max(dp[j],dp[j-w[i]]+v[i])
      }
    }
  }
  return dp[N] 
}

5.其他经典题

背包裂变题

// 1.最大价值
let dp = Array(size + 1).fill(0)
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])

// 2.能否装满或最多装多少
let dp = Array(size + 1).fill(0)
dp[j] = Math.max(dp[j], dp[j - nums[j]] + nums[j])

// 3.装满背包有几种方法
let dp = Array(size + 1).fill(0)
dp[0] = 1 // 递推基础值
dp[j] += dp[j - nums[i]]

// 4.装满背包最小所需物品数
let dp = Array(size + 1).fill(Infinity)
dp[0] = 0
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1)

求01背包容量从0扩充到6时每次的最优解,包含选择物品下标j最大价值

function bagProblem(weights, values, size) {
  let jLen = weights.length
  let dp = Array(jLen).fill().map(() => Array(size).fill({ idxs: [], value: 0 }))
  for (let i = size; i >= weights[0]; i--) dp[0][i] = { idxs: [0], value: values[0] }
  
  for (let j = 1; j < jLen; j++) { // 物品种类j
    for (let i = 0; i <= size; i++) { // 背包容量i
      if (i < weights[j] || dp[j - 1][i].value > dp[j - 1][i - weights[j]].value+values[j]) {
        dp[j][i] = dp[j - 1][i] 
        continue
      }
      
      let pre = dp[j - 1][i - weights[j]]
      dp[j][i] = { idxs: [...pre.idxs, j], value: pre.value + values[j] }
    } 
  }
  dp[jLen-1].forEach((e,idx)=>console.log('背包重量:',idx,' 选择物品: ',e.idxs,' 最大价值:',e.value))
}
bagProblem([1, 3, 4, 5], [15, 20, 30, 55], 6)
// 背包重量: 0  选择物品:  []  最大价值: 0
// 背包重量: 1  选择物品:  [ 0 ]  最大价值: 15
// 背包重量: 2  选择物品:  [ 0 ]  最大价值: 15
// 背包重量: 3  选择物品:  [ 1 ]  最大价值: 20
// 背包重量: 4  选择物品:  [ 0, 1 ]  最大价值: 35
// 背包重量: 5  选择物品:  [ 3 ]  最大价值: 55
// 背包重量: 6  选择物品:  [ 0, 3 ]  最大价值: 70

降为1维

function bagProblem(weights, values, size) {
  let iLen = weights.length
  let dp = Array(size + 1).fill({ idxs: [], value: 0 })
  for (let i = 0; i < iLen; i++) {
    for (let j = size; j >= weights[i]; j--) {
      let { value, idxs } = dp[j - weights[i]]
      if (dp[j].value > value + values[i]) continue
      
      dp[j] = { idxs: [...idxs, i],value: value + values[i]}
    }
  }

  dp.forEach((e, idx) => console.log(`背包容量:${idx},选择物品:[${e.idxs}],最大价值:${e.value}`))
  // 背包容量:0,选择物品:[],最大价值:0
  // 背包容量:1,选择物品:[0],最大价值:15
  // 背包容量:2,选择物品:[0],最大价值:15
  // 背包容量:3,选择物品:[1],最大价值:20
  // 背包容量:4,选择物品:[0,1],最大价值:35
  // 背包容量:5,选择物品:[3],最大价值:55
  // 背包容量:6,选择物品:[0,3],最大价值:70
}
bagProblem([1, 3, 4, 5], [15, 20, 30, 55], 6)

爬楼梯

function(n){
  let dp = [] // 状态定义
  dp[0] = 0, dp[1] = 1, dp[2] = 2 状态初始化
  
  for(let i = 3; i<=n; i++) dp[i] = dp[i-1] + dp[i-2] // 3.状态转移方程
  
  return dp[n]
}

打家劫舍

function(nums){
  // 处理边界 
  if(nums.length==0) return 0 
  if(nums.length==1) return nums[0]
  
  function getMax(nums){
    // 处理边界 
    if(nums.length==0) return 0
    if(nums.length==1) return nums[0]
    
    // 状态定义、初始化
    let dp = []
    dp[0][0] = 0
    dp[0][1] = nums[0]
    
    for(let i = 1; i<nums.length; i++){
      // 状态转移方程
      dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]) // 这家不偷则nums[i]不纳入考虑,故取i-1之前的最大值
      dp[i][1] = nums[i] + dp[i-1][0]
    }
    return Math.max(dp[nums.length-1][0],dp[nums.length-1][1]) // 最后一家偷与不偷
  }
  // 因为第一家和最后一家相通
  let res1 = getMax(nums.slice(1)) // 除去第一家
  let res2 = getMax(nums.slice(0,nums.length-1)) // 除去最后一家
  return Math.max(res1,res2)
}

参考

1.「算法与数据结构」一张脑图带你看动态规划算法之美

2. dd大牛的《背包九讲》

3. 动态规划之背包问题系列