背包DP总结

01背包

问题:有n个重量和价值分别为wi和vi的物品,从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。

使用dfs搜索时依次对第i种物品都考虑选择或不选择,如果选择那么剩余容量为m-wi,然后在剩余的物品中继续搜索;如果不选择那么剩余容量不变,继续在后面的物品中搜索。使用动态规划的方法,则把每次开始搜索时容量和当前需要判断的物品看做一个状态,得到的转移方程为
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w i ] + v i ) dp[i][j]=max(dp[i-1][j],dp[i-1][j-wi]+vi) dp[i][j]=max(dp[i1][j],dp[i1][jwi]+vi)
i表示当前判断的物品,j表示剩余容量,通过观察发现转移方程中只会出现i和i+1两行,所以可以进一步优化空间复杂度。
d p [ j ] = m a x ( d p [ j ] , d p [ j − w i ] + v i ) dp[j]=max(dp[j],dp[j-wi]+vi) dp[j]=max(dp[j],dp[jwi]+vi)
根据转移方程得到如下代码:

for (int i = 0; i < n; i++) {
    for (int j = m; j >= 0; j--) {
        if (j >= w[i])
            dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
    }
}
out.println(dp[m]);
完全背包

问题:有n个重量和价值分别为wi和vi的物品,每种物品数量无限多,从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。

与01背包的不同处在于每种物品可以无限多,因此选择一个物品之后还可继续从当前物品开始选择,转移方程为:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w [ i ] ] + v i ) dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+vi) dp[i][j]=max(dp[i1][j],dp[i][jw[i]]+vi)
空间优化后的转移方程
d p [ j ] = m a x ( d p [ j ] , d p [ j − w i ] + v i ) dp[j]=max(dp[j],dp[j-wi]+vi) dp[j]=max(dp[j],dp[jwi]+vi)
根据转移方程得到如下代码:

for (int i = 0; i < n; i++) {
    for (int j = w[i]; j <= m; j++) {
        dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
    }
}
out.println(dp[m]);

注意完全背包中j的循环方向发生变化。

多重背包

问题:有n个重量和价值分别为wi和vi的物品,每种物品数量为si,从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。

使用朴素解法所以转移方程为:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − k ∗ w i ] + k ∗ v [ i ] ) dp[i][j]=max(dp[i-1][j-k*wi]+k*v[i]) dp[i][j]=max(dp[i1][jkwi]+kv[i])
其中k表示当前物品选择次数,空间优化的转移方程:
d p [ j ] = m a x ( d p [ j − k ∗ w i ] + k ∗ v [ i ] ) dp[j]=max(dp[j-k*wi]+k*v[i]) dp[j]=max(dp[jkwi]+kv[i])
根据转移方程得到如下代码:

for (int i = 0; i < n; i++) {
    for (int j = m; j >= 0; j--) {
        for (int k = 0; k <= s[k]; k++) {
            if (k * w[i] > j) {
                continue;
            }
        	dp[j] = Math.max(dp[j], dp[j - k * w[i]] + k * v[i]);
        }
    }
}
out.println(dp[m]);
单调队列优化

单调队列优化的目标是优化第3层循环,第三层中是计算某几项的最大值,而且这几项是有一定规律的,
d p [ j ] = m a x ( d p [ j ] , d p [ j − w i ] + v i , d p [ j − 2 ∗ w i ] + 2 ∗ v i , ⋯   , d p [ j − k ∗ w i ] + k ∗ v i ) , 0 < = k < = j / w i dp[j]=max(dp[j],dp[j-wi]+vi,dp[j-2*wi]+2*vi,\cdots,dp[j-k*wi]+k*vi), 0<=k<=j/wi dp[j]=max(dp[j],dp[jwi]+vi,dp[j2wi]+2vi,,dp[jkwi]+kvi),0<=k<=j/wi

d p [ j ] = m a x ( d p [ j − ( k − 1 ) ∗ w i ] + k ∗ v i ) = m a x ( d p [ j ′ ] + ( j − j ′ ) w i ∗ v i ) dp[j]=max(dp[j-(k-1)*wi]+k*vi)=max(dp[j']+\frac{(j-j')}{wi} * vi) dp[j]=max(dp[j(k1)wi]+kvi)=max(dp[j]+wi(jj)vi)

上面的公式看起来不太直观,公式中参与比较的索引都同余,将同余的索引用 a j a_j aj表示,
d p [ a j ] = m a x ( d p [ a j ′ ] + ( j − j ′ ) ∗ v i ) = m a x ( d p [ a j ′ ] − j ′ ∗ v i ) + j ∗ v i , s i + j ′ < = j dp[a_{j}]=max(dp[a_{j'}]+(j-j')*vi)=max(dp[a_{j'}]-j'*vi)+j*vi,si+j'<=j dp[aj]=max(dp[aj]+(jj)vi)=max(dp[aj]jvi)+jvi,si+j<=j

Deque<Integer> queVal = new LinkedList<>();
Deque<Integer> queIndex = new LinkedList<>();
for (int i = 0; i < n; i++) {
    for (int a = 0; a < w[i]; a++) {
        queVal.clear();
        for (int j = 0; a + j * w[i] <= m; j++) {
            int val = dp[a + j * w[i]] - j * v[i];
            while (!queVal.isEmpty() && queVal.peekLast() <= val) {
                queVal.removeLast();
                queIndex.removeLast();
            }
            queVal.addLast(val);
            queIndex.addLast(j);
            dp[a + j * w[i]] = queVal.peekFirst() + j * v[i];
            if (queIndex.peekFirst() + s[i] == j) {
                queIndex.removeFirst();
            }
        }
    }
}
out.println(dp[m]);
其他问题
  • 有一些问题需要将背包刚好装满,这时需要设置异常值用来表示无法满包,例如在leetcode322找零问题中,将初始值设置1000000007表示无法凑成所给的总金额,。
int INF = 1000000007;
int[] dp = new int[amount+1];
Arrays.fill(dp, INF);
dp[0] = 0;
for (int i = 0; i < coins.length; i++) {
    for (int j = coins[i]; j <= amount; j++) {
        dp[j] = Math.min(dp[j], dp[j-coins[i]] + 1);
    }
}
return dp[amount] >= INF ? -1 : dp[amount];

leetcode518是计数问题,初始化为0表示无法满包,即方案数为0。

int[] dp = new int[amount + 1];
Arrays.fill(dp, 0);
dp[0] = 1;
for (int i = 0; i < coins.length; i++) {
    for (int j = coins[i]; j <= amount; j++) {
        dp[j] += dp[j - coins[i]];
    }
}
return dp[amount];
  • 负数背包,因为dp的索引存在负数,所以需要将索引整体右移offset,直到索引全部为正数,初始索引(零点)为offset。且遍历时需要根据容量的正负考虑不同的遍历方向。
待续

多重背包二进制优化,多重背包计数,多重背包完全装满等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值