动态dp和dp套dp

Posted 66t6

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态dp和dp套dp相关的知识,希望对你有一定的参考价值。

概述

这是两类特别的(dp)种类,分别是带修改的(dp),与(dp)过程本身息息相关的(dp)

动态dp

概述

一些简单的(dp)如果带修改怎么办

如果状态是普通设法,修改一个值的话包含这个值的所有情况都会被改,均摊(O(n))

一种粗暴的看法,我们可以让所有状态包含的值均摊,比如用倍增划分区间的设法,让均摊被包含的状态数变为(log),但这个说法很模糊,我们并不知道什么状态可以倍增

事实上,这牵扯到一个很重要的东西,"转移"这个运算有结合律

如果有的话,我们就可以一段一段的维护,合并,在树上就可以通过各种平衡树或树剖分成一条一条重链来处理

很多矩阵的新定义都具有结合律

多余的提一句,在树上,刚才这种方式又可以用只(dp)关键点的的虚树(dp)替代

问题

(mathtt{BSOJ5290})支持修改点权,动态求树的最大独立集

考虑普通(dp)

(f_{i,0/1})表示(i)点不选/选子树的最大独立集,答案就是(max{f_{1,0},f_{1,1}})

(f_{x,0}=sumlimits_{yin son_x}{f_{y,0},f_{y,1}})
(f_{x,1}=sumlimits_{yin son_x}f_{y,0}+a_x)

这是考虑了所有儿子的情况,再加入不考虑重儿子(或者实儿子)的答案

(g_{x,0}=sumlimits_{yin lson_x}{f_{y,0},f_{y,1}})
(g_{x,1}=sumlimits_{yin lson_x}f_{y,0}+a_x)

那么(f_{x,0}=g_{x,0}+max{f_{hson_x,0},f_{hson_x,1}})
(f_{x,1}=g_{x,1}+f_{hson_x,0})

定义矩阵新运算(C=A*B)表示(C_{i,j}=maxlimits_k{A_{i,k}+B_{k,j}}),易证明这个运算是具有结合律但不具有交换律的

转移可以写成(egin{bmatrix}g_{x,0}&g_{x,0}\g_{x,1}&-inftyend{bmatrix}*egin{bmatrix}f_{y,0}\f_{y,1}end{bmatrix}=egin{bmatrix}f_{x,0}\f_{x,1}end{bmatrix}(y=hson_x))

对每条重链(事实上是待测点和所在重链底部中间部分)维护矩阵连乘积即可,其实你可以注意到这个成绩的顺序是自底向上的,因此对(dfn)为下标的线段树上是先右后左

更新就是跳重链到根,(O(1))改底部的(g)值(因为这种边是轻边才会影响(g)),(O(log?))改重链区间连乘积

很久以前写的代码

