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)
}