背包问题
参考文献:背包九讲
一:01背包问题
最基础的背包问题,关键是每个物品只要一件,基本的状态转移方程就是:f[i][v]=max{f[i-1][v],f[i-1][v-w[i]]+v[i]}
有个需要注意的地方是:要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。可以这么思考,因为要求装满,一开始只要f[0]时满足“装满”,这时为0,其余的都没有合法解,因此为-∞。一般做法时间复杂度和空间复杂度都是O(N*V),可以将空间复杂度降到O(V)
例子:HDU 2602:
空间复杂度为:O(N*V):
16512821 | 2016-03-11 15:20:45 | Accepted | 2602 | 78MS | 5732K | 633 B | G++ | seasonal |
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define max(a,b) a>b?a:b
int f[1050][1050];
int value[1050], volume[1050];
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
int n, v,i,j;
memset(f, 0, sizeof(f));
scanf("%d%d", &n, &v);
for (i = 1; i <= n; i++)
scanf("%d", &value[i]);
for (i = 1; i <= n; i++)
scanf("%d", &volume[i]);
for (i = 1; i <= n; i++)
for (j = 0; j <= v; j++)
{
if (volume[i] <= j)//假如当前的能放入背包才考虑状态转移方程
f[i][j] = max(f[i - 1][j], f[i - 1][j - volume[i]] + value[i]);
else //否则直接等于上一个,相当于这个不放
f[i][j] = f[i - 1][j];
}
printf("%d\n", f[n][v]);
}
return 0;
}
要点就是利用f[v]储存上一个i-1的值,此时f[v]与f[v-w[i]]比较时其实已经是在比较i-1时的值。注意要倒序,使j从V…0,因为此时f[j]存储的实际是f[i-1][j]的值,如果从小到大算,假设要求f[5]先要求出f[3],求出f[3]后f[3]储存的就是f[i][3],而计算f[5]需要的是f[i-1][3],所以要倒序保证小的值不会被更新。
16513374 | 2016-03-11 16:28:38 | Accepted | 2602 | 31MS | 1424K | 538 B | G++ | seasonal |
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define max(a,b) a>b?a:b
int f[1050];
int value[1050], volume[1050];
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
int n, v,i,j;
memset(f, 0, sizeof(f));
scanf("%d%d", &n, &v);
for (i = 1; i <= n; i++)
scanf("%d", &value[i]);
for (i = 1; i <= n; i++)
scanf("%d", &volume[i]);
for (i = 1; i <= n; i++)
for (j = v; j >= volume[i]; j--)//这里是倒序,使f[j]与f[i-1][j],f[j-volume[i]]与f[i-1][j-volume[i]]相互对应
f[j] = max(f[j], f[j - volume[i]] + value[i]);//f[j]相当于存了前一个i-1的f值,此时比较与原本二维一个道理
printf("%d\n", f[v]);
}
return 0;
}
二:完全背包问题
与01背包最大的不同在于物品的数量可以是无穷个,所以基本的二维状态转移方程应该是这样的:
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]},0<=k*c[i]<=v
这里用二维其实是比较麻烦的,因为涉及到k不好写,用一维的方法反而比较好写:
for i=1..N
for v=0..V
f[v]=max{f[v],f[v-cost]+weight}
但这里与01背包不同之处在于它的v是升序的,01背包中降序是为了确保物品只加入一个,使上一个i-1的状态(没有加物品)不会被更新掉,而完全背包中因为物品有无穷个,反而需要不停更新,使上一个i-1的状态也可以是加过了物品,所以必须要用升序。
例子:POJ1384&&HDU1114
题意:
往储蓄罐里存硬币,已知空储蓄罐和放满后的重量,给出n种硬币的价值和重量,要求装满储蓄罐可能的最小价值和
要点:
首先是要求必须装满,这里就是前面01背包的初始化问题,DP[0]=0,其余的均赋值为+∞,然后用完全背包求最小值。状态转移方程为:dp[j] = min(dp[j], dp[j - w[i]] + p[i])
16536210 | 2016-03-13 10:29:57 | Accepted | 1114 | 78MS | 1460K | 697 B | G++ | seasonal |
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define INF 0xfffffff
#define min(a,b) a>b?b:a
int dp[10005],w[505],p[505];
int main()
{
int empty, full,T;
int i, j;
scanf("%d", &T);
while (T--)
{
scanf("%d%d", &empty, &full);
int v = full - empty;
int n;
scanf("%d", &n);
for (i = 0; i < n; i++)
scanf("%d%d", &p[i], &w[i]);
for (j = 0; j <= v; j++)
dp[j] = INF; //注意初始化
dp[0] = 0;
for (i = 0; i < n; i++)
for (j = w[i]; j <= v; j++)//一维的完全背包
dp[j] = min(dp[j], dp[j - w[i]] + p[i]);
if (dp[v] == INF)
printf("This is impossible.\n");
else
printf("The minimum amount of money in the piggy-bank is %d.\n", dp[v]);
}
return 0;
}
三:多重背包
不同之处在于物品规定了个数,第i个物品有c[i]个。一般使用二进制拆解,将复杂度降为O(V*Σlog n[i]),方法如下:
针对第i个物品有c[i]个,我们可以将c[i]拆成1,2,4,...,2^(k-1),n[i]-2^k+1,这样想放1~n[i]中任意个都可以用前面的系数表示,如9=1+2+4+2,这样同时将对应的物品价值和重量乘系数作为一个新的物品,同时这个物品只有一个,就是01背包。还有如果一开始物品数大于总重量/物品质量,就直接可以视为无穷个,因为反正都是用不光,就转化为完全背包问题。
伪代码:
procedure MultiplePack(cost,weight,amount)
if cost*amount>=V
CompletePack(cost,weight)
return
integer k=1
while k<num
ZeroOnePack(k*cost,k*weight)
amount=amount-k
k=k*2
ZeroOnePack(amount*cost,amount*weight)
例子:HDU2191
16570781 | 2016-03-16 14:37:38 | Accepted | 2191 | 0MS | 1432K | 985 B | G++ | seasonal |
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define max(a,b) a>b?a:b
int v[1500], w[1500], c[1500],dp[1500];
int n, m;
void one_pack(int cost, int weight)//01背包
{
for (int j = m; j >= cost; j--)
dp[j] = max(dp[j], dp[j - cost] + weight);
}
void complete_pack(int cost, int weight)//完全背包
{
for (int j = cost; j <= m; j++)
dp[j] = max(dp[j], dp[j -cost] + weight);
}
void multiple_pack(int cost, int weight, int count)
{
int i;
if (cost*count >= m)//如果一开始就count*cost>=m,可以看成无限种也就是完全背包
{
complete_pack(cost, weight);
}
else
{
int k=1;
while (k < count)
{
one_pack(k*cost, k*weight);//相当于01背包
count -= k;
k *= 2; //利用二进制思想可以表示1-count的所有值
}
one_pack(count*cost, count*weight);//最后处理剩下的个数
}
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
memset(dp, 0, sizeof(dp));
scanf("%d%d", &m, &n);
for (int i = 0; i < n; i++)
scanf("%d%d%d", &w[i], &v[i], &c[i]);
for (int i = 0; i < n; i++)
multiple_pack(w[i], v[i], c[i]);
printf("%d\n", dp[m]);
}
return 0;
}