動態DP之全局平衡二叉樹


前置知識

在學習如何使用全局平衡二叉樹之前,你首先要知道如何使用樹鏈剖分解決動態DP問題。這里僅做一個簡單的回顧,建議在有一定基礎的情況下看。

首先,維護序列的動態DP我們就不說了,這里只討論樹上的動態DP問題。

然后,目前個人感覺,動態DP往往有一些奇怪的特征。
一般問題是支持動態修改某一個點的權值,以及詢問根節點的(也就是全局的)或者是某一個子樹的DP值。
而通常是從靜態的情況下入手,寫出一個結構簡單的DP轉移式,然后將其中和輕兒子以及子樹的根有關的項提出來,然后得到了當前的根和重兒子之間的轉移式。有了這個之后,我們就在全局維護每一個子樹的根的輕兒子的信息,然后,比如說樹鏈剖分,就是將一條重鏈上的信息全部快速總和起來,就能夠得到這個子樹的答案了。

至於將信息合並這一步,還有一些細節。

首先,將有重兒子轉移過來的DP式展開之后,要能夠形成一個比較簡單的形式。每一個子樹的根由重兒子轉移過來的形式必須是一樣的。
然后,如果是線性的式子的話,往往是采用矩陣乘法來進行信息的總和(而往往又采用直接寫轉移的方式來減小常數);而如果是其它的情況的話,往往又是前綴和,后綴和,或者是所有區間的和、積之類的。

這里說的比較籠統,大家將就理解一下吧~

全局平衡二叉樹

大致介紹

在對使用樹鏈剖分解決動態DP問題比較熟悉之后,再來看這個東西就比較好理解了。

因為樹鏈剖分是\(O(n \log^2 n)\)的,所以有可能被卡。而我們熟知的\(O(n \log n)\)的LCT又往往加上常數后比樹剖還慢...那么有什么既是\(O(n \log n)\)的,常數又相對較小的方法呢?這個時候全局平衡二叉樹就出現了。

它其實和樹鏈剖分很像,都是對於一根重鏈要特殊處理。下面將詳細介紹如何對於一顆給定的樹求出這樣子的一個全局平衡二叉樹。

首先是一個大致的思路,就是對於一根重鏈而言,將它維護成一棵每一個節點代表一個區間的平衡二叉樹(至於根據什么信息來使它平衡,后面再說),然后和樹鏈剖分一樣,將原圖中一個點的所有輕兒子還是接到它自己這個點上。相當於整個圖並不是一個嚴格的平衡二叉樹,只有對於某一根重鏈而言,它才是一棵二叉樹。

建圖過程

