【[USACO13NOV]沒有找零No Change】
阿新 • • 發佈:2019-01-02
其實我是點單調佇列的標籤進來的,之後看著題就懵逼了
於是就去題解裡一翻,發現樓上樓下的題解說的都好有道理,
f[j]表示一個再使用一個硬幣就能到達i的某個之前狀態,b[now]表示使用那個能使狀態j變到i的硬幣的面值,find表示這些花費可以到達的最大距離,由於字首和保持單調可以用二分求解,方程不就是f[i]=max(f[i],find(p[f[j]]+b[now]))嗎
但這道題怎麼用單調佇列優化呢
我們觀察這個方程你會發現無論是b[now],p[f[j]]都跟i沒有什麼關係,而只要是p[f[j]]+b[now]越大,相應的find的值也就越大
於是我們就可以愉快的單調佇列優化這個dp了,用一個單調佇列把每次的p[f[j]]+b[now]存起來,每次有新元素入隊時維護佇列的單調性,之後當所有元素入隊完,直接用隊首的最大值進行一遍find就好了,這樣就可以避免進行多次find了
儘管單調佇列是用常數奇大的deque實現的,但開了O2能跑到120 ms,輕鬆卡到最優解第一
#include<iostream> #include<queue> #include<cstring> #include<cstdio> #define re register #define int long long #define maxn 100001 using namespace std; int f[65540],a[maxn],b[17],n,m,num,p[maxn]; int c[17]; inline void check(int x) { memset(c,0,sizeof(c)); int pp=m; while(x) { if(x&1) c[pp]=1; pp--; x>>=1; } } inline int read() { char c=getchar(); int x=0; while(c<'0'||c>'9') c=getchar(); while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-48,c=getchar(); return x; } inline int find(int x) { int l=1,r=n,tot=0; while(l<=r) { int mid=l+r>>1; if(p[mid]<=x) l=mid+1,tot=mid; else r=mid-1; } return tot; } signed main() { m=read(); n=read(); for(re int i=1;i<=m;i++) b[i]=read(),num+=b[i]; for(re int i=1;i<=n;i++) a[i]=read(); p[1]=a[1]; for(re int i=2;i<=n;i++) p[i]=p[i-1]+a[i]; for(re int i=0;i<=(1<<m)-1;i++) { deque<int> q; for(re int j=1;j<=m;j++) { if(!(i&(1<<(m-j)))) continue;//這裡跟樓上樓下幾篇題解不太一樣,這個狀態轉成二進位制後左邊第一位表示的是第一個硬幣是否被選擇 int k=i^(1<<(m-j)); while(q.size()&&q.back()<p[f[k]]+b[j]) q.pop_back();//跟隊尾元素比較,如果比隊尾大就彈出隊尾,維護單調佇列單調性 q.push_back(p[f[k]]+b[j]);//入隊 } //其實這裡不用單調佇列優化也是可以的,我們只需要儲存一下最大值就好了,這樣應該還能快一些,但是用單調佇列優化dp的這種思路還是比較重要的 f[i]=find(q.front());//只用一遍find就好了 } int ans=-1; for(re int i=0;i<=(1<<m)-1;i++) { if(f[i]!=n) continue;//當前狀態根本到不了n,就直接下一個 check(i);//將當前的狀態轉成二進位制數 int tot=0; for(re int j=1;j<=m;j++) if(c[j]==0) tot+=b[j]; //如果沒有被選擇,那麼就把它加上 if(tot>ans) ans=tot; } cout<<ans; return 0; }