背包问题.docx
《背包问题.docx》由会员分享,可在线阅读,更多相关《背包问题.docx(31页珍藏版)》请在冰豆网上搜索。
![背包问题.docx](https://file1.bdocx.com/fileroot1/2023-2/4/83419898-ab2a-4f4a-9674-bcbada12229c/83419898-ab2a-4f4a-9674-bcbada12229c1.gif)
背包问题
背包问题
常州一中林厚从
背包问题是信息学奥赛中的经典问题。
背包问题可以分为0-1背包和部分背包两种类型,0-1背包还可以再分为有限背包和无限背包(完全背包)。
背包问题的求解涉及到贪心、递归、递推、动态规划、搜索等多种算法。
熟练掌握各种背包问题及其变形试题的解法,是信息学奥赛选手从入门走向提高的必经之路。
先简单归纳一下涉及到的这几种重要算法:
1、贪心:
贪心法可以归纳为“每步取优”。
假设你的程序要走1~n共n步,则你只要保证在第i步(i=1..n)时走出的这一步是最优的。
所以,贪心法不是穷举,而只是一种每步都取优的走法。
但由于目光短浅,不考虑整体和全局,所以“步步最优”并不能保证最后的结果最优。
比如经典的“两头取数”问题、“n个整数连接成最大数”问题、“删数”问题等。
2、递归:
递归算法可以归纳为将问题“由大化小”。
也就是将一个大问题分解为若干个“性质相同”的子问题,求解的的过程,一般是通过“函数的递归调用”,不断将大问题逐步细化、直至元问题(边界情况),最后通过递归函数的自动返回得到问题的解。
递归算法的关键是递归函数的构造,它的效率往往比较低,原因在于大量的“冗余”计算。
比如经典的“斐波那挈数列”问题,在递归实现时效率极低,存在着大量的冗余计算,可以采用“记忆化”的方法优化。
3、递推:
递推问题往往有一个“递推公式”,其实和“递归公式”差不多,但是出发点不一样,递归的思想是“要想求什么就要先求出什么”。
而递推是从问题的边界情况(初始状态)出发,一步步往下走,直到走完n步,判断最后的解。
由于其中的每一步并不知道当前一步的哪一个值对后面的步骤有用,所以只能把所有情况(一步的所有走法)全部计算出来,也造成了很多的“冗余计算”。
时间上往往没有太多的优化余地,但空间上经常利用“滚动数组”等方式,把空间复杂度由O(n2)降到O(2n)。
比如经典的“杨辉三角形”问题、“判断n是否是斐波那挈数”问题等。
4、动态规划:
本质上是一种克服了“冗余”的“递归”算法。
它依然是不断地把一个大问题分解为若干个性质相同的子问题,而这些子问题里面会存在着很多的重叠。
为了高效地解决问题,不重复计算出现的重叠子问题,往往在求解的时候采用“递推”的方法,由小到大,按“阶段”一步一步生成。
所以如果重叠的子问题越多,则动态规划相比直接递归求解来说,效率就越高。
使用动态规划算法的前提条件是“无后效性”和“最优子结构”。
5、搜索:
就是一种带有策略的“穷举”。
实现方法多种多样,在这儿我们主要是指递归搜索(深搜)。
比如经典的“n的有序拆分”问题。
搜索的效率往往也是比较低的,一般可以通过合理的“剪枝”来优化。
第1节部分背包
所谓部分背包是指每件物品都可以取出部分装入背包中,而不是要么不要、要么全要。
例1、有一个背包,能承受的最大重量为max。
另有n件物品,第i件物品的重量为w[i]、价值为p[i],且每件物品都可以任意拆分。
求该背包能装入的物品最大总价值?
[算法分析]
方法:
贪心法
先计算所有物品的单位价值(即P[i]/W[i]的值),这个值越大,表明该物品的单位价值越大,那越是应该优先选择装入背包中。
此处的贪心策略是可以通过“反证法”严格证明的。
但并不是所有题目的贪心策略都可以方便地证明的,有些看似正确,其实贪心法并不能得到正确的解。
在有限的竞赛时间内,一般都是通过尝试不断找反例来验证自己的贪心策略的正确性。
Programex1a;
Constmaxn=100;
Varp,w:
array[1..maxn]ofreal;
temp,max,total:
real;
n,i,j:
integer;
Begin
Readln(n,max);{读入物品数及背包容量}
Fori:
=1tondo{读入每件物品的重量、价值}
Readln(w[i],p[i]);
fori:
=1ton-1do{按单位价值降序排列}
forj:
=i+1tondo
ifp[i]*w[j]
begin
temp:
=p[i];p[i]:
=p[j];p[j]:
=temp;
temp:
=w[i];w[i]:
=w[j];w[j]:
=temp;
end;
total:
=0;
fori:
=1tondo{从单位价值大的开始,贪心选取物品}
ifw[i]<=maxthenbegintotal:
=total+p[i];max:
=max-w[i];end{可以全取}
elsebegintotal:
=total+p[i]/w[i]*max;break;end;{只能取部分}
writeln(total:
0:
2);
end.
[测试数据]
输入:
5100
102
203
303
405
506
输出:
13.6
第2节0-1背包
所谓0-1背包,就是指一件物品是不可分的,要么不取(0)、要么全取
(1)。
根据每件物品的数量多少,0-1背包可以再分为0-1有限背包(每件物品只有1个)和0-1无限背包(完全背包),有时还有所谓的多重背包(即每件物品都有K[i]个)。
一、0-1有限背包
1、求最多可放入的体积
例2、装箱问题
[问题描述]
有一个箱子容量为maxv(正整数,0≤maxv≤20000),同时有n件物品(0≤n≤30),每件物品有一个体积vi(正整数)和一个价值pi(正整数)。
要求从这n件物品中任取若干件装入箱内,使箱子的剩余空间最小。
[输入样例]
1003{maxv,n}
704020{v1,v2,……,vn}
[样例输出]
10
[算法分析]
方法1:
搜索
Programex2a;
constmaxn=31;
vari,best,n,maxv:
integer;
v,s:
array[0..maxn]ofinteger;{设s[n]为前n件物品的体积和}
proceduresearch(k,restv:
integer);{搜索第k个物品,当前剩余空间为restv}
begin
ifrestv=restv;
ifrestv-(s[n]-s[k-1])>=bestthenexit;
{如果把剩下的全部放进去,得到的解也不会比当前的更优,那么就剪枝}
ifk<=nthenbegin
ifrestv>=v[k]thensearch(k+1,restv-v[k]);
{如果第k件物品可以放进去,那么就递归放第k+1件物品}
search(k+1,restv);{如果第k件物品不放,则对第k+1件物品递归}
end;
end;
begin{main}
readln(maxv,n);
fori:
=1tondoread(v[i]);
best:
=maxv;{best用来记录最后的答案,初始化为箱子的容量}
fillchar(s,sizeof(s),0);
fori:
=1tondo
s[i]:
=s[i-1]+v[i];{预处理}
search(1,maxv);{从第1件物品开始搜,当前剩余体积为maxv}
writeln(best);
end.
方法2:
动态规划
设F[i,j]为一布尔型数组,表示在前i件物品中选择若干个放入箱子,其占用的体积正好为j的情况。
问题的解为maxv减去所有满足F[n,j]=true的j的最大值。
这样,就将一个最优化问题转化为一个判定性问题。
而:
F[i,j]=F[i-1,j-v[i]](v[i]<=j<=maxv)
边界:
F[0,0]:
=true
以上为未经优化的二维动态规划,主要的程序代码如下:
f[0,0]:
=true;
fori:
=1tondo
forj:
=maxvdowntov[i]do{注意与后面无限背包的区别}
f[i,j]:
=f[i-1,j]orf[i-1,j-v[i]];
fori:
=maxvdownto1do
iff[n,i]thenbegin
best:
=maxv-i;
break;
end;
F[i,j]的变化:
由上一行往下一行逐步推,红色的T表示值的变化。
j=0
1
…
20
…
30
…
40
…
60
…
70
…
90
…
100
i=0
T
F
F
F
F
F
F
F
F
1
FT
F
F
F
F
F
FT
F
F
2
FT
F
F
F
FT
F
FT
F
F
3(n)
FT
F
FT
F
FT
FT
FT
FT
F
观察发现,其实当前状态只与前一阶段状态有关,可以降至一维数组实现。
即用F[i]表示任意选取若干件物品,占用体积为i的情况可否实现,实现方法如下:
F[0]:
=true;
Fori:
=1tondo
Forj:
=maxvdowntov[i]do{注意与后面无限背包的区别}
F[j]:
=F[j]orF[j-v[i]];
参考程序如下:
Programex2b;
constmaxn=31;
vari,j,n,maxv:
integer;
v:
array[0..maxn]ofinteger;
f:
array[0..20000]ofboolean;
begin
readln(maxv,n);
fori:
=1tondoread(v[i]);
fori:
=1tomaxvdof[i]:
=false;
f[0]:
=true;
fori:
=1tondo
forj:
=maxvdowntov[i]do
f[j]:
=f[j]orf[j-v[i]];
fori:
=maxvdownto1do
iff[i]then
beginwriteln(maxv-i);halt;end;
end.
[测试数据]
输入:
105
12345
输出:
0
2、求可以放入的最大价值
[算法分析]
方法1:
搜索
Programex2c;
constmaxn=31;
vari,best,n,maxv:
integer;
v,p,s:
array[0..maxn]ofinteger;{设s[i]为前i件物品的总价值}
proceduresearch(k,restv,ps:
integer);
{搜索第k件物品,当前剩余空间为restv,当前总价值为ps}
begin
ifps>=bestthenbest:
=ps;{如果当前价值比当前最优答案优,则修改当前最优答案}
ifps+(s[n]-s[k-1])ifk<=nthenbegin
ifrestv>=v[k]thensearch(k+1,restv-v[k],ps+p[k]);{取该物品}
search(k+1,restv,ps);{不取该物品}
end;
end;
begin{main}
readln(maxv,n);
fori:
=1tondoread(v[i]);
fori:
=1tondoread(p[i]);
best:
=0;
fillchar(s,sizeof(s),0);
fori:
=1tondo
s[i]:
=s[i-1]+p[i];
search(1,maxv,0);
writeln(best);
end.
[测试数据]
输入:
1003
704020
504030
输出:
80
输入:
105
12345
54321
输出:
14
方法2:
动态规划
根据第n件物品取与不取的两种情况,可以得到动态转移方程为:
f[n,maxv]=max{f[n-1,maxv-v[n]],f[n-1,maxv]}。
也可以降为一维:
f[j]=max{f[j-v[i]]+p[i],f[j]},j从maxv到v[i],i从1到n。
Programex2d;
constmaxn=31;
vari,j,best,n,maxv:
integer;
v,p:
array[0..maxn]ofinteger;
f:
array[0..20000]ofinteger;
begin
readln(maxv,n);
fori:
=1tondoread(v[i]);
fori:
=1tondoread(p[i]);
fillchar(f,sizeof(f),0);
best:
=0;
fori:
=1tondo
forj:
=maxvdowntov[i]do
begin
iff[j-v[i]]+p[i]>f[j]thenf[j]:
=f[j-v[i]]+p[i];
iff[j]>bestthenbest:
=f[j];
end;
writeln(best);
end.
3、求恰好装满的情况数
[算法分析]
方法:
动态规划
转移方程为:
f[n,v]=f[n-1,v-w[n]]+f[n-1,v],表示从n件物品中取若干件,组成的体积为v的情况数,f[0,0]=1。
Programex2e;
constmaxn=31;
vari,j,n,maxv:
integer;
v:
array[0..maxn]ofinteger;
f:
array[0..maxn,0..20000]ofinteger;
begin
readln(maxv,n);
fori:
=1tondoread(v[i]);
fillchar(f,sizeof(f),0);
f[0,0]:
=1;
fori:
=1tondo
forj:
=0tomaxvdo
ifj=0thenf[i,j]:
=1
elseifj>=v[i]thenf[i,j]:
=f[i-1,j-v[i]]+f[i-1,j]
elsef[i,j]:
=f[i-1,j];
writeln(f[n,maxv]);
end.
[测试数据]
输入:
105
12345
输出:
3
二、0-1无限背包(完全背包)
例3、采药改编
把题目的每株草药都改成:
每种草药的数量都足够多
样例的输出为:
140
[算法分析]
问题就变成了0/1无限背包问题,可以用动态规划做,问题就是求采摘第m种草药需要的时间所能达到的的最大价值。
设方程f(j)表示取第i种草药(i为变量),花去时间j所能达到的最大价值,则动态转移方程为:
f(j)=max(f(j),f(j-time[i])+p[i])。
Programex3;
vartime:
array[1..100]oflongint;
p:
array[1..100]oflongint;
f:
array[0..1000]oflongint;
i,ans,t,m,j:
longint;
functionmax(a,b:
longint):
longint;
begin
ifa>bthenmax:
=aelsemax:
=b;
end;
begin{main}
readln(t,m);
fori:
=1tomdoreadln(time[i],p[i]);
fori:
=0totdof[i]:
=0;
fori:
=1tomdo
forj:
=time[i]totdo
f[j]:
=max(f[j],f[j-time[i]]+p[i]);
ans:
=0;
fori:
=1totdoans:
=max(ans,f[i]);
writeln(ans);
end.
三、0-1有限背包的变形——多重背包
例4、多重背包
[问题题目]
有N种物品和一个容量为V的背包。
第i种物品最多有m[i]件可用,每件体积是v[i],价值是w[i]。
求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
[输入样例]
1003{maxv,n}
704020{v1,v2,……,vn}
504030{p1,p2,……,pn}
123{m1,m2,m3……,mn}
[算法分析]
本题和0-1无限背包问题很类似。
基本的方程只需将0-1无限背包问题的方程略微一改即可,因为对于第i种物品有m[i]+1种策略:
取0件,取1件……取m[i]件。
令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则:
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=m[i]}。
复杂度是O(V*∑m[i])。
另一种好想好写的基本方法是转化为0-1有限背包求解:
把第i种物品换成m[i]件0-1有限背包中的物品,则得到了物品数为∑m[i]的01有限背包问题,直接求解,复杂度仍然是O(V*∑m[i])。
但是我们期望将它转化为01有限背包问题之后能够像0-1无限背包一样降低复杂度。
应用二进制的思想,我们考虑把第i种物品换成若干件物品,使得原问题中第i种物品可取的每种策略——取0..m[i]件——均能等价于取若干件代换以后的物品。
另外,取超过m[i]件的策略必不能出现。
方法是:
将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。
使这些系数分别为1,2,4,...,2^(k-1),m[i]-2^k+1,且k是满足m[i]-2^k+1>0的最大整数。
例如,如果m[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。
分成的这几件物品的系数和为m[i],表明不可能取多于m[i]件的第i种物品。
另外这种方法也能保证对于0..m[i]间的每一个整数,均可以用若干个系数的和表示。
这样就将第i种物品分成了(logm[i])种物品,将原问题转化为了复杂度为O(V*∑logm[i])的0-1有限背包问题,是很大的改进。
参考程序如下:
Programex4;
constmaxn=31;
vari,j,best,n,maxv,k,t:
integer;
v,p,num,v1,p1:
array[0..maxn]ofinteger;
f:
array[0..20000]ofinteger;
begin
readln(maxv,n);
fori:
=1tondoread(v[i]);
fori:
=1tondoread(p[i]);
fori:
=1tondoread(num[i]);
k:
=0;{组合后的物品个数}
fori:
=1tondo{对第i件物品进行组合}
begin
t:
=1;
whilenum[i]>0do
ifnum[i]>=tthenbegin
inc(k);
v1[k]:
=v[i]*t;
p1[k]:
=p[i]*t;
num[i]:
=num[i]-t;
t:
=t*2;
end
elsebegin
inc(k);
v1[k]:
=v[i]*num[i];
p1[k]:
=p[i]*num[i];
num[i]:
=0;
end;
end;
v:
=v1;p:
=p1;n:
=k;{更新物品}
fillchar(f,sizeof(f),0);
best:
=0;
fori:
=1tondo{01背包}
forj:
=maxvdowntov[i]do
begin
iff[j-v[i]]+p[i]>f[j]thenf[j]:
=f[j-v[i]]+p[i];
iff[j]>bestthenbest:
=f[j];
end;
writeln(best);
end.
3、求恰好装满的情况数
例5、求自然数n(<=1000)本质上不同的质数和的表达式的数目。
输入:
17
输出:
17
输入:
31
输出:
111
输入:
47
输出:
614
输入:
101
输出:
43709
[算法分析]
方法1:
穷举
生成每一个质数的系数的排列,再一一测试。
效率显然很低。
Programex5a;
varn,l,now,tot,i,j:
longint;
xs:
array[0..1001]oflongint;
pr:
array[0..1001]oflongint;
flag:
boolean;
procedurecal(x:
longint);
vari:
longint;
begin
now:
=0;
fori:
=1toxdo
inc(now,xs[i]*pr[i]);
end;
proceduretry(dep:
longint);
vari,j:
longint;
begin
cal(dep-1);
ifnow>nthenexit;
ifdep=l+1then
begin
cal(dep-1);
ifnow=ntheninc(tot);
exit;
end;
fori:
=0tondivpr[dep]do
begin
xs[dep]:
=i;
try(dep+1);
xs[dep]:
=0;
end;
end;
begin{main}
readln(n);
l:
=0;
fori:
=2tondo
begin
flag:
=true;
forj:
=2totrunc(sqrt(i))do
ifimodj=0then
begin
flag:
=false;
break;
end;
ifflagthenbegininc(l);pr[l]:
=i;end;
end;
tot:
=0;
try
(1);
writeln(tot);
end.
方法2:
递归搜索
Programex5b;
varn,l,tot,i,j:
longint;
pr:
array[0..1001]oflongint;
flag:
boolean;
proceduretry(dep,rest:
longint);
vari,j,x:
longint;
begin
if(rest<=0)or(dep=l+1)then
begin
ifrest=0theninc(tot);
exit;
end;
fori:
=0torestdivpr[dep]do
try(dep+1,rest-pr[dep]*i);
end;
begin{main}
readln(n);
l:
=0;