您的位置:首页 > 其它

[jzoj]1501. 糖果(优化多维背包的多种方法)

2017-05-31 14:24 267 查看
一般来说,求解多维背包是求最小价值,但是也可以求其他的,例如此题:

多维背包复杂度O(nmk),但有很多的优化方法.

Link

https://jzoj.net/senior/#contest/show/1992/2

Problem

给定n个背包的重量ci及个数ki,使其分成两堆使得差最小.

Data constraint

50%的数据1<=N,Ki,Ci<=100.

100%的数据1<=N<=100,1<=Ki<=500,1<=Ci<=200.

Solution

Method-1-1

我们直接采用多维背包求解,不加任何优化:

var
f:array[0..10000000] of boolean;
x,y:array[1..100] of longint;
n,i,j,k,sum,ans:longint;
function min(x,y:longint):longint;
begin
if x<y then exit(x); exit(y);
end;
begin
readln(n);
for i:=1 to n do
begin
readln(x[i],y[i]);
inc(sum,x[i]*y[i]);
end;

f[0]:=true;
for i:=1 to n do
for k:=1 to x[i] do
for j:=sum downto y[i] do
if f[j-y[i]] then f[j]:=true;

ans:=sum;
for i:=1 to sum do
if f[i] then ans:=min(ans,abs(i-(sum-i)));

writeln(ans);
end.


对于此题,可以拿到72分.

Method-1-2

我们采取两个优化:二进制优化每次sum依次累加的优化.

var
f:array[0..10000000] of boolean;
d:array[1..100,1..100] of longint;
x,y,len:array[1..100] of longint;
n,i,j,k,t,sum,ans:longint;
function min(x,y:longint):longint;
begin
if x<y then exit(x); exit(y);
end;
begin
readln(n);
for i:=1 to n do
begin
readln(x[i],y[i]);

t:=x[i];
k:=1;
while t-k>0 do
begin
t:=t-k;
inc(len[i]);
d[i,len[i]]:=k;
k:=k shl 1;
end;
inc(len[i]);
d[i,len[i]]:=t;
end;

f[0]:=true;
for i:=1 to n do
begin
inc(sum,x[i]*y[i]);
for k:=1 to len[i] do
for j:=sum downto y[i]*d[i,k] do
if f[j-y[i]*d[i,k]] then f[j]:=true;
end;

ans:=sum;
for i:=1 to sum do
if f[i] then ans:=min(ans,abs(i-(sum-i)));

writeln(ans);
end.


可以多过一个数据点,并且对于刚刚跑400ms的一个数据,现在只跑了40ms,可见优化之大,但由于数据的原因,并不能多拿多少分.

于是继续优化.

Method-1-3

注意到1<=Ci<=200,所以我们可以考虑把相同重量的背包合并在一起,那么二进制的优化就会明显很多.

var
tot,a,len:array[0..200] of longint;
bz:array[0..200] of boolean;
f:array[0..10000000] of boolean;
d:array[0..100,1..100] of longint;
n,i,j,k,t,sum,ans,lena,x,y,maxsum:longint;
function min(x,y:longint):longint;
begin
if x<y then exit(x); exit(y);
end;
begin
readln(n);
fillchar(bz,sizeof(bz),true);
for i:=1 to n do
begin
readln(x,y);
inc(maxsum,x*y);
inc(tot[y],x);
if bz[y] then
begin
bz[y]:=false;
inc(lena);
a[lena]:=y;
end;
end;
for i:=1 to lena do
begin
t:=tot[a[i]];
k:=1;
while t-k>0 do
begin
t:=t-k;
inc(len[i]);
d[i,len[i]]:=k;
k:=k shl 1;
end;
inc(len[i]);
d[i,len[i]]:=t;
end;

f[0]:=true;
for i:=1 to lena do
begin
inc(sum,a[i]*tot[a[i]]);
for k:=1 to len[i] do
for j:=sum downto a[i]*d[i,k] do
if f[j-a[i]*d[i,k]] then f[j]:=true;
end;

ans:=sum;
for i:=1 to sum do
if f[i] then ans:=min(ans,abs(i-(sum-i)));

writeln(ans);
end.


于是这样子,可以拿到92分,进了一大步.

Method-1-4

我们再来两个优化:

我们把Ai∗Di,k按从小到大排个序,可以发现刚刚跑900ms+的点只跑了500ms+,优化了近0.4秒.

当然,还有一个更加强劲的优化,我们判断如果当前可行的最优解已经产生,那么就没必要继续进行dp了,直接输出.

注意常数,如果把判断放在第三重循环里,就会多将近0.1秒.

var
tot,a,len:array[0..200] of longint;
bz:array[0..200] of boolean;
f:array[0..10000000] of boolean;
d:array[0..100,1..100] of longint;
n,i,j,k,t,sum,ans,lena,x,y,maxsum:longint;
function min(x,y:longint):longint;
begin
if x<y then exit(x); exit(y);
end;
begin
readln(n);
fillchar(bz,sizeof(bz),true);
for i:=1 to n do
begin
readln(x,y);
inc(maxsum,x*y);
inc(tot[y],x);
if bz[y] then
begin
bz[y]:=false;
inc(lena);
a[lena]:=y;
end;
end;
for i:=1 to lena do
begin
t:=tot[a[i]];
k:=1;
while t-k>0 do
begin
t:=t-k;
inc(len[i]);
d[i,len[i]]:=k;
k:=k shl 1;
end;
inc(len[i]);
d[i,len[i]]:=t;
end;

for i:=1 to lena-1 do
for j:=i+1 to lena do
if a[i]*tot[a[i]]>a[j]*tot[a[j]] then
begin
a[0]:=a[i]; a[i]:=a[j]; a[j]:=a[0];
len[0]:=len[i]; len[i]:=len[j]; len[j]:=len[0];
d[0]:=d[i]; d[i]:=d[j]; d[j]:=d[0];
end;

f[0]:=true;
for i:=1 to lena do
begin
inc(sum,a[i]*tot[a[i]]);
for k:=1 to len[i] do
begin
for j:=sum downto a[i]*d[i,k] do
if f[j-a[i]*d[i,k]] then f[j]:=true;
if f[maxsum div 2] then
begin
if maxsum mod 2=1 then writeln(1) else writeln(0);
halt;
end;
end;
end;

ans:=sum;
for i:=1 to sum do
if f[i] then ans:=min(ans,abs(i-(sum-i)));

writeln(ans);
end.


Method-2

根据这种题的一般规律,答案差是不会很大的, 我们限定在一个最大却又时间稳妥的范围,然后直接按照一般思路dp就好了.

即枚举一个状态,并通过这个状态由一个背包分成两份去更新下一个状态:

var
n,i,j,p,tmp:longint;
c,k:array[1..100]of longint;
f:array[0..100,0..400]of boolean;
begin
readln(n);
for i:=1 to n do
readln(k[i],c[i]);
f[0,0]:=true;
for i:=1 to n do
for j:=0 to 400 do
if f[i-1,j] then
for p:=0 to k[i] do
begin
tmp:=abs(j+(k[i]-p-p)*c[i]);
if tmp<=400 then f[i,tmp]:=true;
end;
for i:=0 to 400 do
if f[n,i] then
begin
writeln(i);
break;
end;
end.


这种方法对于这道题的数据很吃香,最大的一个点只跑了20ms,考试时可以将以上两种方法并用,使得正确率最高!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: