【题解】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
所以我们考虑重新设计状态
回想最初设计的两个维度,
- 第i个物品
- 硬币的使用状况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
*/

浙公网安备 33010602011771号