背包问题
1 01背包问题
1.1 问题:
有(N)件物品和容量为(V)的背包,放第(i)件物品的体积是(C_i),得到的价值是(W_i),问放入背包哪些物品能使价值总和最大。
1.2 思路:
首先,在类似的问题中,贪心思想是错误的,这点可以自己思考一下。
在这样一个问题中,我们思考经典的动态规划的思路,对于每一个物品,我们有两种策略:放,或不放。
我们定义(F[i,v])为前(i)件物品恰好放入容量为(v)的背包可以得到的最大价值。
放:(F[i,v]=F[i-1,v-C_i]+W_i)
不放:(F[i,v]=F[i-1,v])
for(int i=1;i<=n;i++)
{
for(int j=c[i];j<=v;j++)
{
dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i]);
}
}
1.3 一些优化:
以上想法的时间空间复杂度均为(O(VN)) ,很显然时间复杂度不能再往下优化了。
但空间复杂度还可以优化,因为我们的(F(i,v))都是从(F(i-1,x)) 递推而来,也用不到(F(i,x)),考虑把二维压缩成一维 。
for(int i=1;i<=n;i++)
{
for(int j=V;j>=c[i];j--)
{
dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
}
}
这样只需保证在更新(dp[j]) 时,要用到的(dp[j])和(dp[j-c[i]]) 都还未在当轮被更新,保证我们用到的是(dp[i-1][j])和(dp[i-1][j-c[i]]) ,那么具体的实现就是把内层循环倒着跑,这样在更新(dp[j])时,确保比他小的(dp[j-c[i]])还未被更新
模板题代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define clean(a,b) memset(a,b,sizeof(a));
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
const int maxn=1e3+9;
int c[maxn],w[maxn],dp[maxn];
int n,v;
void ZeroOnePack(int c,int w)
{
for(int i=v;i>=c;i--)
{
dp[i]=max(dp[i],dp[i-c]+w);
}
}
int main()
{
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++) scanf("%d%d",&c[i],&w[i]);
for(int i=1;i<=v;i++) dp[i]=0;
for(int i=1;i<=n;i++) ZeroOnePack(c[i],w[i]);
printf("%d
",dp[v]);
return 0;
}
内层循环的下限其实可以改为(max(V-sum_{j=i}^{N}w[j],c[i]))
我们知道空间优化后的状态转移方程为(dp[i]=max(dp[j],dp[j-c[i]]+w[i])) (i为物品编号,j为当前体积v),那么对于([i+1,n])的情况,这里的(j-c[i]) 最多取值也就取到(sum_{j=i}^{N}w[j]),而(sum_{j=i}^{N}w[j]) 到(c[i]) 也不会取到,也就没有计算的必要
既然这样,我们就不用更新到过左的位置,也就是只需保证当前物品以及后面的物品都能放下就可以了
这种优化在背包体积很大时很有优势
被优化代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define clean(a,b) memset(a,b,sizeof(a));
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
const int maxn=1e3+9;
int c[maxn],w[maxn],dp[maxn],sum[maxn];
int n,v;
void ZeroOnePack(int c,int w,int sum)
{
int del=max(c,v-sum);
for(int i=v;i>=del;i--)
{
dp[i]=max(dp[i],dp[i-c]+w);
}
}
int main()
{
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++) scanf("%d%d",&c[i],&w[i]);
for(int i=n;i>=1;i--) sum[i]=sum[i+1]+w[i];
for(int i=1;i<=v;i++) dp[i]=0;
for(int i=1;i<=n;i++) ZeroOnePack(c[i],w[i],sum[i]);
printf("%d
",dp[v]);
return 0;
}
1.4 一些细节
在模板题中,题目并没有对是否要装满背包做出要求,但在一些其他的问题中会要求“恰好装满背包”的最优解,而区别在于初始化。
如果是恰好装满背包,那除了(dp[0]=0)外,(dp[i]=-inf (iin[1,V])) 因为(dp[i])代表容量为i的背包被恰好装满时的价值,我们(dp[0])可以理解为:容量为0的背包被“nothing”恰好装满时的价值为(0),但其他的i并没有类似的合法的解,属于一个未定义的状态。
同理,未被要求必须恰好装满时任何容量的背包都有一个合法的解,那就是装了“nothing”时的价值为(0)。
2 完全背包问题
2.1 题目:
有(N)种物品和容量为(V)的背包,每种物品可以无限取用,放第(i)种物品的体积是(C_i),得到的价值是(W_i),问放入背包哪些物品能使价值总和最大。
2.2 思路:
与01背包唯一不同的地方在于,他的每种物品有无限多个,而01背包每种物品只能取一次,而每种物品的策略也从(取或不取)两种变成了(取0件,取1件,取2件,...,取(left lfloor V/C_i ight floor)件)
如果仍用01背包的想法,把每种物品取多少,想成是每种物品的每一件我取还是不取
我们定义(F[i,v])为前(i)种物品恰好放入容量为(v)的背包可以得到的最大价值,得到状态转移方程
(F[i,v]=maxleft{F[i-1,v-kC_i]+kW_i|0leq kC_ileq v ight})
01背包的时间复杂度为(O(NK)) ,每种物品只有两个状态,完全背包每种物品有(left lfloor V/C_i ight floor+1) 个状态,时间复杂度为(O(NKsumfrac{V}{C_i}))
2.3 试着优化下
与01背包相比,这样的时间复杂度未免过于大了,我们考虑是否有方法把时间复杂度降下来
- 若两件物品(i,j) 满足(C_ileq C_j) 且(W_ileq W_j) ,则可以不用考虑(j)了
可以先将费用大于(V)的去掉,然后去找费用相同的物品,价值最高的那一个
虽然这种优化能大大减少物品的件数,但貌似并不能改善最坏情况下的时间复杂度
- 因为每个数都可以得到他的二进制表示,那么每一个问题的可行的答案都可以用满足(C_i2^kleq V) 的非负整数(k) (费用为(C_i2^k),价值(W_i2^k))的物品来表示
这样就可以把每种物品拆成(O(logleft lfloor V/C_i ight floor)) 件物品
- 在01背包中,我们让内层循环倒着跑的原因是只想让(F(i,x)) 用到(F(i-1,x)) 而不是还未更新的(F(i,x)) ,以保证每件物品只选一次,但如果是完全背包,就没有这种顾虑了
for(int i=1;i<=n;i++)
{
for(int j=c[i];j<=v;j++)
{
dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
}
}
我们发现,把这种写法回退到初始的二维,是这样的
(F[i,v]=maxleft{F[i-1,v],F[i,v-C_i]+W_i ight})
我们是否能给他一个合理的解释呢
确实,还是最初的那个取还是不取的问题,取?取(F[i,x])一定会比(F[i-1,x])要优吧 ( (F[i-1,x]leq F[i,x]) ),不取?那自然还是(F[i-1,v]) 了。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define clean(a,b) memset(a,b,sizeof(a));
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
const int maxn=1e3+9;
int c[maxn],w[maxn],dp[maxn];
int n,v;
void CompletePack(int c,int w)
{
for(int i=c;i<=v;i++)
{
dp[i]=max(dp[i],dp[i-c]+w);
}
}
int main()
{
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++) scanf("%d%d",&c[i],&w[i]);
for(int i=1;i<=n;i++) CompletePack(c[i],w[i]);
printf("%d
",dp[v]);
return 0;
}
3 多重背包问题
3.1 题目:
有(N)种物品和容量为(V)的背包,第(i)种物品最多有(M_i)件可用,放第(i)种物品的体积是(C_i),得到的价值是(W_i),问放入背包哪些物品能使价值总和最大,且空间总和不超过背包容量。
3.2 思路:
题目和完全背包类似,但多了些限制,对于第(i)种物品,我们的策略数从(left lfloor V/C_i ight floor+1) 变成了(M_i+1) (取0件,取1件,取2件,...,取(M_i)件)
我们定义(F[i,v])为前(i)种物品恰好放入容量为(v)的背包可以得到的最大价值,得到状态转移方程
(F[i,v]=maxleft{F[i-1,v-kC_i]+kW_i|0leq kleq M_i ight})
时间复杂度(O(Vsum M_i))
3.3 优化:
之前我们是吧多重背包用了完全背包的想法来想,那么他能否和01背包联系在一起呢
把第(i)种物品换成(M_i) 件01背包中的物品,得到了物品数为(sum M_i)的01背包问题,时间复杂度还是(O(Vsum M_i))
但我们仍考虑二进制的思想,我们把第(i)种物品换成若干件物品,是的原问题中第(i)种物品可取的每一种策略均能用我们分成的若干件物品代替,即(1,2,2^2,2^3,dots,2^{k-1},M_i-2^k+1) ,(k)是满足(M_i-2^k+1>0)的最大整数
例如(M_i=13),(k=3) 分成(1,2,4,6) 四件物品
这样,原问题的时间复杂度被降为 (O(Vsum logM_i)) √
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define clean(a,b) memset(a,b,sizeof(a));
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
const int maxn=1e3+9;
int c[maxn],w[maxn],dp[maxn],m[maxn];
int n,v;
void ZeroOnePack(int c,int w)
{
for(int i=v;i>=c;i--)
{
dp[i]=max(dp[i],dp[i-c]+w);
}
}
void CompletePack(int c,int w)
{
for(int i=c;i<=v;i++)
{
dp[i]=max(dp[i],dp[i-c]+w);
}
}
int main()
{
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++) scanf("%d%d%d",&c[i],&w[i],&m[i]);
for(int i=1;i<=n;i++)
{
if(m[i]*c[i]>=v) CompletePack(c[i],w[i]);
else
{
for(int k=1;k<m[i];k*=2)
{
ZeroOnePack(k*c[i],k*w[i]);
m[i]-=k;
}
ZeroOnePack(m[i]*c[i],m[i]*w[i]);
}
}
printf("%d
",dp[v]);
return 0;
}
4 混合背包问题
属于哪种背包就用哪种方法求解即可
for(int i=1;i<=n;i++)
{
if(第i件物品属于01背包) ZeroOnePack(c[i],w[i]);
else if(第i件物品属于完全背包) CompletePack(c[i],w[i]);
else if(第i件物品属于多重背包) MultiplePack(c[i],w[i],m[i]);
}
5 二维费用背包问题
5.1 问题
二维费用背包是指:对于每件物品,具有两种不同的费用,选择这件物品必须同时付出这两种费用,对于每种费用都有一个可付出的最大值(背包容量),那么怎样选择物品可以得到最大的价值?
设第(i)件物品所需的两种费用分别为(C_i) 和(D_i) 。两种可付出的最大值(也叫背包容量)分别为(V) 和(U) ,物品价值维(W_i)。
5.2 方法
费用加了一维,状态也加一维就好了,设(F[i,v,u]) 表示前(i)件物品付出两种费用分别为(v)和(u)时可获得的最大价值
可得到状态转移方程
$F[i,v,u]=max{F[i-1,v,u],F[i-1,v-C_i,u-D_i]+W_i} $
用之前优化空间的思想,把三维变成二维
当每件物品只取一次 循环逆序,当每件物品可选多次 循环顺序,当每件物品有固定件数时拆分物品 都是一样的
[参考自 崔添翼-背包九讲]