决策单调性 dp

时间:2022-12-16 13:59:57

对于一些具有决策单调性的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)。对于这里涉及两个符号的问题,我们可以统一符号(如上两个式子)。

决策单调性 dp决策单调性 dp
#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]);
}
View Code

 

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])。

决策单调性 dp决策单调性 dp
#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]);
}
View Code

 

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(一定要认真一步步的化式子!!!),然后符号统一一下。

决策单调性 dp决策单调性 dp
#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]);
}
View Code

 

bzoj3675 序列分割

题目大意:一个n个数的序列,分k次,每次从一个位置分开后,将两边的子段和的乘积加给ans,求ans的最大值。

思路:看到这道题目想做个四重的循环,后来就开始优化。我们可以得到dp[i,m]=dp[j,m-1]+sum[j]*(sum[i]-sum[j])(这里倒着考虑比较好想),然后进行斜率优化。这道题目中可能出现分母为0的情况,就赋成正负无穷就可以了。

决策单调性 dp决策单调性 dp
#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]);
}
View Code

 

 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]),然后就可以斜率优化了。

决策单调性 dp决策单调性 dp
#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]);
}
View Code

 

 

还有一种降至O(nlogn)的方法,穷举每一个数的时候,二分这个决策开始的转折点,然后更新决策。这个过程用栈实现。

某个模拟赛的题目

题目大意:给定一些区间,求出一些(多于一个)区间使它们的交与并的乘积最大。

思路:我们将被包含的区间都去掉(同时要更新答案,否则可能会丢掉答案),然后我们得到了左右端点都严格递增的一些线段,进行dp优化,就可以了。(这是在我们假定决策单调性的情况下。)

当时的题解说有决策单调性,但是写的时候发现并不能a掉,特判了小数据才a。希望有大神能够解答一下。

决策单调性 dp决策单调性 dp
#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);
}
View Code

 

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就会变小,而不是这个了。)

决策单调性 dp决策单调性 dp
#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);
}
View Code