背包问题培训学案.docx
《背包问题培训学案.docx》由会员分享,可在线阅读,更多相关《背包问题培训学案.docx(17页珍藏版)》请在冰豆网上搜索。
背包问题培训学案
背包问题
(2)
2010年3月11日晚
培训内容:
1、砝码称重的背包解法。
2、SubsetSums集合的背包解法。
3、数字游戏。
4、滚动数组的应用。
砝码称重的背包解法
【问题分析】11122333
把问题稍做一个改动,已知a1+a2+a3+a4+a5+a6个砝码的重量w[i],w[i]∈{1,2,3,5,10,20}其中砝码重量可以相等,求用这些砝码可称出的不同重量的个数。
这样一改就是经典的0/1背包问题的简化版了。
把a1个砝码看成0/1背包中的第1个物品,重量与价值均为a1*1。
把a2个砝码看成0/1背包中的第2个物品,重量与价值均为a2*2。
只是要注意这个题目不是求最大载重量,是统计所有的可称出的重量的个数。
programfmcz;
constw:
array[1..6]of1..20=(1,2,3,5,10,20);
maxll=1001;
var
i,j:
longint;
a,b:
array[1..10]oflongint;
f:
array[1..1001]oflongint;
begin
fori:
=1to6do
begin
read(a[i]);
b[i]:
=a[i]*w[i];
end;
readln;
fori:
=1to6do
begin
forj:
=maxlldowntob[i]do
begin
iff[j-b[i]]+b[i]>f[j]thenf[j]:
=f[j-b[i]]+b[i];{体积与价值相同}
end;
end;
writeln(f[maxll]);{maxll能达到多少就有多少种重量}
end.
砝码称重的测试数据如下:
4.1110000Total=3
4.2220000Total=6
4.3103000Total=7
4.4340500Total=36
4.5222222Total=82
4.6032745Total=185
4.7063421Total=79
4.8123456Total=204
4.8654321Total=83
4.101010101011Total=140
SubsetSums集合背包解法
SubsetSums
集合
对于从1到N的连续整集合合,能划分成两个子集合,且保证每个集合的数字和是相等的。
举个例子,如果N=3,对于{1,2,3}能划分成两个子集合,他们每个的所有数字和是相等的:
∙{3}and{1,2}
这是唯一一种分发(交换集合位置被认为是同一种划分方案,因此不会增加划分方案总数)
如果N=7,有四种方法能划分集合{1,2,3,4,5,6,7},每一种分发的子集合各数字和是相等的:
∙{1,6,7}and{2,3,4,5}{注1+6+7=2+3+4+5}
∙{2,5,7}and{1,3,4,6}
∙{3,4,7}and{1,2,5,6}
∙{1,2,4,7}and{3,5,6}
给出N,你的程序应该输出划分方案总数,如果不存在这样的划分方案,则输出0。
程序不能预存结果直接输出。
PROGRAMNAME:
subset
INPUTFORMAT
输入文件只有一行,且只有一个整数N
SAMPLEINPUT(filesubset.in)
7
OUTPUTFORMAT
输出划分方案总数,如果不存在则输出0。
SAMPLEOUTPUT(filesubset.out)
4
当1到n的总和为奇数时,一定没有一种方案,可以直接输出0,否则把总和div2当作背包的容量,用1到n个数去做0/1背包,把每一种情况加起来,状态方程:
f[j]=f[j]+f[j-i],由于n个数都使用了两次,所以情况总数也就是答案的2倍,所以输出时div2就可以了。
不知道为什么的请看下面:
如n=3时,s=3;集合有[{1,2}和{3},{3}和{1,2}]这样就重复了,所以div2。
f[j]集合的是由{j-i}和{i}组合的.f[j-i]有几种情况,那j-i和i的组合就有几种情况了,这是加法原理。
}
programsubset;
varn,i,j:
longint;
s:
int64;
st:
array[0..390]ofint64;
begin
assign(input,'subset.in');reset(input);
assign(output,'subset.out');rewrite(output);
whilenoteofdo
begin
read(n);
s:
=(n+1)*nshr1;
ifsand1=1thenwriteln(0)
else
begin
s:
=sshr1;
fori:
=1tondo
forj:
=sdowntoido
f[j]:
=f[j]+f[j-i];
writeln(f[s]shr1);
end;
end;
close(input);close(output);
end.
varf:
array[0..100000]ofint64;
i,n,m,j:
longint;
begin
assign(input,'subset.in');
reset(input);
assign(output,'subset.out');
rewrite(output);
readln(n);
close(input);
m:
=(1+n)*ndiv2;
ifmmod2=1thenbeginwriteln('0');close(output);halt;end;
m:
=mdiv2;
f[0]:
=1;
fori:
=1tondo
begin
forj:
=m-idownto0do
f[i+j]:
=f[i+j]+f[j];{f[m]=f[m]+f[m-1]f[m-1]=f[m-1]+f[m-2]}
end;
writeln(f[m]div2);
close(output);
end.
动态规划一般解决两类问题,一类是最优化问题,就是问你最大价值最小数什么的,另一类是方案总数问题。
滚动数组
滚动数组就是动态规划时反复利用已开辟的空间,丢弃大量无用数组的方法
作用是大规模动规时省内存
常用于DP之中,在DP过程中,我们在由一个状态转向另一个状态时,很可能之前存储的某些状态信息就已经无用了,例如在01背包问题中,从理解角度讲我们应开DP[i][j]的二维数组,第一维我们存处理到第几个物品,也就是阶段了,第二维存储容量,但是我们获得DP[i],只需使用DP[i-1]的信息,DP[i-k],k>1都成了无用空间,因此我们可以将数组开成一维就行,迭代更新数组中内容,滚动数组也是这个原理,目的也一样,不过这时候的问题常常是不可能缩成一维的了,比如一个DP[i][j]需要由DP[i-1][k],DP[i-2][k]决定,i不知道怎么说?
举个例子
d[0]d[1]d[2]d[3]d[4]d[5]d[6]d[7]d[8]
112358132134
d:
array[1..100]ofinteger;
d[0]=1;d[1]=1;
for(i=2;i<100;i++)
d[i]=d[i-1]+d[i-2];
printf("%d",d[99]);
上面这个循环d[i]只依赖于前两个数据d[i-1]和d[i-2];
为了节约空间用滚动数组的做法
d:
array[1..100]ofinteger;
d[0]d[1]d[2]d[0]d[1]d[2]d[0]d[1]d[2]
112358132134
d[0]=1;d[1]=1;
for(i=2;i<100;i++)
d[i%3]=d[(i-1)%3]+d[(i-2)%3];
printf("%d",d[99%3]);
注意上面的取余运算,我们成功地只保留了需要的最后3个解,数组好象在“滚动”一样,所以叫滚动数组
对于二维也可以用(代码可能不太正确和完善,但是可以理解例子):
inti,j,d[100][100];
for(i=1;i<100;i++)
for(j=0;j<100;j++)
d[i][j]=d[i-1][j]+d[i][j-1];
上面的d[i][j]只依赖于d[i-1][j],d[i][j-1];
运用滚动数组
inti,,j,d[2][100];
for(i=1;i<100;i++)
for(j=0;j<100;j++)
d[i%2][j]=d[(i-1)%2][j]+d[i%2][j-1];
光光的作业(homework)
[问题描述]
光光上了高中,科目增多了。
在长假里,光光的老师们都非常严厉,都给他布置了一定量的作业。
假期里,光光一共有的时间是k小时。
在长假前,老师们一共给光光布置了n份作业,第i份作业需要的时间是ti小时。
但是由于老师们互相不商量,因此光光有可能不能完成老师的作业。
当可能不能完成老师的作业时,光光就事后去向老师说明,然后被老师批评一顿了事。
对于一件作业,只有2种情况:
完成或者不完成(快要完成也算不完成)。
如果没完成,受到批评是天经地义的。
但是,不同的作业对于光光来说,批评的力度是不同的。
第i件作业如果没完成,就要受到pi个单位的批评。
多次这样之后,光光想要在长假前就知道他至少会受到多少个单位的批评。
你能帮助他吗?
[输入]
输入文件homework.in包含以下内容:
第一行只有一个数字k。
第二行只有一个数字n。
接下来n行,每行两个数字,分别是ti和pi,两个数字之间用一个空格分开。
100%的数据中,k<=100000,ti<=10000,pi<=10000;
30%的数据中,n<=20;
100%的数据中,n<=500。
[输出]
输出文件homework.out仅包含一行,是一个数字,代表了光光最少受到的批评。
[样例]
homework.in
5
3
26
13
47
homework.out
6
滚动数组例题:
var
f:
array[0..1,0..100000]oflongint;
t,p:
array[1..500]ofinteger;
k,n,i,j,c,c2,pall:
longint;
begin
//assign(input,'homework.in');reset(input);
//assign(output,'homework.out');rewrte(output);
readln(k);
readln(n);
pall:
=0;
fillchar(f,sizeof(f),0);
fori:
=1tondobegin
readln(t[i],p[i]);
pall:
=pall+p[i];
forj:
=t[i]tokdo
iff[0,j]
=p[i];
end;
c:
=0;
fori:
=1tondobegin
c:
=1-c;
forj:
=1tokdobegin
f[c,j]:
=f[c,j-1];
ifj-t[i]>0then
iff[1-c,j-t[i]]+p[i]>f[c,j]thenf[c,j]:
=f[1-c,j-t[i]]+p[i];(mod2)
end;
end;
writeln(pall-f[c,k]);
//close(input);close(output);
end.
1、P34:
数字游戏
要求:
能读懂该程序;能回答该题后面提出的一个问题:
把两阶段的程序段合并成一段。
思考:
如果要将ai、ai+1、ai+2……、aj-1、aj分成p部分,怎样分?
1≤i≤n
1≤j≤n
i≤k≤j-1
将ai、ai+1……、ak、ak+1、ak+2……aj
分成p-1部分分成第p部分
比如:
把1、2、3、4、5、6分成2部分,
123456
第m部分
如果ai、ai+1……、ak、ak+1、ak+2……aj不在一条直线上,而是在圆周上,怎么划分呢?
m-1部分由i..j组成。
第m部分由1..i-1和j+1..n组成。
m-1部分
fori:
=1tondo{计算一部分内的数和对10的模的所有可能情况}
forj:
=i+1tondo
begin
g[i,j]:
=(g[i,j-1]+g[j,j])mod10;
fmax1[i,j]:
=g[i,j];
fmin1[i,j]:
=g[i,j];
end;{for}
forp:
=2tom-1do{阶段:
递推计算划分2部分…m-1部分的结果值}
begin
fillchar(fmax,sizeof(fmax),0);{划分p部分的状态转移方程初始化}
fillchar(fmin,sizeof(fmin),$FF);
fori:
=1tondo{状态:
枚举被划分为p部分的数字区间}
forj:
=itondo
fork:
=itoj-1do{决策:
ai..ak被划分成p-1部分}
begin
iffmax1[i,k]*g[k+1,j]>fmax[i,j]then
{计算将ai、ai+1、…aj划分成p个部分的状态转移方程}
fmax[i,j]:
=fmax1[i,k]*g[k+1,j];
if(fmin1[i,k]>=0)and((fmin1[i,k]*g[k+1,j]fmin[i,j]:
=fmin1[i,k]*g[k+1,j];
end;{for}
fmin1:
=fmin;fmax1:
=fmax;
end;{for}
max:
=0;min:
=maxlongint;{将a1、a2、…an划分成m个部分的最大值和最小值初始化}
ifm=1then{计算n个数划分成一部分的最大值和最小值}
beginmax:
=g[1,n];min:
=g[1,n];end{then}
elsefori:
=1tondo{将a1…ai-1、aj+1…an设为第m部分,计算最大值和最小值}
forj:
=itondo
if(i<>1)or(j<>n)then
begin
if(g[1,i-1]+g[j+1,n])mod10*fmax1[i,j]>maxthen
max:
=(g[1,i-1]+g[j+1,n])mod10*fmax1[i,j];
if(fmin1[i,j]>=0)and((g[1,i-1]+g[j+1,n])mod10*fmin1[i,j]min:
=(g[1,i-1]+g[j+1,n])mod10*fmin1[i,j];
end;{then}
writeln(min);writeln(max);{输出最小值和最大值}
本题的计算过程分两个阶段
第一个阶段:
将圆周上的n个数排成一个序列,计算ai、ai+1、…aj划分成m-1个部分的最大值fmax1[i,j]和最小值fmin1[i,j];
第二个阶段:
将序列首尾相接。
枚举第m部分的所有可能情况,在fmax1和fmin1的基础上,计算圆周上的n个数划分成m个部分的最大值max和最小值min。
由于是一个圈,所以要从1-n中的每个点打断进行DP,最后统计最大最小值
思考:
能否将两个阶段合并,用一个状态转移方程来解决呢?
请修改程序。
6mod10=(-4)mod10=6
将N个数拉成2N个数的链,秒杀
vari,j,k,m,n,l,max,min,value:
longint;
a:
array[1..50]oflongint;
sum:
array[0..100]oflongint;
f,g:
array[1..50,1..10]oflongint;
functionmin1(i,j:
longint):
longint;
begin
ifi exit(j);
end;
functionget(i,j:
longint):
longint;
2
begin
get:
=sum[j]-sum[i-1];
ifget<0theninc(get,100000000);
get:
=getmod10;
end;
4
-1
begin
max:
=-maxlongint;
min:
=maxlongint;
readln(n,m);
3
fori:
=1tondo
begin
read(a[i]);
inc(sum[i],sum[i-1]+a[i]);(sum[1]=2sum[2]=1sum[3]=4sum[4]=8)
end;
fori:
=1tondo
inc(sum[i+n],sum[i+n-1]+a[i]);
fori:
=1tondo
begin
value:
=get(i,i);
f[1,1]:
=value;
g[1,1]:
=value;
forj:
=2tondo
begin
value:
=get(i,i+j-1);
f[j,1]:
=value;
g[j,1]:
=value;
fork:
=2tomin1(j,m)do
begin
f[j,k]:
=-maxlongint;
g[j,k]:
=maxlongint;
forl:
=1toj-1do
begin
value:
=get(l+i,j+i-1);
ifvalue*f[l,k-1]>f[j,k]thenf[j,k]:
=value*f[l,k-1];
ifvalue*g[l,k-1]=value*f[l,k-1]
end;
end;
end;
ifmax=f[n,m];
ifmin>g[n,m]thenmin:
=g[n,m];
end;
writeln(min);
writeln(max);
end.
vara,b:
array[0..100]oflongint;
f1,f2:
array[0..10,0..52]oflongint;
num:
array[0..51,0..51]oflongint;
n,m,i,t,max1,min1,j,k:
longint;
functionmax(x,y:
longint):
longint;
begin
ifx>ythenmax:
=xelsemax:
=y;
end;
functionmin(x,y:
longint):
longint;
begin
ifx>ythenmin:
=yelsemin:
=x;
end;
begin
readln(n,m);
fori:
=1tondoreadln(a[i]);
a[0]:
=a[n];
max1:
=-maxlongint;
min1:
=maxlongint;
fort:
=1tondo
begin
fillchar(num,sizeof(num),0);
fillchar(b,sizeof(b),0);
fillchar(f1,sizeof(f1),0);
fillchar(f2,sizeof(f2),0);
fori:
=ttot+n-1dob[i-t+1]:
=a[imodn];
fori:
=1tondo
forj:
=itondonum[i,j]:
=(num[i,j-1]+((b[j]+10000000)mod10))mod10;
fori:
=1tondo
begin
f1[1,i]:
=num[1,i];
f2[1,i]:
=num[1,i];
end;