对于一些具有决策单调性的dp题目,我们可以应用斜率优化将复杂度从O(n^2)降到O(n)。
bzoj1010 HNOI2008 玩具装箱toy
题目大意:对于一些一维长度的物品,我们可以将连续的i~j个物品放在一起,费用是(j-i+sigma lk(i<=k<=j)-L)^2,求n个物品最小费用。
思路:对于这类dp题目,我们可以通过进行一系列数学化简,找出其中的斜率,进而斜率优化。我们设sum[i]表示前i件物品的长度和,s[i]为sum[i]+i,dp[i]=dp[j]+(s[i]-s[j]-L)^2。我们取j<k,dp[j,i]-dp[k,i]>0,带入之前的式子,将L+1,将含i的移到右边,含j、k的移到另一边,就会得到((dp[k,i]+sk^2)-(dp[j,i]+sj^2))/(sk-sj)<2(si-L)①,我们设左边=w[k,j],显然当w[k,j]<右边的时候,k更优。对于j<k<i,w[i,k]<w[k,j]②时,k不可能是之后用到的最优解,所以可以舍弃。我们维护可能用到的最优解,不断的舍弃那些不好的解,就能将复杂度降至O(n)。对于这里涉及两个符号的问题,我们可以统一符号(如上两个式子)。
#include<iostream> #include<cstdio> #include<cstring> #define maxnode 50005 using namespace std; long long dp[maxnode]={0},sum[maxnode]={0},q[maxnode]={0},l; long long getup(int j,int k) { return dp[j]+(sum[j])*(sum[j])-(dp[k]+(sum[k])*(sum[k])); } long long getdown(int j,int k) { return (sum[j]-sum[k]); } long long getdp(int i,int j) { return dp[j]+(sum[i]-sum[j]-l)*(sum[i]-sum[j]-l); } double g(int j,int k) { return getup(j,k)*1.0/(2.0*getdown(j,k)); } int main() { int n,i,j,head,tail; scanf("%d%lld",&n,&l); for (i=1;i<=n;++i) { scanf("%lld",&sum[i]); sum[i]+=sum[i-1]; } for (i=1;i<=n;++i) sum[i]+=(long long)i; head=tail=1;q[tail]=0;++l; for (i=1;i<=n;++i) { while(head<tail&& g(q[head+1],q[head]) < 1.0*(sum[i]-l)) ++head; dp[i]=getdp(i,q[head]); while(head<tail&& g(i,q[tail]) < g(q[tail],q[tail-1])) --tail; q[++tail]=i; } printf("%lld\n",dp[n]); }
bzoj1911 APIO2010特别行动队
题目大意:n名士兵,连续的一组士兵的战斗力是他们最初战斗力和x的一个二次函数,重新编队后求最大战斗力。
思路:同上,进行化简。显然dp[j,i]=dp[j]+a(sum[i]-sum[j])^2+b(sum[i]-sum[j])+c,取j<k,dp[j,i]-dp[k,i]<0,化简后有((dpk+asumk^2)-(dpj+asumj^2))/(sumk-sumj)>(2asumi+b)。然后根据符号统一原则,就可以得到g(q[head+1],q[head])>(2asumi+b),g(i,que[tail])>g(que[tail],que[tail-1])。
#include<iostream> #include<cstdio> #include<cstring> #define maxnode 1000005 using namespace std; long long dp[maxnode]={0},sum[maxnode]={0},q[maxnode]={0},a,b,c; long long getdp(int i,int j) { return dp[j]+a*(sum[i]-sum[j])*(sum[i]-sum[j])+b*(sum[i]-sum[j])+c; } long long getup(int j,int k) { return dp[j]+a*sum[j]*sum[j]-(dp[k]+a*sum[k]*sum[k]); } long long getdown(int j,int k) { return sum[j]-sum[k]; } double g(int j,int k) { return (getup(j,k)*1.0)/(getdown(j,k)); } int main() { int i,j,n,head,tail; scanf("%d%lld%lld%lld",&n,&a,&b,&c); sum[0]=0; for (i=1;i<=n;++i) { scanf("%lld",&sum[i]); sum[i]+=sum[i-1]; } head=tail=1;q[tail]=0; for (i=1;i<=n;++i) { while(head<tail&&g(q[head+1],q[head])>(sum[i]*2.0*a+b)) ++head; dp[i]=getdp(i,q[head]); while(head<tail&&g(i,q[tail])>g(q[tail],q[tail-1])) --tail; q[++tail]=i; } printf("%lld\n",dp[n]); }
bzoj1597 土地购买
题目大意:n块土地,购买其中i块土地的代价是max(xi)*max(yi),求买下n块土地的最小价值。
思路:首先,我们对于i、j,如果xi<xj,yi<yj,那么就可以忽略i。那么我们就可以对于x升序第一关键字y降序第二关键字排列,然后对于之前的i、j忽略不计。然后就是斜率优化dp了。dp[j,i]=dp[j]+yj+1*xi。取j<k,我们可以化出(dpk-dpj)/(yk+1-yj+1)>-xi(一定要认真一步步的化式子!!!),然后符号统一一下。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxnode 50005 using namespace std; struct use{ int xi,yi; }area[maxnode],a[maxnode]; long long q[maxnode]={0},dp[maxnode]={0}; bool visit[maxnode]={false}; int my_comp(const use x,const use y) { if (x.xi<y.xi) return 1; if (x.xi>y.xi) return 0; if (x.yi>y.yi) return 1; return 0; } long long getdp(int i,int j) { return dp[j]+(long long)a[j+1].yi*(long long)a[i].xi; } long long getup(int j,int k) { return dp[j]-dp[k]; } long long getdown(int j,int k) { return (long long)a[j+1].yi-(long long)a[k+1].yi; } double g(int j,int k) { return getup(j,k)*1.0/getdown(j,k); } int main() { int i,j,n,head,tail,tot=0; scanf("%d",&n); for (i=1;i<=n;++i) scanf("%d%d",&area[i].xi,&area[i].yi); sort(area+1,area+n+1,my_comp); for (i=1;i<=n;++i) { j=i-1; while(j&&area[j].yi<=area[i].yi) { visit[j]=true;--j; } } for (i=1;i<=n;++i) if (!visit[i]) a[++tot]=area[i]; head=tail=1;q[tail]=0; for (i=1;i<=tot;++i) { while(head<tail&&g(q[head+1],q[head])>=-a[i].xi) ++head; dp[i]=getdp(i,q[head]); while(head<tail&&g(i,q[tail])>=g(q[tail],q[tail-1])) --tail; q[++tail]=i; } printf("%lld\n",dp[tot]); }
bzoj3675 序列分割
题目大意:一个n个数的序列,分k次,每次从一个位置分开后,将两边的子段和的乘积加给ans,求ans的最大值。
思路:看到这道题目想做个四重的循环,后来就开始优化。我们可以得到dp[i,m]=dp[j,m-1]+sum[j]*(sum[i]-sum[j])(这里倒着考虑比较好想),然后进行斜率优化。这道题目中可能出现分母为0的情况,就赋成正负无穷就可以了。
#include<iostream> #include<cstdio> #include<cstring> #define maxnode 100005 #define inf 0x7fffffffffffffffLL using namespace std; long long dp[maxnode][2]={0},q[maxnode]={0},sum[maxnode]={0}; int cur=0,n; long long getdp(int i,int j) { return dp[j][cur^1]+sum[j]*(sum[i]-sum[j]); } long long getup(int j,int k) { return (dp[j][cur^1]-sum[j]*sum[j])-(dp[k][cur^1]-sum[k]*sum[k]); } long long getdown(int j,int k) { return sum[j]-sum[k]; } double g(int j,int k) { long long i,jj; i=getdown(j,k);jj=getup(j,k); if (i==0) { if (jj>0) return inf; if (jj==0) return 0; if (jj<0) return -inf; } else return jj*1.0/i; } int main() { int h,t,i,j,k; scanf("%d%d",&n,&k); for (i=1;i<=n;++i) { scanf("%lld",&sum[i]); sum[i]+=sum[i-1]; } for (i=1;i<=k;++i) { cur^=1;h=t=1;q[t]=0; for (j=1;j<=n;++j) { while(h<t&&g(q[h+1],q[h])>-sum[j]) ++h; dp[j][cur]=getdp(j,q[h]); while(h<t&&g(j,q[t])>g(q[t],q[t-1])) --t; q[++t]=j; } } printf("%lld\n",dp[n][cur]); }
bzoj1096 [ZJOI2007] 仓库建设
题目大意:山坡上有n个工厂,从山上到山脚依次是1-n,到第一个工厂的距离为xi,每个工厂有产品pi个,从一个工厂建仓库的钱是ci。每个工厂的产品只能向下运到某一个仓库,每一个产品一单位长度的运费是1。求所有产品都能进仓库的最小费用(建仓库的费用和运费总和)。
思路:首先每个产品一定运到离它下面最近的那个仓库费用最小,同时第n个位置一定要建仓库。我们用f[i]表示第i个位置建仓库的总费用,ss[j,i]表示j到i的产品运到n的运费,ss[j,i]=sigma(k=j~i)((xi[n]-xi[k])*pi[k]),设sum[k]=(xi[n]-xi[k])*pi[k],可以用前缀和的思想维护出来,ge[i,j]表示i~j的产品个数和,f[i]=f[j]+ci[i]+ss[j+1,i]-ge[j+1,i]*(xi[n]-xi[i]),j<k时,dp[j,i]>dp[k,i],化简可得(dp[j]-dp[k]-sum[j]+sum[k])/(ge[k]-ge[j])>(xi[n]-xi[i]),然后就可以斜率优化了。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxnode 1000005 using namespace std; long long fi[maxnode]={0},xi[maxnode]={0},pi[maxnode]={0},ci[maxnode]={0}, sum[maxnode]={0},hdis[maxnode]={0},q[maxnode]={0},n; long long getdp(int i,int j) { return (fi[j]+ci[i]-(xi[n]-xi[i])*(sum[i]-sum[j])+hdis[i]-hdis[j]); } long long getup(int i,int j){return (fi[i]-fi[j]-hdis[i]+hdis[j]);} long long getdown(int i,int j){return (sum[j]-sum[i]);} double g(int i,int j){return (getup(i,j)*1.0)/(getdown(i,j));} int main() { int m,i,j,head,tail; scanf("%d",&n); for (i=1;i<=n;++i) { scanf("%I64d%I64d%I64d",&xi[i],&pi[i],&ci[i]);sum[i]=sum[i-1]+pi[i]; } for (i=1;i<=n;++i) hdis[i]=hdis[i-1]+(xi[n]-xi[i])*pi[i]; head=tail=1;q[tail]=0; for (i=1;i<=n;++i) { while(head<tail&&g(q[head+1],q[head])>(1.0*(xi[n]-xi[i]))) ++head; fi[i]=getdp(i,q[head]); while(head<tail&&g(i,q[tail])>g(q[tail],q[tail-1])) --tail; q[++tail]=i; } printf("%I64d\n",fi[n]); }
还有一种降至O(nlogn)的方法,穷举每一个数的时候,二分这个决策开始的转折点,然后更新决策。这个过程用栈实现。
某个模拟赛的题目
题目大意:给定一些区间,求出一些(多于一个)区间使它们的交与并的乘积最大。
思路:我们将被包含的区间都去掉(同时要更新答案,否则可能会丢掉答案),然后我们得到了左右端点都严格递增的一些线段,进行dp优化,就可以了。(这是在我们假定决策单调性的情况下。)
当时的题解说有决策单调性,但是写的时候发现并不能a掉,特判了小数据才a。希望有大神能够解答一下。
#include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #define maxnode 1000010 using namespace std; struct use{ long long ll,rr; }aa[maxnode]={0}; struct zhan{ int l,r,jue; }zz[maxnode]={0}; long long ans=0; int n,m; int my_comp(const use x,const use y) { if (x.ll==y.ll) { if (x.rr>y.rr) return 1; else return 0; } if (x.ll<y.ll) return 1; else return 0; } long long calc(int i,int j) { return ((aa[j].rr-aa[i].ll)*(aa[i].rr-aa[j].ll)); } int find(int x,int i) { int l,r,mid; if (calc(i,zz[x].r)<calc(zz[x].jue,zz[x].r)) return zz[x].r+1; l=zz[x].l;r=zz[x].r; while(l<r) { mid=(l+r)/2; if (calc(i,mid)>calc(zz[x].jue,mid)) r=mid; else l=mid+1; } return l; } void dp() { int i,j,tot=0,now=0; long long ff; zz[now=++tot]=(zhan){1,n,1}; for (i=2;i<=n;++i) { if (zz[now].r<i) ++now; ff=calc(zz[now].jue,i); ans=max(ans,ff); if (calc(zz[tot].jue,n)<calc(i,n)) { while(tot>=now) { if (calc(zz[tot].jue,zz[tot].l)<calc(i,zz[tot].l)) --tot; else { int k=find(tot,i); zz[tot].r=k-1;zz[++tot]=(zhan){k,n,i}; break; } } } } } long long calc1(int i,int j) { return (aa[j].rr-aa[i].ll)*(aa[i].rr-aa[j].ll); } void work() { int i,j; for (i=1;i<n;++i) for (j=i+1;j<=n;++j) ans=max(ans,calc1(i,j)); } int main() { int i,j,tot; scanf("%d",&n); for (i=1;i<=n;++i) scanf("%I64d%I64d",&aa[i].ll,&aa[i].rr); sort(aa+1,aa+n+1,my_comp);tot=1; for (i=2;i<=n;++i) { if (aa[i].rr>aa[tot].rr) aa[++tot]=aa[i]; else ans=max(ans,(aa[tot].rr-aa[tot].ll)*(aa[i].rr-aa[i].ll)); } n=tot; if (n<3000) work(); else dp(); printf("%I64d\n",ans); }
bzoj1044 木棍分割
题目大意:给定一些木棍,首尾顺次相连,最多切m次,使最长的一段最小的值和方案数。
思路:第一问二分答案,然后贪心的能选就选不能选就开一个新的或者返回不行,分的段过多也是不行的;第二问dp一下,f[i][j]表示前j个木棍分i段的方案数(这里的i段一定是每段都不为空,答案中取f[i][n]的和就可以了),f[i][j]=sigma f[i-1][k](sum[j]-sum[k]<=ans1),这里因为是前缀和所以决策一定单调,保存一下这个决策更新的起点,对f[i]做一个前缀和就可以求出答案了,最后复杂度是O(nm)的。因为空间给的很小,所以要滚动数组。(第二问的方案中一定有ans1这个长度的一段,因为如果不能分出这样的一段,肯定是每一段都比这个小(大的不会更新进答案),那么ans1就会变小,而不是这个了。)
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxnode 50005 #define p 10007 using namespace std; int ai[maxnode]={0},sum[maxnode]={0},f[2][maxnode]={0},g[2][maxnode]={0},n,m; bool judge(int d) { int i,ss=0,tot=0; for (i=1;i<=n;++i) { if (ss+ai[i]<=d){ss+=ai[i];} else {++tot;if (ai[i]>d) return false; ss=ai[i];} } if (tot>m) return false; else return true; } int main() { freopen("stick.in","r",stdin); freopen("stick.out","w",stdout); int i,j,k,l,r,mid,ans=0; scanf("%d%d",&n,&m); for (i=1;i<=n;++i) { scanf("%d",&ai[i]);sum[i]=sum[i-1]+ai[i]; } l=ai[1];r=sum[n]; while(l!=r) { mid=(l+r)/2; if (judge(mid)) r=mid; else l=mid+1; } printf("%d ",l);r=0; for (i=1;i<=n;++i) { if (sum[i]<=l) f[0][i]=1; g[0][i]=(g[0][i-1]+f[0][i])%p; } for (i=2;i<=m+1;++i) { k=0;r^=1;g[r][0]=0; for (j=1;j<=n;++j) { while(sum[j]-sum[k]>l) ++k; k=max(0,k-1); f[r][j]=((g[r^1][j-1]-g[r^1][k])%p+p)%p; g[r][j]=(g[r][j-1]+f[r][j])%p; } ans=(ans+f[r][n])%p; } printf("%d\n",ans); fclose(stdin); fclose(stdout); }