下面正式開始講構圖的過程:

  1. 跑一遍DFS,預處理出每一個節點的重兒子、子樹大小、輕子樹大小(即\(1+\sum_{v\in lightson[u]}siz[v]\),記為\(lsiz[u]\));
  2. 從根節點(假定是1號節點)開始遍歷,首先將以當前點為頂端的重鏈整個提出來,先下去處理這根鏈上所有節點的所有輕子樹,然后根據之前所說的,將輕子樹接到它們對應的父節點上去就可以了。(此過程中只需要記錄\(treefa[]\))就可以了。在這個過程中,將輕兒子的信息統計到存在子樹根的某個數據結構上就可以了(這里假定是矩陣,記為\(matr1[]\)
  3. 然后再來看如何處置當前的這根重鏈。把當前的這根重鏈看成是一個區間,即一個序列。然后在這個序列上一直做類似於點分治一樣的算法,也就是不斷的找重心。但是注意了,這里的重心是在\(lsiz[]的定義下的\)。因為是在序列上找重心,我們只需要從左到右枚舉,找到一個類似於帶權中點的東西就可以了;
  4. 在建立這棵二叉樹的過程中,注意將一個點的左右兒子的信息PushUp上來,同時還需要注意上傳信息時“計算的方向”(尤其是做矩陣乘法,因為它沒有交換律)。然后這個就相當於是維護的一個區間的信息了,存在當前這個點的另一個數據結構里面(這里還是假定是矩陣,記為\(matr2[]\))。

到這里,整個全局平衡二叉樹就建好了。再次強調,matr1[]存的是輕兒子以及自己的信息,而matr2[]存的是對於某一根重鏈上的區間的信息。

修改過程

加入我們當前要把第x個點的權值修改為v,那么我們來看是如何進行操作的:

  1. 首先將x這個點自己的信息修改了,但是只修改matr1[];
  2. 然后模仿樹剖,一步一步往上跳,只不過這里是真的"一步一步往上跳"。假如當前節點為p,假如treefa[p]到p這條邊是輕邊的話,在修改對當前點做PushUp(這是用來合並區間信息的)之前,先把當前點對於treefa[p]原來的貢獻先去掉(這里往往是根據矩陣的構造方式直接進行修修改,而不要想着什么矩陣除法之類的...),然后對當前的點做PushUp,然后在將新的貢獻假如到treefa[p]中;否則的話,就直接對於當前的點做PushUp就可以了。

然后這里就做完了修改操作。

詢問過程

詢問這里分為兩種,一種是詢問全局的,也就是整棵樹的根的DP信息,那么這個時候就直接將根(1號節點)所在重鏈的二叉樹的根所維護的區間信息直接拿出來用就好了。

而第二種情況,也就是詢問某一個子樹的DP信息的時候,就稍微麻煩一點。

大致的思想還是,模仿樹剖,在這個點所在的重鏈序列上,將它及它下面的鏈上的點信息合並上來即可。

畫個圖:
對於某一重鏈的二叉樹
圖中紅色的部分就是需要統計的信息。觀察之后,可以發現,只有當x!=ch[treefa[x]][1]時,treefa[x]以及ch[treefa[x]][1]的信息才需要被統計。

這個可以根據平衡樹的性質自行推倒的。

時間復雜度的證明

分成兩部分進行考慮:

首先是輕邊,根據重兒子的定義,很顯然,向下走一層,子樹的大小至少會減少一半;

然后是重邊,由於我們是找的重心,那么lsiz[]也至少會減少一半。

根據以上偽證,我們可以發現這個東西是大致\(O(n \log n)\)的...

而實測起來雖然每道題是要比樹剖快一點,但是大多數情況下都差不多...但至少能夠保證絕對不會比樹剖慢...

有人說代碼復雜度差不多,但是我覺得,以我菜雞的實現能力來看,代碼和樹剖的代碼長度差不多一樣的...

板題

既然樹剖解決動態DP的板題是洛谷 P4719 【模板】動態dp,那么我們全局平衡二叉樹的板題就是洛谷 P4751 動態dp【加強版】啦~

下面是這道題的代碼,以及一些批注。至於矩陣長啥樣,可以參考一下其他博主的樹剖的矩陣,長得一模一樣...,就懶得推了...

#include<cstdio>
#include<cstring>
#include<algorithm>
#define MAXN 1000000
#define MAXM 3000000
#define INF 0x3FFFFFFF
using namespace std;
struct edge
{
    int to;
    edge *nxt;
}edges[MAXN*2+5];
edge *ncnt=&edges[0],*Adj[MAXN+5];
int n,m;
struct Matrix
{
    int M[2][2];
    Matrix operator * (const Matrix &B)
    {
        static Matrix ret;
        for(int i=0;i<2;i++)
            for(int j=0;j<2;j++)
            {
                ret.M[i][j]=-INF;
                for(int k=0;k<2;k++)
                    ret.M[i][j]=max(ret.M[i][j],M[i][k]+B.M[k][j]);
            }
        return ret;
    }
}matr1[MAXN+5],matr2[MAXN+5];//每個點維護兩個矩陣
int root;
int w[MAXN+5],dep[MAXN+5],son[MAXN+5],siz[MAXN+5],lsiz[MAXN+5];
int g[MAXN+5][2],f[MAXN+5][2],trfa[MAXN+5],bstch[MAXN+5][2];
int stk[MAXN+5],tp;
bool vis[MAXN+5];
void AddEdge(int u,int v)
{
    edge *p=++ncnt;
    p->to=v;p->nxt=Adj[u];Adj[u]=p;
    
    edge *q=++ncnt;
    q->to=u;q->nxt=Adj[v];Adj[v]=q;
}
void DFS(int u,int fa)
{
    siz[u]=1;
    for(edge *p=Adj[u];p!=NULL;p=p->nxt)
    {
        int v=p->to;
        if(v==fa)
            continue;
        dep[v]=dep[u]+1;
        DFS(v,u);
        siz[u]+=siz[v];
        if(!son[u]||siz[son[u]]<siz[v])
            son[u]=v;
    }
    lsiz[u]=siz[u]-siz[son[u]];//輕兒子的siz和+1
}
void DFS2(int u,int fa)
{
    f[u][1]=w[u],f[u][0]=0;
    g[u][1]=w[u],g[u][0]=0;
    if(son[u])
    {
        DFS2(son[u],u);
        f[u][0]+=max(f[son[u]][0],f[son[u]][1]);
        f[u][1]+=f[son[u]][0];
    }		 
    for(edge *p=Adj[u];p!=NULL;p=p->nxt)
    {
        int v=p->to;
        if(v==fa||v==son[u])
            continue;
        DFS2(v,u);
        f[u][0]+=max(f[v][0],f[v][1]);//f[][]就是正常的DP數組
        f[u][1]+=f[v][0];
        g[u][0]+=max(f[v][0],f[v][1]);//g[][]數組只統計了自己和輕兒子的信息
        g[u][1]+=f[v][0];
    }
}
void PushUp(int u)
{
    matr2[u]=matr1[u];//matr1是單點加上輕兒子的信息,matr2是區間信息
    if(bstch[u][0])
        matr2[u]=matr2[bstch[u][0]]*matr2[u];
    //注意轉移的方向,但是如果我們的矩乘定義不同,可能方向也會不同
    if(bstch[u][1])
        matr2[u]=matr2[u]*matr2[bstch[u][1]];
}
int getmx2(int u)
{
    return max(matr2[u].M[0][0],matr2[u].M[0][1]);
}
int getmx1(int u)
{
    return max(getmx2(u),matr2[u].M[1][0]);
}
int SBuild(int l,int r)
{
    if(l>r)
        return 0;
    int tot=0;
    for(int i=l;i<=r;i++)
        tot+=lsiz[stk[i]];
    for(int i=l,sumn=lsiz[stk[l]];i<=r;i++,sumn+=lsiz[stk[i]])
        if(sumn*2>=tot)//是重心了
        {
            int lch=SBuild(l,i-1),rch=SBuild(i+1,r);
            bstch[stk[i]][0]=lch;bstch[stk[i]][1]=rch;
            trfa[lch]=trfa[rch]=stk[i];
            PushUp(stk[i]);//將區間的信息統計上來
            return stk[i];
        }
    return 0;
}
int Build(int u)
{
    for(int pos=u;pos;pos=son[pos])
        vis[pos]=true;
    for(int pos=u;pos;pos=son[pos])
        for(edge *p=Adj[pos];p!=NULL;p=p->nxt)
            if(!vis[p->to])//是輕兒子
            {
                int v=p->to,ret=Build(v);
                trfa[ret]=pos;//輕兒子的treefa[]接上來
            }
    tp=0;
    for(int pos=u;pos;pos=son[pos])
        stk[++tp]=pos;//把重鏈取出來
    int ret=SBuild(1,tp);//對重鏈進行單獨的SBuild(我猜是Special Build?)
    return ret;//返回當前重鏈的二叉樹的根
}
void Modify(int u,int val)
{
    matr1[u].M[1][0]+=val-w[u];
    w[u]=val;
    for(int pos=u;pos;pos=trfa[pos])
        if(trfa[pos]&&bstch[trfa[pos]][0]!=pos&&bstch[trfa[pos]][1]!=pos)
        {
            matr1[trfa[pos]].M[0][0]-=getmx1(pos);
            matr1[trfa[pos]].M[0][1]=matr1[trfa[pos]].M[0][0];
            matr1[trfa[pos]].M[1][0]-=getmx2(pos);
            PushUp(pos);
            matr1[trfa[pos]].M[0][0]+=getmx1(pos);
            matr1[trfa[pos]].M[0][1]=matr1[trfa[pos]].M[0][0];
            matr1[trfa[pos]].M[1][0]+=getmx2(pos);
        }
        else
            PushUp(pos);
}
inline int read()
{
    int ret=0,f=1;char c=0;
    while(c<'0'||c>'9'){c=getchar();if(c=='-')f=-f;}
    ret=10*ret+c-'0';
    while(true){c=getchar();if(c<'0'||c>'9')break;ret=10*ret+c-'0';}
    return ret*f;
}
inline void print(int x)
{
    if(x==0)	return;
    print(x/10);putchar(x%10+'0');
}
int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
        w[i]=read();
    int u,v;
    for(int i=1;i<n;i++)
    {
        u=read(),v=read();
        AddEdge(u,v);
    }
    DFS(1,-1);
    //求重兒子
    DFS2(1,-1);
    //求初始的DP值,也可以在Build()里面求,但是這樣寫就和樹剖的寫法統一了
    for(int i=1;i<=n;i++)
    {
        matr1[i].M[0][0]=matr1[i].M[0][1]=g[i][0];
        matr1[i].M[1][0]=g[i][1],matr1[i].M[1][1]=-INF; //初始化矩陣
    }
    root=Build(1);//root即為根節點所在重鏈的重心
    int lastans=0;
    for(int i=1;i<=m;i++)
    {
        u=read(),v=read();
        u^=lastans;//強制在線
        Modify(u,v);
        lastans=getmx1(root);//直接取值
        if(lastans==0)	putchar('0');
        else			print(lastans);
        putchar('\n');
    }
    return 0;
}

希望能夠對你有所幫助!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM