【模板】动态DP&对于树链剖分的一些思考&全局平衡二叉树

链分治

链分治就是树链剖分。

在需要回答在子树/或结点到根路径查询时,由于 d f s dfs dfs序的连续性,可以用到树链剖分将复杂度降到 l o g 2 n log^2n log2n

先不考虑具体如何维护每个点的 d p dp dp值,假设转移是 O ( 1 ) O(1) O(1)的,可以用线段树维护,那么修改某个点的 d p dp dp值实际上就是修改了它到根路径上的值,查询答案即为根结点上的值。

在链分治后,只会跳 l o g n logn logn次链,每次链内/链与链交接处都可以 l o g n logn logn修改。

动态DP

查询操作

求树上的最大权独立集的权值大小。

f [ x ] [ 1 / 0 ] f[x][1/0] f[x][1/0]分别表示选/不选点 x x x时, x x x子树上的最大权独立集的权值大小,

正常的 D P DP DP转移:
f [ x ] [ 0 ] = m a x y ∈ s o n x ( f [ y ] [ 0 ] , f [ y ] [ 1 ] ) f[x][0]=max_{y\in son_x}(f[y][0],f[y][1]) f[x][0]=maxysonx(f[y][0],f[y][1])
f [ x ] [ 1 ] = v a l x + m a x y ∈ s o n x f [ y ] [ 0 ] f[x][1]=val_x+max_{y\in son_x}f[y][0] f[x][1]=valx+maxysonxf[y][0]

为了达到每次链内/链与链交接处都可以 l o g n logn logn修改,我们显然不能对于每个点逐个儿子的转移,而需要把信息压成重儿子和轻儿子两部分。

g [ x ] [ 1 / 0 ] g[x][1/0] g[x][1/0]分别表示不考虑重儿子情况下,选/不选点 x x x时, x x x子树上的最大权独立集的权值大小。

现在的 D P DP DP转移:
f [ x ] [ 0 ] = g [ x ] [ 0 ] + m a x ( f [ s o n x [ 0 ] , f [ s o n x ] [ 1 ] ] ) f[x][0]=g[x][0]+max(f[son_x[0],f[son_x][1]]) f[x][0]=g[x][0]+max(f[sonx[0],f[sonx][1]])
f [ x ] [ 1 ] = g [ x ] [ 1 ] + f [ s o n x ] [ 0 ] f[x][1]=g[x][1]+f[son_x][0] f[x][1]=g[x][1]+f[sonx][0]

似乎可以 O ( 1 ) O(1) O(1)合并了,线性的加和取 m a x max max

考虑维护连续的一段线性的加和取 m a x max max的方法,我们联想到了矩阵。

上述转移可以仿照 F l o y d Floyd Floyd的形式写成矩阵乘法:
[ f [ x ] [ 0 ] f [ x ] [ 1 ] ] = [ g [ x ] [ 0 ] g [ x ] [ 0 ] g [ x ] [ 1 ] − i n f ] × [ f [ s o n x ] [ 0 ] f [ s o n x ] [ 1 ] ] \left[ \begin{matrix} f[x][0] \\ f[x][1] \end{matrix} \right]= \left[ \begin{matrix} g[x][0]& g[x][0]\\ g[x][1] & -inf \end{matrix} \right]\times \left[ \begin{matrix} f[son_x][0]\\ f[son_x][1] \end{matrix} \right] [f[x][0]f[x][1]]=[g[x][0]g[x][1]g[x][0]inf]×[f[sonx][0]f[sonx][1]]

而对于 t o p = x top=x top=x的这条重链,不断向下展开连乘式,得到:
[ f [ x ] [ 0 ] f [ x ] [ 1 ] ] = [ g [ x ] [ 0 ] g [ x ] [ 0 ] g [ x ] [ 1 ] − i n f ] × [ g [ s o n x ] [ 0 ] g [ s o n x ] [ 0 ] g [ s o n x ] [ 1 ] − i n f ] × [ g [ s o n s o n x ] [ 0 ] g [ s o n s o n x ] [ 0 ] g [ s o n s o n x ] [ 1 ] − i n f ] . . . \left[ \begin{matrix} f[x][0] \\ f[x][1] \end{matrix} \right]= \left[ \begin{matrix} g[x][0]& g[x][0]\\ g[x][1] & -inf \end{matrix} \right]\times \left[ \begin{matrix} g[son_x][0]& g[son_x][0]\\ g[son_x][1] & -inf \end{matrix} \right]\times \left[ \begin{matrix} g[son_{son_x}][0]& g[son_{son_x}][0]\\ g[son_{son_x}][1] & -inf \end{matrix} \right]… [f[x][0]f[x][1]]=[g[x][0]g[x][1]g[x][0]inf]×[g[sonx][0]g[sonx][1]g[sonx][0]inf]×[g[sonsonx][0]g[sonsonx][1]g[sonsonx][0]inf]...

并且链分治后 x x x s o n x son_x sonx d f s dfs dfs序是连续的,所以可以用线段树维护一段连续区间的矩阵的连乘积。
每个结点的矩阵长这样:
[ g [ x ] [ 0 ] g [ x ] [ 0 ] g [ x ] [ 1 ] − i n f ] \left[ \begin{matrix} g[x][0]& g[x][0]\\ g[x][1] & -inf \end{matrix} \right] [g[x][0]g[x][1]g[x][0]inf]

查询直接回答根节点的值即可。

修改操作

我们发现修改操作 ( x , y ) (x,y) (x,y)只需要修改 x x x的矩阵和 x x x不断跳链到达根节点过程中经过的每个链顶结点的矩阵。

所以将复杂度转成了:

在链分治后,只会跳 l o g n logn logn次链,每次链内/链与链交接处都可以 l o g n logn logn修改。

代码

luoguP4719 【模板】动态dp

#include<bits/stdc++.h>
#define mid ((l+r)>>1)
#define lc k<<1
#define rc k<<1|1
using namespace std;
const int N=1e5+10;

int n,m;
int val[N],dp[N][2];
int head[N],to[N<<1],nxt[N<<1],tot;
int df[N],rv[N],ed[N],dfn;
int sz[N],top[N],f[N],son[N];

struct Mat{
    int g[2][2];
    Mat operator *(const Mat&ky)const{
        Mat re;
        re.g[0][0]=max(g[0][0]+ky.g[0][0],g[0][1]+ky.g[1][0]);
        re.g[0][1]=max(g[0][0]+ky.g[0][1],g[0][1]+ky.g[1][1]);
        re.g[1][0]=max(g[1][0]+ky.g[0][0],g[1][1]+ky.g[1][0]);
        re.g[1][1]=max(g[1][0]+ky.g[0][1],g[1][1]+ky.g[1][1]);
        return re;
    }
}t[N<<2],a[N],tp,C,D;

inline void lk(int u,int v)
{to[++tot]=v;nxt[tot]=head[u];head[u]=tot;}

void dfs(int x)
{
    sz[x]=1;dp[x][1]=val[x];
    for(int j,i=head[x];i;i=nxt[i]){
        j=to[i];if(j==f[x]) continue;
        f[j]=x;dfs(j);sz[x]+=sz[j];
        if(sz[j]>sz[son[x]]) son[x]=j;
        dp[x][1]+=dp[j][0];dp[x][0]+=max(dp[j][1],dp[j][0]);
    }
}

void dfss(int x,int tpo)
{
    top[x]=tpo;df[x]=++dfn;rv[dfn]=x;
    if(!son[x]) {ed[tpo]=dfn;return;}
    dfss(son[x],tpo);
    for(int j,i=head[x];i;i=nxt[i]){
        j=to[i];if(j==f[x] || j==son[x]) continue;
        dfss(j,j);
    }
}

void build(int k,int l,int r)
{
    if(l==r){
        int x=rv[l];
        a[l].g[0][0]=a[l].g[0][1]=dp[x][0]-max(dp[son[x]][0],dp[son[x]][1]);
        a[l].g[1][0]=dp[x][1]-dp[son[x]][0];
        t[k]=a[l];return;
    }
    build(lc,l,mid);build(rc,mid+1,r);
    t[k]=t[lc]*t[rc];
}

Mat ask(int k,int l,int r,int L,int R)
{
    if(L<=l && r<=R) return t[k];
    if(R<=mid) return ask(lc,l,mid,L,R);
    if(L>mid) return ask(rc,mid+1,r,L,R);
    return ask(lc,l,mid,L,R)*ask(rc,mid+1,r,L,R);
}

void modify(int k,int l,int r,int pos)
{
    if(l==r){t[k]=a[l];return;}
    if(pos<=mid) modify(lc,l,mid,pos);
    else modify(rc,mid+1,r,pos);
    t[k]=t[lc]*t[rc];
}

inline void chg(int x,int y)
{
    int z=df[x];
    a[z].g[1][0]+=(y-val[x]);val[x]=y;
    for(;;){
        C=ask(1,1,n,df[top[x]],ed[top[x]]);
        modify(1,1,n,z);
        D=ask(1,1,n,df[top[x]],ed[top[x]]);
        x=f[top[x]];if(!x) break;
        z=df[x];
        a[z].g[0][0]+=(max(D.g[0][0],D.g[1][0])-max(C.g[0][0],C.g[1][0]));
        a[z].g[0][1]=a[z].g[0][0];
        a[z].g[1][0]+=(D.g[0][0]-C.g[0][0]);
    }
}

int main(){
    int i,j,x,y;
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;++i) scanf("%d",&val[i]);
    for(i=1;i<n;++i){
        scanf("%d%d",&x,&y);
        lk(x,y);lk(y,x);
    }
    dfs(1);dfss(1,1);
    build(1,1,n);
    for(;m;--m){
        scanf("%d%d",&x,&y);
        chg(x,y);tp=ask(1,1,n,1,ed[1]);
        printf("%d\n",max(tp.g[0][0],tp.g[1][0]));
    }
    return 0;
}

全局平衡二叉树(global biased tree)

树剖虽然常数小,但复杂度终究是 O ( l o g 2 n ) O(log^2n) O(log2n)的。
LCT虽然复杂度是均摊 O ( l o g n ) O(logn) O(logn),但常数终究是大的。

每次都询问某个叶子结点到根节点的连乘积,这是一个关键的特殊性质。

由于树是静态的,所以考虑截取 L C T LCT LCT的部分操作,使用全局平衡二叉树将复杂度降到严格 O ( l o g n ) O(logn) O(logn)

考虑对于每条重链建一个 b s t bst bst,重链之间连上虚边。而建森林的时候将每个点加权使得其尽量平衡。

具体方法为:

对于一条重链,每个点的权值设为它的轻儿子的 s i z e size size之和+1(加不加一不重要,只是这样好算: v a l = s z [ x ] − s z [ s o n x ] val=sz[x]-sz[son_x] val=sz[x]sz[sonx]),找到带权重心后,把它作为这一层结点,递归左右区间建立左右儿子。

重链之间的虚边按照原树形态建就好了。

考虑这样建出来的树,每次向上跳一次(虚边/实边),轻儿子的总 s i z e size size至少翻一倍,所以树高是 l o g n logn logn的。

每个点维护自己的矩阵和 b s t bst bst内的子树连乘积,修改的同时更新上来即可(如果是虚边还需要改一下父亲的矩阵)。

代码

luoguP4751 动态dp【加强版】

#include<bits/stdc++.h>
#define gc getchar
using namespace std;
const int N=1e6+10;

int n,m,rt,ans;
int val[N],ch[N][2];
int head[N],to[N<<1],nxt[N<<1],tot;
int stk[N],tl,siz[N],sz[N],f[N],son[N];
bool vs[N];

char cp,OS[100];
inline void rd(int &x)
{
    cp=gc();x=0;int f=0;
    for(;!isdigit(cp);cp=gc()) if(cp=='-') f=1;
    for(;isdigit(cp);cp=gc()) x=(x<<3)+(x<<1)+(cp^48);
    if(f) x=-x;
}
 
inline void ot(int x)
{
    int re=0;
    for(;(!re)||(x);x/=10) OS[++re]='0'+x%10;
    for(;re;--re) putchar(OS[re]);
    putchar('\n');
}

struct Mat{
    int g[2][2];
    Mat(){g[0][0]=g[0][1]=g[1][0]=g[1][1]=-0x3f3f3f3f;}
    Mat operator *(const Mat&ky)const{
        Mat re;int i,j,k;
        for(i=0;i<2;++i)
         for(j=0;j<2;++j)
          for(k=0;k<2;++k) 
           re.g[i][j]=max(re.g[i][j],g[i][k]+ky.g[k][j]);
        return re;
    }
    inline void zs(int x){g[1][0]=x;g[0][0]=g[0][1]=0;}
    inline void itia(){g[1][1]=g[0][0]=0;}
}a[N],b[N];

inline void lk(int u,int v)
{to[++tot]=v;nxt[tot]=head[u];head[u]=tot;}

inline void pu(const int &x){b[x]=b[ch[x][0]]*a[x]*b[ch[x][1]];}

void dfs(int x,int fr)
{
    sz[x]=1;
    for(int j,i=head[x];i;i=nxt[i]){
        j=to[i];if(j==fr) continue;
        dfs(j,x);sz[x]+=sz[j];
        if(sz[j]>sz[son[x]]) son[x]=j;
    }
}

int sbuild(int l,int r)
{
    if(l>r) return 0;
    int pos=lower_bound(siz+l,siz+r+1,(siz[r]-siz[l-1]-1)/2+1+siz[l-1])-siz,x,lc,rc;
    x=stk[pos];
    lc=sbuild(l,pos-1);
    rc=sbuild(pos+1,r);
    f[lc]=f[rc]=x;ch[x][0]=lc;ch[x][1]=rc;pu(x);
    return x;
}

int build(int x)
{
    int k,u,i,j;
    for(k=x;k;k=son[k]) vs[k]=true;
    for(k=x;k;k=son[k])
     for(i=head[k];i;i=nxt[i]){
     	j=to[i];if(vs[j]) continue;
     	f[(u=build(j))]=k;
        a[k].g[0][0]+=max(b[u].g[0][0],b[u].g[1][0]);
        a[k].g[0][1]=a[k].g[0][0];
        a[k].g[1][0]+=b[u].g[0][0];
     }
    for(tl=0,i=x;i;i=son[i]) {
        stk[++tl]=i;
        siz[tl]=siz[tl-1]+sz[i]-sz[son[i]];
    }
    return sbuild(1,tl);
}

inline bool isr(int x){
return ((ch[f[x]][0]!=x)&&(ch[f[x]][1]!=x));}

inline void chg(int x,int y)
{
    a[x].g[1][0]+=(y-val[x]);val[x]=y;
    for(;x;x=f[x]){
        if(f[x]&&isr(x)){
            y=f[x];
            a[y].g[0][0]-=max(b[x].g[0][0],b[x].g[1][0]);
            a[y].g[1][0]-=b[x].g[0][0];pu(x);
            a[y].g[0][0]+=max(b[x].g[0][0],b[x].g[1][0]);
            a[y].g[1][0]+=b[x].g[0][0];
            a[y].g[0][1]=a[y].g[0][0];
        }else pu(x);
    }
}

int main(){
    int i,x,y;
    a[0].itia();b[0].itia();
    rd(n);rd(m);
    for(i=1;i<=n;++i) {rd(val[i]);a[i].zs(val[i]);}
    for(i=1;i<n;++i){rd(x);rd(y);lk(x,y);lk(y,x);}
    dfs(1,0);rt=build(1);
    for(;m;--m){
        rd(x);rd(y);x^=ans;
        chg(x,y);
        ot(ans=max(b[rt].g[0][0],b[rt].g[1][0]));
    }
    return 0;
}
    原文作者:平衡二叉树
    原文地址: https://blog.csdn.net/corsica6/article/details/84099758
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