#include<cstdio>
#include<algorithm>
#include<string.h>
#define re register
#define ls(x) ((x)<<1)
#define rs(x) (((x)<<1)|1)
#define N 200005 
using namespace std;
struct Matrix{
    int num[2][2];
    inline Matrix(void){memset(num,0,sizeof num);}
    inline friend Matrix operator*(re Matrix a,re Matrix b){
        re int i,j,k;re Matrix c;
        for(i=0;i<2;++i)for(j=0;j<2;++j)for(k=0;k<2;++k)c.num[i][j]=max(c.num[i][j],a.num[i][k]+b.num[k][j]);
        return c;
    }
}ldpm[N<<1],t[N<<1];
int n,q,dp[N][2],size[N],son[N],pos[N],st[N],ed[N],antipos[N],h[N],cnt,scnt,ldp[N][2],top[N],val[N],dep[N],fa[N];
struct Edge{
    int to,next;
}e[N<<1];
inline void AddEdge(re int x,re int y){e[++cnt]=(Edge){y,h[x]};h[x]=cnt;}
inline void pushup(re int x){t[x]=t[ls(x)]*t[rs(x)];}
inline void dfs1(re int x,re int depth,re int prt){
//  printf("%d %d %d
",x,depth,prt);
    re int i,y;dep[x]=depth;size[x]=1;
    dp[x][1]=val[x];fa[x]=prt;
    for(i=h[x];i;i=e[i].next){
        y=e[i].to;if(prt==y)continue;
        dfs1(y,depth+1,x);
        size[x]+=size[y];
        if(!son[x]||size[y]>size[son[x]])son[x]=y;
        dp[x][1]+=max(0,*dp[y]);
        *dp[x]+=max(0,max(*dp[y],dp[y][1]));
//      printf("%d->%d %d %d
",x,y,dp[x][1],*dp[x]); 
    }
}
inline void dfs2(re int x,re int Top,re int prt){
    re int i,y;
    top[x]=Top;pos[x]=++scnt;antipos[scnt]=x;ldp[x][1]=val[x];st[x]=scnt;ed[Top]=scnt;
    if(son[x])dfs2(son[x],Top,x);
    for(i=h[x];i;i=e[i].next){
        y=e[i].to;if(y==prt||y==son[x])continue;
        dfs2(y,y,x);
        ldp[x][1]+=max(0,*dp[y]);*ldp[x]+=max(0,max(*dp[y],dp[y][1]));
    }
}
inline void Change(re int pos,re int l,re int r,re int k){
    if(l==r){t[pos]=ldpm[l];return;}
    re int mid=(l+r)>>1;
    if(k<=mid)Change(ls(pos),l,mid,k);
    else Change(rs(pos),mid+1,r,k);
    pushup(pos);
}
inline Matrix Query(re int pos,re int l,re int r,re int ql,re int qr){
    if(ql<=l&&r<=qr) return t[pos];
    int mid=(l+r)/2;
    if(qr<=mid)return Query(ls(pos),l,mid,ql,qr);
    if(ql>mid)return Query(rs(pos),mid+1,r,ql,qr);
    return Query(ls(pos),l,mid,ql,qr)*Query(rs(pos),mid+1,r,ql,qr);
}
inline Matrix Ask(re int x){return Query(1,1,scnt,st[top[x]],ed[top[x]]);}
inline void Build(re int pos,re int l,re int r){
    if(l>r)return ;
    if(l==r){t[pos]=ldpm[l];return;}//!
    re int mid=(l+r)>>1;
    Build(ls(pos),l,mid);Build(rs(pos),mid+1,r);
    pushup(pos);
}
inline void Change(re int x,re int y){
    re Matrix past,now;
    ldpm[st[x]].num[1][0]+=(y-val[x]);val[x]=y;
    while(x){
        past=Ask(top[x]);
        Change(1,1,scnt,st[x]);
        now=Ask(top[x]);
        x=fa[top[x]];
        ldpm[st[x]].num[0][0]+=max(now.num[0][0],now.num[1][0])-max(past.num[0][0],past.num[1][0]);
        ldpm[st[x]].num[0][1]=ldpm[st[x]].num[0][0];
        ldpm[st[x]].num[1][0]+=now.num[0][0]-past.num[0][0];
    }
}
inline void Read(void){
    re int i,j,x,y;
    scanf("%d%d",&n,&q);
    for(i=1;i<=n;++i)scanf("%d",val+i);
    for(i=1;i<n;++i){
        scanf("%d%d",&x,&y);
        AddEdge(x,y);AddEdge(y,x);
    }
    dfs1(1,1,0);dfs2(1,1,0);
    for(i=1;i<=n;++i){
        ldpm[st[i]].num[0][0]=ldpm[st[i]].num[0][1]=ldp[i][0];
        ldpm[st[i]].num[1][0]=ldp[i][1];
//      cout<<'q'<<ldp[i][0]<<' '<<ldp[i][1]<<' '<<top[i]<<' '<<son[i]<<endl; 
    }
    Build(1,1,scnt);     
}
inline void Solve(void){
    re int x,y;re Matrix m;
    while(q--){
        scanf("%d%d",&x,&y);
        Change(x,y);
        m=Ask(1);
        printf("%d
",max(m.num[0][0],m.num[1][0]));
    }
}
int main(void){
    Read();
    Solve();
    return 0;
}

小可爱出题人一般是不会卡这(O(nlog^2n))的垃圾算法的

全局平衡二叉树

考虑我们好像真没用到重链剖分特殊的性质,看起来长链剖分,实链剖分都可以

但一个奇怪的事情就是(LCT)好像就可以做到(O(nlog n))只是常数过大

问题就在于(LCT)可以改树的形态,而我们并不需要,我们只需要把树做链剖分,保持树高(O(log n)),可以支持链上单点修改

食用论文《QTREE 解法的一些研究》可得"全局平衡二叉树"做法

方法很简单,对于每一条重链:

首先把重链铺平成一条长为(N)的序列

(s_i=size_i-size_{hson_i})

求出最小的(i)使得(sumlimits_{j=1}^i s_igeqslant frac{1}{2}sumlimits_{j=1}^N s_i),并用这个点作为这条重链的根,实边连本重链,虚边接上面重链

(e.g.)
技术图片
(来自(mathtt{color{red}Fee\_cle6418})的课件)

对于建树部分是这样的

inline int Build(re int l,re int r){
    if(l>r)return 0;
    re int pos=lower_bound(s+l,s+r+1,(s[r]-s[l-1]-1)/2+1+s[l-1])-s,ls,rs,x;
    x=st[pos],ls=Build(l,pos-1),rs=Build(pos+1,r);
    fa[*son[x]=ls]=fa[son[x][1]=rs]=x;pushup(x);
    return x;
}
inline int dfs(re int x){
    re int i,j,y,rt,top;
    for(i=x;i;i=bson[i])vis[i]=1;
    for(i=x;i;i=bson[i])
        for(j=h[i];j;j=e[j].next){
            if(vis[y=e[j].to])continue;
            fa[rt=dfs(y)]=i;
            a[i].a[1][1]+=max(b[rt].a[1][1],b[rt].a[2][1]);
            a[i].a[1][2]=a[i].a[1][1];
            a[i].a[2][1]+=b[rt].a[1][1]; 
        }
    for(top=0,i=x;i;i=bson[i])st[++top]=i,s[top]=s[top-1]+size[i]-size[bson[i]];
    return Build(1,top); 
}

全部的话在下面

(mathtt{Code})

注意

虽然说矩阵转移方便推广但事实上矩阵乘法由于自带(C^3)的常数,很多题卡不过去,有一些原始的(dp)如区间最大子段和的可删堆解法就可以沿用到树上,因为堆具有可合并性

例题

dp套dp

概述

顾名思义,解决奇怪的涉及(dp)过程的(dp)问题,常见形式有计数对象满足一定(dp)结果

(e.g.)(LCS(A,B)=K)(B)满足(P)条件方案数 或 不包含给定子串的(N)(K)进制数个数

很容易发现这两类问题如果转化为判定问题就真的是普式(dp)

一般的解决思路就是在解决原始判定(dp)过程中计数

更专业的解释直接说明本质:将内层(DP)的结果作为外层(DP)的状态进行(DP)的方法

引例

以上是关于动态dp和dp套dp的主要内容,如果未能解决你的问题,请参考以下文章

dp 套 dp扯谈

动态规划_计数类dp_数位统计dp_状态压缩dp_树形dp_记忆化搜索

[程序员代码面试指南]递归和动态规划-最长公共子串问题(DP,LCST)

BZOJ3864Hero meet devil DP套DP

Codeforces 372B Counting Rectangles is Fun:dp套dp

动态规划