【POI2005】BAN-Bank Notes(单调队列优化多重背包)

这题如果不需要输出方案会简单一些,虽然我在洛谷上AC了但是我的输出方案并不一定正确,纯粹是数据的问题,希望AC的巨佬能贴出来让大家学习一下(网上实在没有洛谷这个版本的题解)

而且这题的重点应该是多重背包的优化。

一般的多重背包是这样的:

void solve()
{
	memset(f,0x3f,sizeof(f));
		
	f[0]=0;
	
	for(int i=1;i<=N;i++)
	for(int j=M;j>=b[i];j--)
	for(int k=1;k<=min(c[i],j/b[i]);k++)
		f[j]=min(f[j],f[j-b[i]*k]+k);
	
	cout<<f[M];
}

可以看出转移方程为:f[x]=min{ f[x-bi*j]+j } ( 1<=j<=min{ci,x/bi} ),这题数据肯定超时。

 

网上看了很久,大概觉得这题单调队列优化是这样的:

现在枚举到第i种硬币,面值bi,个数ci。回顾上面的状态转移方程,发现f[j]和f[j-bi*k]的关联不大,这里就把所有要凑的面值x按照对bi取模的结果分租。

假设取模余d。那么bi*0+d,bi*1+d,bi*2+d....bi*j+d,都是一组的。假设x=bi*j+d,那么满足下面的关系:

f[x] = f[bi*j+d] = min{ f[bi*k+d] + (j-k) }。 其中,j-k<=ci。

也许细心的大佬会觉得应该是 j<=ci,否则万一k和j都取到了,可能会大于c[i]。确实,在上面的代码里,j倒着枚举就是考虑到了这种情况(正着枚举是数量无限的情况)。不过下面在进队时的细节可以避免它。

转化一下: f[bi*j+d] = min{ f[bi*k+d] - k } + j  (j-k<=ci) 。

如果把 bi*0+d~bi*k+d 都入队,那么就转变成了在一个队列里求最小值,可以用单调队列解决。

入队的细节:上面的代码倒着枚举是因为要用到未更新时的数据。类似地,我们也在更新前把f[bi*j+d]入队,再对它更新。

至于记录完全是玄学了,确实不会。不介意可以拿这个混过去。

 

#include<bits/stdc++.h>
using namespace std;
const int MAXN=205;
const int MAXM=20005;
int N,M,b[MAXN],c[MAXN],f[MAXM],ans[MAXN];
struct data{int k,num;};
data q[MAXM],record[MAXM][2];

char C;
void scan(int &x)
{
	for(C=getchar();C<'0'||C>'9';C=getchar());
	for(x=0;C>='0'&&C<='9';C=getchar()) x=x*10+C-'0';
}

char Num[10]; int cnt;
void print(int x)
{
	cnt=0;
	if(!x) Num[cnt++]='0';
	while(x) Num[cnt++]=x%10+'0',x/=10;
	while(cnt--) putchar(Num[cnt]);
	putchar(' ');
}

void out(int x)
{
	if(!x) return;
	data t0=record[x][0],t1=record[x][1];
	if(!t1.num)
	{
		ans[t0.num]+=t0.k;
		out(x-b[t0.num]*t0.k);
	}
	else
	{
		if(ans[t1.num]+t1.k>c[t1.num])
		{
			ans[t0.num]+=t0.k;
			out(x-b[t0.num]*t0.k);
		}
		else
		{
			ans[t1.num]+=t1.k;
			out(x-b[t1.num]*t1.k);
		}
	}
}

int main()
{
	int i,j,d,x,L,R;
	
	scan(N);
	for(i=1;i<=N;i++) scan(b[i]);
	for(i=1;i<=N;i++) scan(c[i]);
	scan(M);
	
	memset(f,0x3f,sizeof(f));
	f[0]=0;
	for(i=1; i<=N; i++)  //枚举第i种硬币 
	for(d=0; d<b[i]; d++)  //枚举除以该硬币面额的余数 
	{
		L=1; R=0;  //维护一个队首元素(num) 最小的队列 
		for(j=0;;j++)  // x=b[i]*j + d ,枚举j 
		{
			x= j*b[i]+d;  //此时的总金额 
			if(x>M) break;  
			
			while( L<=R && j-q[L].k>c[i] ) L++;  //判断是否超过c[i] 
			while( L<=R && f[x]-j<=q[R].num ) R--;  //判断是否满足单调 
			q[++R]=( (data) {j,f[x]-j} );  //更新前入队
			if(q[L].num+j<f[x])
			{
				f[x]=q[L].num+j;
				 
				if(!record[x][0].num) record[x][0]=((data){j-q[L].k,i});
				else record[x][1]=((data){j-q[L].k,i});
			}
		}
	}
	print(f[M]); putchar('\n');
	out(M);
	for(i=1;i<=N;i++) print(ans[i]);
	return 0;
}

 

展开阅读全文

没有更多推荐了,返回首页