前言
今天做到了一道涉及动态规划的问题,自己不会解,看题解都说是0-1背包问题的变体,这个问题刚开始就没太明白,后来每次遇到也没有很好解决,所以今天下定决心把它学会
0-1背包问题
有 N 件物品和一个容量为 V 的背包。放入第 i 件物品占用的体积是 C[i] ,得到的价值是 W[i] 。求解将哪些物品装入背包可使价值总和最大。求出最大总价值。
代码如下:注释相对解释的比较清楚
- 二维数组版
// V是背包容量,C是每件物品的体积,W是每件物品的价值量
public static int zeroOnePack(int V, int[] C, int[] W) {
// 防止无效输入
if ((V <= 0) || (C.length != W.length)) {
return 0;
}
int n = C.length;//物品个数
// dp[i][j]: 对于下标为 0~i 的物品,背包容量为 j 时的最大价值
int[][] dp = new int[n + 1][V + 1];
// 背包空的情况下,价值为 0
dp[0][0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= V; ++j) {
// 同样的背包容量,不选物品 i 的话,当前价值就是取到前一个物品的最大价值,也就是 dp[i - 1][j]
dp[i][j] = dp[i - 1][j];
// 如果选取i,则背包容量减少C[i],价值增加W[i]
// 如果选择物品 i 使得当前价值相对不选更大,那就选取 i,更新当前最大价值
if ((j >= C[i]) && (dp[i][j] < dp[i-1][j - C[i]] + W[i])) {
dp[i][j] = dp[i-1][j - C[i]] + W[i];
}
}
}
// 返回,对于所有物品(0~N),背包容量为 V 时的最大价值
return dp[n][V];
}
- 一维数组版
当前行的遍历用到的值是上一行的前面列的值,如果我们第二层 for 循环遍历的时候倒着遍历的话,保证了前面更新的值不会被新计算的值覆盖掉,仅仅用一维数组就可以解决问题,对空间进行了优化
public static int zeroOnePackOpt(int V, int[] C, int[] W) {
// 防止无效输入
if ((V <= 0) || (C.length != W.length)) {
return 0;
}
int n = C.length;
int[] dp = new int[V + 1];
// 背包空的情况下,价值为 0
dp[0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = V; j >= C[i]; --j) {
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
return dp[V];
}
模拟过程
推荐一个视频,这是b站上一个up主做的,只有3分钟,他把这个过程用动画形式表现了出来,我觉得是我看了那么多解释里解释的最好的,链接如下: 0-1背包问题
数据
具体过程(截取了比较有代表性的几步)
最终结果
P.S 这个up主竟然是个高中生,惊呆
邮资问题
某人有8 角的邮票5 张,1 元的邮票4 张,1 元8 角的邮票6 张,用这些邮票中的一张或若干张可以得到多少中不同的邮资?
题解
动态规划
- 把这道题想成求是否有恰好装满容量为8,9,…188的背包的方案,8和188是题目所给邮资的最小和最大值,这也是0-1背包问题的一种经典问法,代码如下:
#include<iostream>
using namespace std;
int main()
{
int f[189]={
1};
int a[] = {
0,8,8,8,8,8,10,10,10,10,18,18,18,18,18,18};
//f[j]即邮资为j的方案数
for(int i=1;i<16;i++)
{
for(int j=188;j>=a[i];j--)
f[j]+=f[j-a[i]];//最重要的一步
}
int count=0;//统计邮资情况总数
//f[i]不为0,则说明邮资为i有方案,计入count
for(int i=1;i<189;i++)
{
if(f[i])
count++;
}
cout<<count;
}
暴力破解
- 当然暴力破解也是可以的,可以利用set去重,代码如下:
#include<iostream>
#include<set>
using namespace std;
int main()
{
set<int>num;
for(int i=0;i<=5;i++)
{
for(int j=0;j<=4;j++)
{
for(int k=0;k<=6;k++)
{
//至少一张邮票,排除i,j,k都为0的情况
if(i+j+k==0)
continue;
num.insert(8*i+10*j+18*k);
}
}
}
cout<<num.size();
}