【题解】P3092 [USACO13NOV]No Change G

【题解】P3092 [USACO13NOV]No Change G

题目传送门

题目要求

首先看到题目要求的是将N个物品依次买下,最多能剩下多少钱,若无法买下,输出-1

题目分析

因为N个物品是依次买下,所以容易想到像背包那样把“考虑到第i个物品”作为阶段
又因为硬币的数量范围很小,再用一个二进制数表示硬币的使用情况作为状态

即f[i][s]

但我们发现有很多问题:

一是dp要存什么:
如果按照题目要求存最多剩余钱数的话,我们发现,实际上没必要,因为可以通过状态来计算
那要是用01表示能否用这些硬币买下前i个物品的话,那似乎又有些浪费

二是复杂度不对:
空间复杂度是N*2^K,而且我们无法用滚动数组优化,因为我们转移时,需要用到不仅(i-1)的状态
时间复杂度就更离谱,还要乘个二分转移的复杂度logN

所以我们考虑重新设计状态

回想最初设计的两个维度,

  1. 第i个物品
  2. 硬币的使用状况s

首先s这个维度是必需的,否则无法转移

那么第一个维度呢?

我们去掉它试试

即仅用一个s表示状态,那么记录什么呢?

我们刚才去掉了第一个维度,便不知道当前买到第几个物品了

所以正好可以用f[s]来记录硬币使用状况为s时最多能买到第几个物品

那能转移吗?

枚举最后使用的是哪一枚硬币,易得方程式

\[f[s]=f[s\oplus(1<<j)]+num[f[s\oplus(1<<j)],j],其中0<=j<k \]

num[i,j]表示从第i个物品开始,硬币j最多能买多少个物品,可以O(NKlogN)预处理出来
转移复杂度为k,总复杂度为O(NKlogN+2^K*K)

总结

  • 在dp的状态设计中,若发现复杂度太大,且难以优化,可考虑是否哪一维的状态可省略,必要时,可将这一维状态变成我们dp数组要记录的东西

ps.本蒟蒻第一次完全自己做出的状态压缩动态规划,本篇blog也是完全按照自己的思维顺序写下的,在此记录一下

Code

#include<bits/stdc++.h>
using namespace std;

inline int read()
{
	register int x=0,w=1;
	register char ch=getchar();
	while((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
	if(ch=='-') {ch=getchar();w=-1;}
	while(ch>='0'&&ch<='9') {x=(x<<3)+(x<<1)+(ch^48);ch=getchar();	}
	return x*w;
}
const int M=1e5+10;
int n,k,c[17],sum[M],num[M][17],f[1<<16];
int main()
{
    k=read();n=read();
    for(int i=1;i<=k;++i) c[i]=read();
    int x;
    for(int i=1;i<=n;++i){
    	x=read();
    	sum[i]=sum[i-1]+x;
	}
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<=k;++j)
		{
			int l=0,r=n-i+2,mid;
			while(l+1<r)
			{
				mid=l+r>>1;
				if(sum[i+mid-1]-sum[i-1]<=c[j]) l=mid;
				else r=mid;
			}
			num[i][j]=l;
		}
	}
	memset(f,0xcf,sizeof(f));
	f[0]=0;
	for(int i=1;i<(1<<k);++i)
	{
		for(int j=0;j<k;++j)
		{
			if(i>>j&1) 
			f[i]=max(f[i],f[i^(1<<j)]+num[f[i^(1<<j)]+1][j+1]); 
		}
	}
	int ans=-1;
	for(int i=1;i<(1<<k);++i)
	{
		if(f[i]==n){
			int res=0;
			for(int j=0;j<k;++j){
				if(!(i>>j&1)) res+=c[j+1];
			}
			ans=max(ans,res);
		} 
	}
	cout<<ans;
	return 0;
}
/*
1 3
5
1 2 2
*/
/*
2 4
3 4
2 2 1 2
*/
posted @ 2021-07-14 20:14  glq_C  阅读(67)  评论(0)    收藏  举报