• 动态规划DP的优化


    写一写要讲什么免得忘记了。DP的优化。

    大概围绕着"是什么","有什么用","怎么用"三个方面讲.

    主要是《算法竞赛入门经典》里的题目讲解,但是有些过于简单的删去了,添加了一些不怎么简单的省选题目作为例子

    这里的DP优化都是涉及到O(nk)到O(nk-1)方法比较巧妙也有用到数学里面的定理之类。

    所以秉着由易到难的原则,安排内容如下:

    专题1:动态规划基础知识和计数DP、数位DP(几大类DP的类型介绍)

    专题2:DP的简单优化(稍微提两句mjy大佬的任务) 

    专题3:单调队列优化DP和斜率优化(这个比较难也比较重要)  blog

    专题4:四边形不等式优化DP

    专题5:习题课

    专题1:动态规划基础知识和计数DP、数位DP(几大类DP的类型介绍)

    专题2:DP的简单优化(稍微提两句mjy大佬的任务)

    专题3:单调队列优化DP和斜率优化(这个比较难也比较重要)

    斜率优化DP   blog

    将这个DP的优化方法之前我们必须看一个例子,优化的前提一直是暴力DP不错!

    P3195 [HNOI2008]玩具装箱TOY

    这个DP方程非常好想,F[i]从第1个到第i个物品放在箱子里的最小花费。

    转移从第j个物品开始考虑在第j个物品放完之后的(i+1)到第j个物品放在一个容器中,每次决策一次那么得出方程式

    为了方便起见我们这里的L++,然后用sum[x]表示C的前缀和那么DP方程就可以改写为:

    然后我们发现对于确定的i,sum[i]+i的值是一定的,我们用s[x]表示sum[i]+i

    进一步改写DP方程:

    于是我们这个DP方程就显的优美了,不妨把暴力的代码打出来把:

    # include<bits/stdc++.h>
    # define int long long
    # define SQR(x) ((x)*(x))
    using namespace std;
    const int MAXN=1e5+10;
    int s[MAXN],f[MAXN];
    int n,L;
    signed main()
    {
        scanf("%lld%lld",&n,&L); L++;
        int t;
        for (int i=1;i<=n;i++)
         scanf("%lld",&t),s[i]=s[i-1]+t;
        for (int i=1;i<=n;i++) s[i]+=i;
        memset(f,0x3f,sizeof(f)); f[0]=0;
        for (int i=1;i<=n;i++)
         for (int j=0;j<i;j++)
          f[i]=min(f[i],f[j]+SQR(s[i]-s[j]-L)); 
        printf("%lld
    ",f[n]);  
        return 0;
     } 

    我们发现这样的算法时间完全承受不了,我们考虑优化!!!

    优化用到的正是斜率优化。

    为了O(1)转移我们必须寻求一种方法来找到最优的转移方案,

    我们不妨把式子化简一下

    对于当前最优的决策方案Fi,我们的每一个j都可以表示一个Fi的取值,这里取到最值,这和直线非常相似我们不妨把带有j的当做变量分离一下试试

    b  +   k  *  x       =     y

    我们发现这样一个神奇的式子,对于每一个j的取值都有一个在J(sj+L,fj+si2+(sj+L)2)与之对应,这个J就是坐标轴上离散的一个点,

    就好比对于所有决策状态中的点J集合,一条直线(斜率K=2si已固定)经过这个J集合中至少一个点,使其截距b,尽可能小。

    观察到题目中的c[i]都是正数意味着k=2*s[i]必然单调递增,我们承认的一个事实是在平面直角坐标系中一条直线k的值越大越陡,截距b越小

    考虑怎样一个数据结构可以维护这样一个,单调递增k的特性呢?答案显然是单调队列,我们只要维护一个下凸包即可。

     

    具体的解释是这样,考虑F[i]的斜率2*si如显然AB的斜率比F[i]的斜率小那么显然,A就是一个废弃的点(由于B的存在我宁可连B也不连A),我们就可以把它弹掉。

    对于更新过的坐标集,第一个点的显然是最优的,由于满足下凸的性质,直线斜率不变,那么截距只能越来越大,这时候更新答案,更新完毕之后由于产生一个新的决策点J

    我们需要对前面的点做一遍检查

    对于新加进来的这个决策点new(就是当前的最优值),我们判断他是不是有资格作为后续DP状态的来源点,

    如果new这个点和A这个点的斜率比BC的斜率还要小,那么BC这两个点将会被清除由于后续来的斜率线段一定会选择过new而不是B或者C

    这也是基于上面的下凸包的性质。

    提醒一下对于当前需要转移的i,我们可以不作记录的原因在于

    对于每一个和i有关的常数我们都会在作差之中消除,我们可以不用理他(抵消!),这样程序就没有了i的干扰了!

    Code:

    # include<bits/stdc++.h>
    # define int long long
    # define SQR(x) ((x)*(x))
    using namespace std;
    const int MAXN=5e4+10;
    int sum[MAXN],F[MAXN],c[MAXN],s[MAXN],q[MAXN];
    int n,L;
    inline double X(int j){ return (double)s[j];}
    inline double Y(int j){ return (double)F[j]+(s[j]+L)*(s[j]+L);}
    //和i无关的每一个j点计算出他的横坐标和纵坐标
    inline double R(int i,int j){return (Y(j)-Y(i))/(X(j)-X(i));}
    //i下的两点斜率
    # define Empty (head>=tail)
    signed main()
    {
        scanf("%lld%lld",&n,&L); L++; sum[0]=0;
        for (int i=1;i<=n;i++) 
            scanf("%d",&c[i]),
            sum[i]=sum[i-1]+c[i],
            s[i]=sum[i]+i;
        int head=1,tail=1; q[1]=0;
    //涉及到取两个元素的队列还是手打比较好
        for (int i=1;i<=n;i++) {
            while (!Empty&&R(q[head],q[head+1])<2*s[i]) head++;
    //不满足下凸的性质队头出
            int j=q[head]; F[i]=F[j]+SQR(s[i]-s[j]-L);
    //转移
            while (!Empty&&R(q[tail-1],q[tail])>R(q[tail],i)) tail--; 
    //不满足下凸性质的队尾出
    q[++tail]=i;
    //加入一个新的决策i
        }
        printf("%lld
    ",F[n]);
        return 0;
     } 
    //这个板子会在后面经常用到

    这里还需要提高一下,我们其实不需要吧这个直线写出来就可以知道斜率,这样减少思维难度。

    是这个方程,我们不妨考虑一个决策k在另一个决策j之前,但是k没有j优秀(对于更新外部循环变量i来说),

    即 

    所以k这个决策无用抛弃。

    可以化简为左边是si和sj或sk乘积形式除过去,就可以得到斜率

    这个式子本质上是和上面是一样的,和R没有什么区别。

    维护的话相似。 

    P2120 [ZJOI2007]仓库建设

    考虑最简单的DP方程:

    f[i]从山顶(1号)到第i号放完的最小代价

    考虑f[i]从j转移过来。

    设Wk表示如果将i这个地点作为建站处那么对于k<i的任意一个点,其代价

    那么从1-j 已经处理完毕,考虑 j+1 到 i 这些物品的结构

    转移方程:

    对于需要转移的每一个x[i]不变,转移方程可以改写为

     

     

    前缀和处理 -x[i]*p[i]和p[i]的前缀和分别为 g[i] 和 P[i]

    这样复杂度降到了O(n^2)

    帖下代码:

    #include <bits/stdc++.h>
    #define int long long
    using namespace std;
    const int MAXN=1e6+10;
    int f[MAXN],x[MAXN],p[MAXN],P[MAXN],c[MAXN],g[MAXN];
    int n;
    signed main()
    {
        scanf("%lld",&n);
        for (int i=1;i<=n;i++) 
            scanf("%lld%lld%lld",&x[i],&p[i],&c[i]),
            P[i]=P[i-1]+p[i],g[i]=g[i-1]-x[i]*p[i];
        memset(f,0x3f,sizeof(f)); f[0]=0;
        for (int i=1;i<=n;i++) 
         for (int j=0;j<i;j++)
          f[i]=min(f[i],f[j]+x[i]*P[i]-x[i]*P[j]+g[i]-g[j]+c[i]);
        printf("%lld
    ",f[n]);  
        return 0;
    }

    接下来将斜率优化的部分:

    f[i]=f[j]+x[i]*P[i]-x[i]*P[j]+g[i]-g[j]+c[i]

    f[i]+x[i]*P[j] = f[j] + x[i]*P[i] +g[i] - g[j] + c[i]

    b +   k   *  x  =  y

    由于斜率单调递增,那么x[i]单调递增所以斜率单调递增,所以处理方法同上!

    #include <bits/stdc++.h>
    #define int long long
    #define Empty (head>=tail)
    using namespace std;
    const int MAXN=1e6+10;
    int f[MAXN],x[MAXN],p[MAXN],P[MAXN],c[MAXN],g[MAXN],q[MAXN];
    double X(int j) { return (double)P[j];}
    double Y(int j) { return (double)f[j]-(double)g[j];}
    double R(int i,int j){return (double)(Y(i)-Y(j))/(X(i)-X(j));}
    int n;
    signed main()
    {
        scanf("%lld",&n);
        for (int i=1;i<=n;i++) 
            scanf("%lld%lld%lld",&x[i],&p[i],&c[i]),
            P[i]=P[i-1]+p[i],g[i]=g[i-1]-x[i]*p[i];
         int head=1,tail=1; q[1]=0;
        for (int i=1;i<=n;i++) {
            while (!Empty&&R(q[head],q[head+1])<x[i]) head++;
            int j=q[head]; f[i]=f[j]+x[i]*P[i]-x[i]*P[j]+g[i]-g[j]+c[i];
            while (!Empty&&R(q[tail-1],q[tail])>R(q[tail],i)) tail--; 
            q[++tail]=i;
        }
        printf("%lld
    ",f[n]);
        return 0;
    }

    P3628 [APIO2010]特别行动队

    先考虑暴力DP+前缀和优化!

    设F[i]表示前i个士兵安排任务最大化战斗力,sum[x]表示x的前缀和数组

     

    对于这个式子可以用前缀和表示,用F(x)=A*x*x+B*x+C代换可知

    依然考虑斜率优化下,还是写成斜率的形式

    依旧把有斜率的东西放到左边,右边保留一个解析式,就像这样:

    b  +   k     *   x      =  y

    这里是斜率k单调递减然后求fi最大值,其实只要向上面一样维护一个上凸包即可!

    代码其实只要改两个符号就差不多了,理解就是取反然后按照斜率递增求Min一样就行。

    code:

    # include <bits/stdc++.h>
    # define int long long
    # define Empty (head>=tail)
    using namespace std;
    const int MAXN=2e6+10;
    int sum[MAXN],f[MAXN],q[MAXN*2],A,B,C,n;
    double X(int j){return sum[j];}
    double Y(int j){return (double)f[j]+A*sum[j]*sum[j]-B*sum[j];} 
    double R(int i,int j){return (double)(Y(i)-Y(j))/(double)(X(i)-X(j));}
    int Fun(int x) {return A*x*x+B*x+C;}
    signed main()
    {
        scanf("%lld",&n);
        scanf("%lld%lld%lld",&A,&B,&C);
        int t;
        for (int i=1;i<=n;i++) 
         scanf("%lld",&t),sum[i]=sum[i-1]+t;
        int head=1,tail=1; q[1]=0;
        for (int i=1;i<=n;i++) {
            while(!Empty&&(R(q[head+1],q[head])>2*A*sum[i])) head++;
            int j=q[head];  f[i]=f[j]+Fun(sum[i]-sum[j]); 
            while (!Empty&&(R(q[tail],q[tail-1])<R(q[tail],i))) tail--;
            q[++tail]=i;
        } 
        printf("%lld
    ",f[n]);
        return 0;
    }

    到这里我们已经完成了斜率优化的入门题型这里给出几个练习,有助于能力提升:

    专题4:四边形不等式优化DP

    四边形不等式:相交小于等于包含

    设w(x,y)是定义在Z上的二元函数,对于a<=b<=c<=d属于Z,都有w(a,d)+w(b,c)>=w(a,c)+w(b,d)

    或者定义a<b,有w(a,b+1)+w(a+1,b)>=w(a,b)+w(a+1,b+1)

    这两种定义是等价的!

    证明:对于a<c,有w(a,c+1)+w(a+1,c)>=w(a,c)+w(a+1,c+1) (第2种定义)

       对于a+1<=c,有w(a+1,c+1)+w(a+2,c)>=w(a+1,c)+w(a+2,c+1) (第2种定义)

       两式相加,得:w(a+1,c+1)+w(a+2,c)+w(a,c+1)+w(a+1,c)>=w(a+1,c)+w(a+2,c+1) + w(a,c)+w(a+1,c+1)

       消去相同项得:w(a+2,c)+w(a,c+1)>=w(a+2,c+1) + w(a,c)   

       同理对于任意的a<=b<=c有w(a,c+1)+w(b,c)>=w(a,c)+w(b,c+1)

       同理对于任意的a<=b<=c<=d都有w(a,d)+w(b,c)>=w(a,c)+w(b,d)

    证毕。

    一维线性DP的四边形不等式优化:

    对于形如  的一维线性DP方程,记录P[i]表示F[i]取到最小值的j,

    若P[i]单调不减,则F具有决策单调性

    定理:若val满足四边形不等式即val为凸(以后为了方便,满足四边形不等式的性质一律叫凸) 则F具有决策单调性

    证明:令i在[1,N],j在[0,P[i]-1],i’在[i+1,N] 

    根据P[i]最优性,得F[P[i]]+val(P[i],i)<=F[j]+val(j,i)

    由于val满足四边形不等式,有val(j,i)+val(P[i],i')<=val(j,i')+val(P[i],i)【相交小于等于包含】

    两式相加,得:F[P[i]]+val(P[i],i)+val(j,i)+val(P[i],i')<=F[j]+val(j,i)+val(j,i')+val(P[i],i)

    消去相同项,得:F[P[i]]+val(P[i],i')<=F[j]+val(j,i)  

    对于i'的最优决策P[i']在[P[i],i']不可能小于P[i],即P[i']>=P[i]

    所以F满足决策单调性

    在循环的任意时刻,数组中的情况一定是形如

    由于决策单调则j1<j2<j3<j4<j5

    求出F[i]后考虑i可能作为F[i'] (i'>i)的决策,那借用单调队列的思想考虑一个位置pos,之前的决策都比i好,之后的决策都比i差,

    我们需要快速找到上述位置并把之后的所有元素改为i,把[pos,i]改为i,

    假设我们的位子在j3(中间那个),那么处理后的数组就变为:

    显然直接修改效率太低,我们在队列中用若干个三元组(j,l,r)表示数组的[l,r]最优决策都是j

    另外队列中无需保留小于P[1~i-1]的部分,(由于F的决策单调性)

    队列头部就是最优决策

    算法:

    1. 检查队头(j0,l0,r0),若r0<=i-1,删除队头,否则l0=i
    2. 取出队头的决策j作为最优决策,状态转移求出F[i]
    3. 尝试插入新决策i:
      • (1)取出队尾(jt,lt,rt)
      • (2)对于F[lt]来说i是比jt更优的决策(由于决策单调对于lt来说i都优于此时的决策那么在队尾整个区间都差于此时的决策),pos=l删除队尾,goto(1)
      • (3)对于F[rt]来说i不如jt更优 goto(5) 
      • (4)不满足(2)和(3)的,在[lt,rt]二分查找到pos,使之前的决策比i更优,之后的决策i更优(就说对于F[mid]来说,i决策比jt决策更优最小化mid),goto(5)
      • (5)把(i,pos,N)插入队尾

     [诗人小G]

    # include <bits/stdc++.h>
    # define int long long
    # define ld long double
    using namespace std;
    const int MAXN=1e6+10;
    int N,P,L;
    ld sum[MAXN],f[MAXN];
    int last[MAXN],nxt[MAXN];
    char s[MAXN][55];
    struct node{ int j,l,r;};
    deque<node>q;
    void Print_B()
    {
        puts("Too hard to arrange");
    }
    void Print_E()
    {
        for (int i=1;i<=20;i++) putchar(45);
        putchar('
    ');
    }
    void write(int x)
    {
        if (x<0) { x=-x; putchar('-');}
        if (x>9) write(x/10);
        putchar('0'+x%10);
    }
    void writeln(int x)
    {
        write(x);putchar('
    ');
    }
    inline int read()
    {
        int X=0,w=0; char c=0;
        while (!(c>='0'&&(c<='9'))) w|=c=='-',c=getchar();
        while ((c>='0'&&(c<='9'))) X=(X<<1)+(X<<3)+(c^48),c=getchar();
        return w?-X:X;
    }
    ld pow(ld x,int n)
    {
        ld ans=1;
        while (n) {
            if (n&1) ans=ans*x;
            x=x*x;
            n>>=1;
        }
        return ans;
    }
    ld calc(int i,int j)
    {
        return (ld)f[j]+pow(abs(sum[i]-sum[j]+(i-j-1)-L),P);
    }
    void Clear()
    {
        memset(f,0,sizeof(f));
        deque<node>tmp; swap(q,tmp);
        memset(last,0,sizeof(last));
        memset(nxt,0,sizeof(nxt));
        sum[0]=0;
    }
    signed main()
    {
        int T; T=read();
        while (T--) {
            Clear();
            N=read();L=read();P=read();
            for (int i=1;i<=N;i++) {
                cin>>s[i];
                int len=strlen(s[i]);
                sum[i]=sum[i-1]+(ld) strlen(s[i]);
            } 
            q.push_back((node){0,1,N});
            for (int i=1;i<=N;i++) {
                while (!q.empty()) {
                    if (q.front().r<i) q.pop_front();
                    else {
                        q.front().l=i; break;
                    }
                }
    
                int j=q.front().j;
                last[i]=j;
                f[i]=calc(i,j);
                int pos=-1;
                while (!q.empty()) {
                    int lt=q.back().l;
                    int rt=q.back().r;
                    int jt=q.back().j;
                    if (calc(lt,i)<=calc(lt,jt)) {
                        pos=lt; q.pop_back(); continue;
                    } else 
                    if (calc(rt,jt)<=calc(rt,i))  break;
                    else {
                        int l=lt,r=rt,ans=1;
                        while (l<r) {
                            int mid=(l+r)>>1;
                            if (calc(mid,i)<=calc(mid,jt)) r=mid;
                            else l=mid+1;
                        }
                        q.back().r=l-1; pos=l;  break;
                    }
                }
                if (pos!=-1) q.push_back((node){i,pos,N});
            }
            if (f[N]>(1e18)*1ll) Print_B();
            else {
                printf("%lld
    ",(int)(f[N]+0.5));
                for (int i=N;i;i=last[i]) nxt[last[i]]=i;
                int now=0;
                for (int i=1;i<=N;i++) {
                    now=nxt[now];
                    int tmp=now;
                    for (int j=i;j<tmp;j++) printf("%s ",s[j]);
                    puts(s[tmp]);
                    i=tmp;
                }
            } 
            Print_E();
        }
        return 0;
    } 

     二维区间DP 四边形不等式定理:

     (特别的要求F[i][i]=w[i][i]=0)

    如果有下面条件成立:

    1. w为凸
    2. 对于任意的a<=b<-c<=d有w(a,d)>=w(b,c)

    那么F也为凸。

    由于我们定义二元函数的凸性是有两种定义方法,

    我们就是要证明:对于任意 i< i+1<=j< j+1,满足f[i][j]+f[i+1][j+1]<=f[i][j+1]+f[i+1][j](交叉小于等于包含) 

    设f[i+1][j]取最小值的时候k=x,f[i][j+1]取最小值的时候k=y

    f[i][j]=f[i][x]+f[x+1][j]+w(i,j)

    f[i+1][j+1]=f[i+1][y]+f[y+1][j+1]+w(i+1,j+1)

    所以左式取最值的时候,左式=f[i][x]+f[x+1][j]+w(i,j)+f[i+1][y]+f[y+1][j+1]+w(i+1,j+1)

    由于w为凸,所以w(i,j)+w(i+1,j+1)<=w(i+1,j)+w(i,j+1)     

    f[i][x]+f[x+1][j]+w(i,j)+f[i+1][y]+f[y+1][j+1]+w(i+1,j+1)<=f[i][j+1]+f[i+1][j]

    右式=f[i][y]+f[y+1][j+1]+w(i,j+1)+ f[i+1][x]+f[x+1][j]+w(i+1,j)

    得:

    f[i][x]+f[x+1][j]+w(i,j)+f[i+1][y]+f[y+1][j+1]+w(i+1,j+1)<=f[i][y]+f[y+1][j+1]+w(i,j+1)+ f[i+1][x]+f[x+1][j]+w(i+1,j)

    展开得:

    f[i][j]+f[i+1][j+1]<=f[i][j+1]+f[i+1][j]

    证毕。

     二维区间DP决策单调性定理:

    如果  (特别的要求F[i][i]=w[i][i]=0)为凸

    那么对于任意i<j都有P[i][j-1]<P[i][j]<P[i+1][j]

    记p=P[i][j],对于任意的i<k<=p,由于F为凸那么f[i][p]+f[i+1][k]>=f[i][k]+f[i+1][p]

    移项可得:f[i+1][k]-f[i+1][p]>=f[i][k]-f[i][p]

    由于p最优,得f[i][k]+f[k+1][j]>=f[i][p]+f[p+1][j]

    (f[i+1][k]+f[k+1][j]+w(i+1,j))-(f[i+1][p]+f[p+1][j]+w(i+1,j))

    = f[i+1][k]-f[i+1][p]+f[k+1][j]-f[p+1][j]

    >=f[i][k]-f[i][p]+f[k+1][j]-f[p+1][j]

    = f[i][k]+f[k+1][j]-(f[i][p]+f[p+1][j])>=0

    所以对于f[i+1][j],p比任何k<=p优所以P[i+1][j]>=P[i][j]

    同理可知P[i][j-1]<=P[i][j]

     

    四边形不等式优化定理总结


    1.四边形不等式的定义:相交小于包含
    两种等价定义:
    对于a<=b<=c<=d属于Z,都有w(a,d)+w(b,c)>=w(a,c)+w(b,d)
    对于a,b属于Z 若 a<b,有w(a,b+1)+w(a+1,b)>=w(a,b)+w(a+1,b+1)
    2.一维线性DP决策单调定理
    对于形如f[i]=min_{0<=j<i}{F[j]+w(j,i)}若w为凸那么F决策单调递增
    3.二维区间DP决策单调性定理
    对于形如F[i][j]=min_{i<=k<=j}{f[i][k]+f[k+1][j]+w(i,j)}
    (特别的要求F[i][i]=w(i,i)=0) 若w为凸则F为凸,
    对于F理应满足决策P[i][j-1]<P[i][j]<P[i+1][j]
    P[l][r]表示当[l,r]分为[l,k]和[k+1,r]两部分时F[l][r]最大

    利用第二种等价定义,证明函数w(x,y)的凸性事实上只要
    证明对于任意j<i,w(j,i+1)+w(j+1,i)>=w(j,i)+w(j+1,i+1)
    只需证明:w(j+1,i)-w(j+1,i+1)>=w(j,i)-w(j,i+1)
    代入换元函数单调性可知w(x,y)的凸性

    更方便的方案:打表暴力DP验证决策单调!

    石子合并弱化版本 https://www.luogu.org/problemnew/show/U58387
    石子合并强化版本 GarsiaWachs算法(这里放过了GW的暴力O(n^2)那是因为数据随机)

    专题5:习题课

  • 相关阅读:
    Android VersionedGestureDetector手势事件
    Android drawBitmapMesh扭曲图像
    如何利用SVN合并代码
    Android 微信分享图文资料
    Android google map 两点之间的距离
    Android 监听ContentProvider的数据改变
    Android 自动朗读(TTS)
    Android ContentProvider的实现
    解决xcode升级之后安装的插件失效
    Android Studio 更换国内源下载依赖库
  • 原文地址:https://www.cnblogs.com/ljc20020730/p/10132077.html
Copyright © 2020-2023  润新知