dp 单调性优化总结
对于单调性优化其实更多的是观察dp的状态转移式子的单调性 进而用优先队列 单调队列 二分查找什么的找到最优决策 使时间更优。
对于这道题就是单调性优化的很好的例子
首先打一个暴力再说。
f[i][j]表示前i个木匠刷前j个木板所得到的最大价值
f[i][j]=max(f[i][j],f[i-1][j]);
f[i][j]=max(f[i][j],f[i][j-1])
f[i][j]=max(f[i][j],f[i-1][k]+(j-k)*r(j));(k<t(j)<=j)&&(j-k<=l(j));
这样的话枚举 i j k 时间复杂度 n^2m;
//#include<bits/stdc++.h> #include<iostream> #include<iomanip> #include<ctime> #include<cstdio> #include<cstring> #include<string> #include<cmath> #include<cstdlib> #include<cctype> #include<utility> #include<queue> #include<deque> #include<vector> #include<stack> #include<algorithm> #include<set> #include<bitset> #include<map> #define INF 2147483646 #define ll long long #define ldb long double #define t(x) t[x].target #define r(x) t[x].receive #define l(x) t[x].l using namespace std; char buf[1<<15],*ft,*fs; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline int read() { int x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(ll x) { x<0?x=-x,putchar('-'):0; int num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar(10);return; } const int MAXN=16002,maxn=102; struct wy { int l,receive,target; friend int operator <(const wy x,wy y) { return x.target<y.target; } }t[MAXN]; int n,m; int f[maxn][MAXN];//f[i][j]表示前i个木匠刷前j个木板所得到的最大价值 //f[i][j]=max(f[i][j],f[i-1][j]); //f[i][j]=max(f[i][j],f[i][j-1]); //f[i][j]=max(f[i][j],f[i-1][k]+(j-k)*r(j));(k<t(j)<=j)&&(j-k<=l(j)); int main() { //freopen("1.in","r",stdin); n=read();m=read(); for(int i=1;i<=m;i++) { l(i)=read(); r(i)=read(); t(i)=read(); } sort(t+1,t+1+m); //for(int i=1;i<=m;i++)cout<<l(i)<<' '<<r(i)<<' '<<t(i)<<endl; for(int i=1;i<=m;i++) { for(int j=1;j<=n;j++) { f[i][j]=max(f[i][j],f[i-1][j]); f[i][j]=max(f[i][j],f[i][j-1]); if(j>=t(i)&&j-l(i)<t(i)) for(int k=max(0,j-l(i));k<t(i);k++) f[i][j]=max(f[i][j],f[i-1][k]+(j-k)*r(i)); } } put(f[m][n]); return 0; }
而 n<=16000 m 100 这就直接T飞了考虑将其优化。
f[i][j]=max{f[i-1][k]-k*r(i)}+j*r(i);
so easy 单调队列优化一下结束 保存决策即可
至于一些细节需要注意 不过单调队列这样优化也算是很常见的吧。
//#include<bits/stdc++.h> #include<iostream> #include<iomanip> #include<ctime> #include<cstdio> #include<cstring> #include<string> #include<cmath> #include<cstdlib> #include<cctype> #include<utility> #include<queue> #include<deque> #include<vector> #include<stack> #include<algorithm> #include<set> #include<bitset> #include<map> #define INF 2147483646 #define ll long long #define ldb long double #define t(x) t[x].target #define r(x) t[x].receive #define l(x) t[x].l using namespace std; char buf[1<<15],*ft,*fs; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline int read() { int x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(ll x) { x<0?x=-x,putchar('-'):0; int num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar(10);return; } //f[i][j]表示前i个木匠刷前j个木板所得到的最大价值 //f[i][j]=max(f[i][j],f[i-1][j]); //f[i][j]=max(f[i][j],f[i][j-1]); //f[i][j]=max(f[i][j],f[i-1][k]+(j-k)*r(i));(k<t(j)<=j)&&(j-k<=l(j)); //考虑将其优化 f[i][j]=max{f[i-1][k]+j*r(i)-k*r(i)}; //f[i][j]=max{f[i-1][k]-k*r(i)}+j*r(i); //so easy 单调队列优化一下结束 保存决策即可 const int MAXN=16002,maxn=102; struct wy { int l,receive,target; friend int operator <(const wy x,wy y) { return x.target<y.target; } }t[MAXN]; int n,m; int f[maxn][MAXN]; int q[MAXN],l,r; int kk(int v,int x){return f[v-1][x]-x*r(v);} int main() { //freopen("1.in","r",stdin); n=read();m=read(); for(int i=1;i<=m;i++) { l(i)=read(); r(i)=read(); t(i)=read(); } sort(t+1,t+1+m); //for(int i=1;i<=m;i++)cout<<l(i)<<' '<<r(i)<<' '<<t(i)<<endl; for(int i=1;i<=m;i++) { l=r=0;l=1;if(t(i)-l(i)<=0)q[++r]=0; for(int j=1;j<=n;j++) { f[i][j]=max(f[i-1][j],f[i][j-1]); if(j>=t(i)) { while(l<=r&&q[l]<(j-l(i)))l++; if(l<=r)f[i][j]=max(f[i][j],kk(i,q[l])+j*r(i)); } if(j>=t(i)-l(i)&&j<t(i)) { while(l<=r&&kk(i,q[r])<=kk(i,j))--r; q[++r]=j; } } } put(f[m][n]); return 0; }
这道题呢 看完题目就觉得很难我甚至觉得dp不太好写
硬写吧 f[i]表示前i个数字所取得的最小代价和 f[i]=min(f[i],f[j]+a[k]);//k j+1~i
直接转移 复杂度 n^3 因为 要求 a[k] j+1~i 之中的最大值
//#include<bits/stdc++.h> #include<iostream> #include<iomanip> #include<ctime> #include<cstdio> #include<cstring> #include<string> #include<cmath> #include<cstdlib> #include<cctype> #include<utility> #include<queue> #include<deque> #include<vector> #include<stack> #include<algorithm> #include<set> #include<bitset> #include<map> #define INF 2147483646 #define ll long long #define ldb long double using namespace std; char buf[1<<15],*ft,*fs; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline int read() { int x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(ll x) { x<0?x=-x,putchar('-'):0; int num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar(10);return; } //f[i]表示前i个数字所取得的最小代价和 //f[i]=min(f[i],f[j]+a[k]);//k j+1~i const int MAXN=100002; int n,m,flag; int a[MAXN],s[MAXN]; ll f[MAXN]; int q[MAXN],l,h; int main() { //freopen("1.in","r",stdin); n=read();m=read(); for(int i=1;i<=n;i++) { a[i]=read(); s[i]=a[i]+s[i-1]; if(a[i]>m)flag=1; } if(flag==1){put(-1);return 0;} for(int i=1;i<=n;i++)f[i]=INF*1000000ll; for(int i=1;i<=n;i++) { for(int j=0;j<i;j++) { int maxx=0; for(int k=j+1;k<=i;k++)maxx=max(maxx,a[k]); if(s[i]-s[j]<=m)f[i]=min(f[i],f[j]+maxx); } } put(f[n]); return 0; }
观察一下式子求最大值我完全可以采用ST表的算法预处理 然后 最终复杂度是n^2 因为要枚举j这个决策点
但是既然是n^2的复杂度我完全可以 采用单调队列来寻找最大值吧 复杂度n^2 还是过不了TAT
//#include<bits/stdc++.h> #include<iostream> #include<iomanip> #include<ctime> #include<cstdio> #include<cstring> #include<string> #include<cmath> #include<cstdlib> #include<cctype> #include<utility> #include<queue> #include<deque> #include<vector> #include<stack> #include<algorithm> #include<set> #include<bitset> #include<map> #define INF 2147483646 #define ll long long #define ldb long double using namespace std; char buf[1<<15],*ft,*fs; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline ll read() { ll x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(ll x) { x<0?x=-x,putchar('-'):0; ll num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar(10);return; } //f[i]表示前i个数字所取得的最小代价和 //f[i]=min(f[i],f[j]+a[k]);//k j+1~i const ll MAXN=100002; ll n,m,flag; ll a[MAXN],s[MAXN]; ll f[MAXN]; ll q[MAXN],l,r; int main() { freopen("1.in","r",stdin); n=read();m=read(); for(ll i=1;i<=n;i++) { a[i]=read(); s[i]=a[i]+s[i-1]; if(a[i]>m)flag=1; } if(flag==1){put(-1);return 0;} for(ll i=1;i<=n;i++)f[i]=INF*1000000ll; for(ll i=1;i<=n;i++) { l=r=0;l=1; for(ll k=1;k<=i;k++) { while(l<=r&&a[k]>=a[q[r]])r--; q[++r]=k; } for(ll j=0;j<i;j++) { while(q[l]<=j)l++; if(s[i]-s[j]<=m)f[i]=min(f[i],f[j]+a[q[l]]); } } put(f[n]); return 0; }
然 考虑终极优化 争取优化到O(n) 或者是 nlogn 不会啊 !
书上是这样说的 对于j(0<=j<i)可能成为最优决策除了需要满足 a[i]-a[j]<=M 还需要满足以下两个条件之一:
1 a[j]=max(a[k])(j<=k<=i) 2 j~i C[i]-C[j]<=M 的最小j
如果两个条件都不满足的话 那么 aj<max(ak) j<=k<=i && j~i C[i]-C[k]<=M
(j<=k<=i)max(a[k])==(j+1<=k<=i)max(a[k]) f[i-1]<=f[j] f[j-1]+max(ak j~i )<=f[i]+max(ak j+1~i)
那么此时 j-1 比 j 更有可能成为最优决策 此时j不可能成为最优决策
所以我们可以维护一个 最早的j 剩下的j 就只能是 单调递增 aj的值单调递减 的东西了 这仅仅是我们从上述两个结论中得来的结论。
那么 这也仅仅是单调性的优化 也就是说一些无用决策将被我们删掉 最后我们只需要 比较这么多单调递减的f[j]+a[k](j+1~i)哪个最小即可
根据以上原理 我们可以筛选出很多不必要的决策 将这些决策舍弃
然后维护单调队列即可 但是 针对状态转移时 针对队列中的每个f值 我们都不知道哪个是最优的所以需要比较。
//#include<bits/stdc++.h> #include<iostream> #include<iomanip> #include<ctime> #include<cstdio> #include<cstring> #include<string> #include<cmath> #include<cstdlib> #include<cctype> #include<utility> #include<queue> #include<deque> #include<vector> #include<stack> #include<algorithm> #include<set> #include<bitset> #include<map> #define INF 2147483646 #define ll long long #define ldb long double #define id(x) q[x].id #define v(x) q[x].v using namespace std; char buf[1<<15],*ft,*fs; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline ll read() { ll x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(ll x) { x<0?x=-x,putchar('-'):0; ll num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar(10);return; } //f[i]表示前i个数字所取得的最小代价和 //f[i]=min(f[i],f[j]+a[k]);//k j+1~i const ll MAXN=100002; ll n,m,flag; ll a[MAXN]; ll f[MAXN]; struct wy { ll id,v; }q[MAXN]; ll sum,t,l,r; int main() { freopen("1.in","r",stdin); n=read();m=read(); for(ll i=1;i<=n;i++) { a[i]=read(); if(a[i]>m)flag=1; } if(flag==1){put(-1);return 0;} for(ll i=1;i<=n;i++) { sum+=a[i]; while(sum>m)sum-=a[t++]; while(l<=r&&id(l)<t)l++; while(l<=r&&a[i]>=v(r))r--; id(++r)=i; v(r)=a[i]; f[i]=f[t-1]+v(l); for(ll j=l;j<r;j++)f[i]=min(f[i],f[id(j)]+v(j+1)); } //for(long long i=1;i<=n;i++)cout<<f[i]<<' '; put(f[n]); return 0; }
这样的话可以直接比较 队列中的某个值 进行状态转移这道题真的很有意思 我想了1天呢。
然后貌似这样的算法是 n^2的 (玄学)比n^2小的多的多 所以可以A掉这道题但是...
为了追求更快的AC我们可以采用set维护一下单调队列中的最小值实现复杂度nlogn
但是这个竟然没 上面那个貌似是n^2的算法要慢的多核心思想找到 最优决策。
//#include<bits/stdc++.h> #include<iostream> #include<iomanip> #include<ctime> #include<cstdio> #include<cstring> #include<string> #include<cmath> #include<cstdlib> #include<cctype> #include<utility> #include<queue> #include<deque> #include<vector> #include<stack> #include<algorithm> #include<set> #include<bitset> #include<map> #define INF 2147483646 #define ll long long #define ldb long double using namespace std; char buf[1<<15],*ft,*fs; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline ll read() { ll x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(ll x) { x<0?x=-x,putchar('-'):0; ll num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar(10);return; } //f[i]表示前i个数字所取得的最小代价和 //f[i]=min(f[i],f[j]+a[k]);//k j+1~i const ll MAXN=100002; ll n,m,flag; ll a[MAXN]; ll f[MAXN]; ll q[MAXN],l,r,sum,t; multiset<ll>s; multiset<ll>::iterator it; int main() { //freopen("1.in","r",stdin); n=read();m=read(); for(ll i=1;i<=n;i++) { a[i]=read(); if(a[i]>m)flag=1; } if(flag==1){put(-1);return 0;} for(ll i=1;i<=n;i++) { sum+=a[i]; while(sum>m)sum-=a[t++]; while(l<=r&&q[l]<t) { if(s.empty()==1){l++;continue;} if((it=s.find(f[q[l]]+a[q[l+1]]))!=s.end());s.erase(it); l++; } while(l<=r&&a[i]>=a[q[r]]) { if(s.empty()==1){r--;continue;} if((it=s.find(f[q[r-1]]+a[q[r]]))!=s.end())s.erase(it); r--; } if(l<=r)s.insert(f[q[r]]+a[i]); q[++r]=i; f[i]=f[t-1]+a[q[l]]; if(s.begin()!=s.end())f[i]=min(f[i],*s.begin()); } //for(ll i=1;i<=n;i++)cout<<f[i]<<' '<<endl; put(f[n]); return 0; }
终于A掉了这道题好题啊 ,这个单调性优化关键是发觉上面的两个引理 也就是单调性的特点 进而优化可选择的决策空间 进而缩短时间。
下面是多重背包的单调性优化 :
首先是 朴素的多重背包
完全当成01背包来写 这个就比较简单了。当时我以为这个复杂度不高的,寒假郑州的集训一道题两次多重背包,
没想到我T飞了。。。
//#include<bits/stdc++.h> #include<iostream> #include<iomanip> #include<ctime> #include<cstdio> #include<cstring> #include<string> #include<cmath> #include<cstdlib> #include<cctype> #include<utility> #include<queue> #include<deque> #include<vector> #include<stack> #include<algorithm> #include<set> #include<bitset> #include<map> #define INF 2147483646 #define ll long long #define ldb long double using namespace std; char buf[1<<15],*ft,*fs; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline ll read() { ll x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(ll x) { x<0?x=-x,putchar('-'):0; ll num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar(10);return; } //f[i]表示前i的空间能拿到的最大价值 const int MAXN=5002; int n,m; int w[MAXN],sum[MAXN],v[MAXN]; int f[MAXN]; int main() { //freopen("1.in","r",stdin); n=read();m=read(); for(int i=1;i<=n;i++) { sum[i]=read(); w[i]=read(); v[i]=read(); } for(int i=1;i<=n;i++) { for(int k=1;k<=sum[i];k++) { for(int j=m;j>=w[i];j--) f[j]=max(f[j],f[j-w[i]]+v[i]); } } put(f[m]); return 0; }
当然 有更优秀的解法 2进制拆分 主要原理是 2^0+2^1+...+2^p+R(剩下来的余数)当然其中p是能达到的最大的p
这一堆数字能拼成任意数字 对其在做01背包的情况下 就能快速得到最优解。
证明: 2^0+2^1+...+2^p 能拼成 任意 m-R之中的任意数字 毋庸置疑想想二进制的表示法。2^0+2^1+...2^p==m-R
而对于 R 因为R<2^p+1 且2^0+2^1+..2^p==2p+1 所以R<=2^0+2^1+...2^p
所以 这堆数字又可以拼成 1~R任意的数字 所以 综上 它是可以拼出1~m之中的任意数字 对每个数字都做一次01背包即可得到最优解
因为是最优解一定会是最优解(犹如是金子总会发光发热 神说要有光嘛)
//#include<bits/stdc++.h> #include<iostream> #include<iomanip> #include<ctime> #include<cstdio> #include<cstring> #include<string> #include<cmath> #include<cstdlib> #include<cctype> #include<utility> #include<queue> #include<deque> #include<vector> #include<stack> #include<algorithm> #include<set> #include<bitset> #include<map> #define INF 2147483646 #define ll long long #define ldb long double using namespace std; char buf[1<<15],*ft,*fs; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline ll read() { ll x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(ll x) { x<0?x=-x,putchar('-'):0; ll num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar(10);return; } //f[i]表示前i的空间能拿到的最大价值 const int MAXN=5002; int n,m; int w[MAXN],sum[MAXN],v[MAXN]; int f[MAXN]; int main() { //freopen("1.in","r",stdin); n=read();m=read(); for(int i=1;i<=n;i++) { sum[i]=read(); w[i]=read(); v[i]=read(); } for(int i=1;i<=n;i++) { if(sum[i]*w[i]>=m) for(int j=w[i];j<=m;j++) f[j]=max(f[j],f[j-w[i]]+v[i]); else { int s=1,cnt=sum[i]; while(s<=cnt) { for(int j=m;j>=s*w[i];j--) f[j]=max(f[j],f[j-s*w[i]]+s*v[i]); cnt-=s;s=s<<1; } for(int j=m;j>=cnt*w[i];j--) f[j]=max(f[j],f[j-cnt*w[i]]+cnt*v[i]); } } put(f[m]); return 0; }
下面是一个单调队列优化的思路 要实现这个我们需把状态转移方程变一变。
f[j]表示前j空间能装的最大价值 f[j]=max{f[j-k*w[i]]+k*v[i]};
由于每次转移 只有倍数之间才会互相转移所以可以写成这样
f[u+p*w[i]]=max{f[u+k*w[i]]+(p-k)*v[i]};
这样使其具有单调性 然后利用单调队列维护最优决策即可 。
//#include<bits/stdc++.h> #include<iostream> #include<iomanip> #include<ctime> #include<cstdio> #include<cstring> #include<string> #include<cmath> #include<cstdlib> #include<cctype> #include<utility> #include<queue> #include<deque> #include<vector> #include<stack> #include<algorithm> #include<set> #include<bitset> #include<map> #define INF 2147483646 #define ll long long #define ldb long double using namespace std; char buf[1<<15],*ft,*fs; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline ll read() { ll x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(ll x) { x<0?x=-x,putchar('-'):0; ll num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar(10);return; } //f[i]表示前i的空间能拿到的最大价值 const int MAXN=5002; int n,m; int w[MAXN],sum[MAXN],v[MAXN]; int f[MAXN]; int k(int i,int u,int p){return f[u+p*w[i]]-p*v[i];} int q[MAXN],l,r; int main() { freopen("1.in","r",stdin); n=read();m=read(); for(int i=1;i<=n;i++) { sum[i]=read(); w[i]=read(); v[i]=read(); } for(int i=1;i<=n;i++) { for(int u=0;u<w[i];u++) { l=1;r=0; int p=(m-u)/w[i]; for(int j=p-1;j>=max(p-sum[i],0);j--) { while(l<=r&&k(i,u,q[r])<=k(i,u,j))r--; q[++r]=j; } for(int j=p;j>=1;j--) { while(l<=r&&q[l]>j-1)l++; f[u+j*w[i]]=max(f[u+j*w[i]],k(i,u,q[l])+j*v[i]); if(j-sum[i]-1>=0) { while(l<=r&&k(i,u,q[r])<=k(i,u,j-sum[i]-1))r--; q[++r]=j-sum[i]-1; } } } } put(f[m]); return 0; }
多看 多想 多实践!