P3092 [USACO13NOV] No Change G 题解
题解
思路
看到 \(1\le k\le16\),我们想到状压DP。
以每枚硬币是否被使用为状态,对其进行枚举。
令 \(dp_i\) 表示状态 \(i\) 下最多能支付到第 \(dp_i\) 件商品,令 \(f_{i,j}\) 表示从第 \(i+1\) 个位置开始,第 \(j\) 枚硬币可以支付 \((i+1,f_{i,j}]\) 这一区间的商品。
于是可以知道:每个状态下最多能支付到的商品位置,是从它的前继状态最多能支付到的那件商品的下一件商品开始,使用新增加的那枚硬币,所能支付到的最后一件商品的位置。
结合转移方程理解这句话:
\[\Large dp_i=\max(f_{dp_{i\oplus2^j},j},\ i\&2^j=0)
\]
写成代码形式:
if(i&(1<<j)==0) dp[i]=max(dp[i],f[dp[i^(1<<j)][j];
像这样。答案就是所有 \(dp_i\ge N\) 的状态中剩余钱数的最小值。
时间复杂度 \(O(NK\log N+2^KK)\),可以解决。
实现
首先需要预处理 \(f\) 数组。笔者使用二分,更优的做法是双指针,留给读者自行思考(绝对不是因为我不会)。
在预处理过程中需要频繁查询区间和,因此可以预处理 \(c\) 数组的前缀和。
代码
#include <cstdio> #define N 100005 int max(int x,int y) {return x>y?x:y;} int k,n; int a[20],c[N]; int f[N][20]; //(i,f[i][k]] int dp[N]; int cnt(int x) //没用的硬币求和 { int ret=0; for(int i=0;i<k;i++) if(!(x&(1<<i))) ret+=a[i]; return ret; } int main() { scanf("%d%d",&k,&n); for(int i=0;i<k;i++) scanf("%d",&a[i]); for(int i=1;i<=n;i++) scanf("%d",&c[i]),c[i]+=c[i-1]; for(int i=0;i<=n;i++) for(int j=0;j<k;j++) { int lb=i,rb=n; while(lb<rb) { int mid=lb+rb+1>>1; if(c[mid]-c[i]<=a[j]) lb=mid; else rb=mid-1; } f[i][j]=lb; } int maxc=-1; for(int i=0;i<(1<<k);i++) { for(int j=0;j<k;j++) if(i&(1<<j)) dp[i]=max(dp[i],f[dp[i^(1<<j)]][j]); if(dp[i]==n) maxc=max(maxc,cnt(i)); } printf("%d",maxc); }
\[\Huge End
\]
分类:
题解
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】