题解 luogu.P3092 [USACO13NOV] No Change G
题目
luogu.P3092 [USACO13NOV] No Change G
这是一道好题。而且综合性较强,我并没有想出正解,在我第一遍做的时候。事后总结复盘,感觉是个很有难度的题。
状态非常不好定义。
题意建模
首先拿到题,我的思路慢慢发散:
- 感觉就连阶段都不好划分。
- 状态也不好定义。
难点就是,我知道要状压,而且是硬币的使用状态。但是后面怎么办?再怎么处理阶段的划分?因为很显然会发现,题目中有说:
FJ 可以随时停下来付款,每次付款只用一个硬币,支付购买的内容是从上一次支付后开始到现在的这些所有物品(前提是该硬币足以支付这些物品的费用)。不幸的是,商场的收银机坏了,如果FJ支付的硬币面值大于所需的费用,他不会得到任何找零。
这就是说,要考虑的情况就会更加复杂。很容易构造出一种无解的状态:最大的金额买不了最大价值的东西。然而如果能买,什么时候去使用最大面额的硬币?后面的状态怎么扩展?。。。。。。等等等,诸如此类乱七八糟的想法就会泉涌而出。免不了思绪混乱。这个时候,正解的巧妙真是令我拍案而起。。。
具体介绍一下混乱的想法。
- 怎么转移状态?
- 即使我定义出来了,也不好用二进制按位查找。这也就是说,阶段的划分不明确,而且缺乏手段。
算法分析
定义状态
考虑到上一个硬币的状态未知,但是需要,所以:
数组f[i]表示用 \(i\) 状态下的硬币可以购买到第几个商品 ,dp[i]表示状态 \(i\) 下的花费
划分阶段
既然状态成功定义,那就是以硬币的使用为阶段。
考虑转移
使用当前硬币的状态一定由使用上一个硬币的状态转移而来
-
外层循环枚举所有状态,内层循环枚举每一位,若当前状态i的第j位为1,则可以进行转移
-
然后可以进行枚举n件物品,考虑每一件是否可以购买,一直到不能购买为止
-
因为一种状态可以被更新多次,所以要取max,保证
dp数组存的是能买到的最大编号,然后更新dp数组和f数组 -
如果到第 \(n\) 件都可以买,则可以购买全部物品,
ans记录当前的最小花费,最后用所有硬币的总面值减去最小花费即为答案
如果ans没有被更新过,说明不能购买,输出−1
时间复杂度 \(O(2^{k}kn)\),TLE。考虑优化。
考虑优化的过程就是考虑哪些地方的时空不平衡,具体来说,就是哪些地方可以空间换时间。
- 发现每次枚举物品统计价值来检查是否能够购买是冗余操作,可以用前缀和预处理一下,然后每次检查的时候进行一次二分就可以了
时间复杂度 \(O(2^{k}klogn)\),可通过本题。
参考代码
#include<iostream>
using namespace std;
const int N=1e5+5,M=18;
int dp[1<<M];
int f[1<<M],s[N],c[N],w[N],m,n,ans,tot;
inline int check(int x, int cha)
{
int l=cha,r=n,mid;
while(l<=r) //终止条件:l>r
{
mid=(l+r)>>1;
if(s[mid]-s[cha-1]==x) return mid;
if(s[mid]-s[cha-1]<x) l=mid+1;
else r=mid-1;
}
return r;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>m>>n;
int all=(1<<m)-1;
for(int i=1;i<=m;i++)
{
cin>>w[i];
tot+=w[i];
}
for(int i=1;i<=n;i++)
{
cin>>c[i];
s[i]=s[i-1]+c[i];
}
ans=0x3f3f3f3f;
for(int i=1;i<=all;i++)
for(int j=1;j<=m;j++)
if(i&1<<j-1)
{
int x=i^(1<<j-1),sum;
if((sum=check(w[j],f[x]+1))>f[i])
f[i]=sum,dp[i]=dp[x]+w[j];
if(f[i]==n) ans=min(ans,dp[i]);
}
cout<<(tot-ans<0?-1:tot-ans)<<endl;
return 0;
}
细节实现
- 一个就是位运算的细节,不要出错。位运算要从 \(1_{(10)}\) 也就是最低位\(000...01_{(2)}\) 第0位开始检查起,而数组的索引却是从1开始,这需要格外注意;
- 还有就是这个二分最终的终止条件,注释在代码中了。最终 \(l\),\(r\) 是什么位置呢?注意到,\(l>r\) 时停止,所以在这之前的临界状态,必定是 \(l+1=r\),也就是说,再执行一轮查找之后,应有:\(r=mid-1,l=mid+1\),也就是,\(r\) 会停在小于所查找对象最大的位置。
总结归纳
还是好难,叫我再做一遍也很难敲出正解一遍AC。继续努力吧。

浙公网安备 33010602011771号