1. 程式人生 > >Link Cut Tree 總結

Link Cut Tree 總結

更新 net lai iostream 離線 引入 註釋 需要 都是

Link-Cut-Tree

Tags:數據結構

更好閱讀體驗:https://www.zybuluo.com/xzyxzy/note/1027479


一、概述

\(LCT\),動態樹的一種,又可以\(link\)又可以\(cut\)
引用:http://www.cnblogs.com/zhoushuyu/p/8137553.html

二、題目

初步

  • [x] P2147 [SDOI2008]Cave 洞穴勘測 https://www.luogu.org/problemnew/show/P2147
  • [x] P3690 【模板】Link Cut Tree https://www.luogu.org/problemnew/show/P3690
  • [x] P3203 [HNOI2010]彈飛綿羊 https://www.luogu.org/problemnew/show/P3203
  • [x] P2173 [ZJOI2012]網絡 https://www.luogu.org/problemnew/show/P2173
  • [x] P1501 [國家集訓隊]Tree II https://www.luogu.org/problemnew/show/P1501
  • [x] P4172 [WC2006]水管局長 https://www.luogu.org/problemnew/show/P4172
  • [x] P2387 [NOI2014]魔法森林 https://www.luogu.org/problemnew/show/P2387
  • [x] [HDU5398] GCD Tree https://vjudge.net/problem/HDU-5398
  • [x] [BZOJ4736]溫暖會指引我們前行 http://uoj.ac/problem/274
  • [x] P1505 [國家集訓隊]旅遊 https://www.luogu.org/problemnew/show/P1505
  • [x] P2542 [AHOI2005]航線規劃 https://www.luogu.org/problemnew/show/P2542
  • [x] P2486 [SDOI2011]染色 https://www.luogu.org/problemnew/show/P2486

    進階

  • [x] [BZOJ4998]星球聯盟 http://www.lydsy.com/JudgeOnline/problem.php?id=4998
  • [x] [BZOJ2959]長跑 http://www.lydsy.com/JudgeOnline/problem.php?id=2959
  • [x] [BJOI2014]大融合 https://loj.ac/problem/2230
  • [x] [COGS2701]動態樹 http://cogs.pro:8080/cogs/problem/problem.php?pid=2701
  • [ ] P3703 [SDOI2017]樹點塗色 https://www.luogu.org/problemnew/show/P3703
  • [ ] [UOJ207]共價大爺遊長沙 http://uoj.ac/problem/207

    變態

  • [ ] P3721 [AH2017/HNOI2017]單旋 https://www.luogu.org/problemnew/show/P3721
  • [ ] P3613 睡覺困難綜合征 https://www.luogu.org/problemnew/show/P3613
  • [ ] [BZOJ3626][LNOI2014]LCA https://ruanx.pw/bzojch/p/3626.html
  • [ ] [BZOJ3514]Codechef MARCH14 GERALD07加強版 https://ruanx.pw/bzojch/p/3514.html
  • [ ] [THUWC 2017]在美妙的數學王國中暢遊 https://loj.ac/problem/2289


三、支持操作

I 維護聯通性

維護兩點聯通性,較易,例題:Cave 洞穴勘測

II 維護樹鏈信息

正是由於這個LCT可以代替樹鏈剖分
運用\(split\)操作把\(x\)\(y\)這條鏈摳出來操作
例題:【模板】Link Cut Tree
這是\(LCT\)的最大作用之一,幾乎在每道題中都有體現
PS:樹剖的常數小且相對容易調試,建議能寫樹剖則寫(如“初步”的後三題,沒有刪邊操作)

III 維護生成樹

例題:“初步”中水管局長到溫暖會指引我們前行

這裏較為重要,理解需要時間

引入:一條路徑的權值定義為該路徑上所有邊的邊權最大值,問x到y的所有路徑中,路徑權值最小的路徑的權值是多少,要求支持加邊或刪邊,\(O(nlogn)\)求解
解決
  • 要求支持加邊,那麽每構成一個環就把環內最大邊刪掉,若支持刪邊則離線逆序處理
  • 化邊為點,每個\(splay\)節點記錄\({fa,ch[2],rev,val,id,d1,d2}\),分別表示父親,孩子,翻轉標記,該點權值(如果該點為邊則為邊權,如果為點那麽最大生成樹中值為\(inf\),最小生成樹中值為\(-inf\)),在該節點所在的\(splay\)中、以該節點為根的子樹中權值最大(小)的點的編號,(若該節點表示邊)與該邊相連的兩個點的編號
  • 加入一條邊\((x,y)\)的時候,判斷\(x,y\)是否聯通,若聯通,\(split(x,y)\),判斷這條路徑上的邊權最大值(最小值)和所加入的邊的邊權的關系,再決定\(continue\)\(cut\)\(link\)

    pushup片段
    int Getmax(int x,int y){return t[x].val>t[y].val?x:y;}
    void pushup(int x){t[x].id=Getmax(x,Getmax(t[lc].id,t[rc].id));}

IV 維護邊雙聯通分量

例題:星球聯盟、長跑

這裏難懂,慢慢體會

解釋

邊雙聯通,其實就是說有兩條不想交的路徑可以到達
這裏表述也不是特別清楚,這兩道題的意思是————把環縮點
兩道題一句話題意:求x,y路徑上點(超級點)的siz(val)之和

實現

類似於\(Tarjan\)縮點,遇到環,暴力DFS把所有點指向一個標誌點
在之後凡要用到一個點就x=f[x]
相當於踏入這個環就改成踏進這個超級點
能夠保證\(DFS\)總復雜度為\(O(n)\)(雖然星球聯盟暴力不縮點也可以過)

核心代碼片段
//並查集find
int find(int x){return f[x]==x?x:f[x]=find(f[x]);}
//讀進來的時候就改成超級點
int x=read(),y=read();x=find(x);y=find(y);
//goal為超級點
void DFS(int x,int goal)
{
    if(lc)DFS(lc,goal);
    if(rc)DFS(rc,goal);
    if(x!=goal){f[x]=goal;siz[goal]+=siz[x];}
}
//每次訪問點的時候都訪問其find
void rotate(int x)
{
    int y=find(t[x].fa),z=find(t[y].fa);
    ...
}
void Access(int x){for(int y=0;x;y=x,x=find(t[x].fa)){splay(x);t[x].ch[1]=y;pushup(x);}}
...

V 維護原圖信息

例題:大融合、動態樹

難懂,煩請細細品味

解釋
  • 先知道這幾個名詞和性質:
  • A、實兒子:\(x\)\(splay\)中的兒子
  • B、虛兒子:與\(x\)在原圖中有直接連邊但和\(x\)不在同一棵\(splay\)
  • C、若在原圖中\(x\)\(y\)的父親,且\(x\)\(y\)不在同一棵\(splay\)中,那麽\(y\)所在的\(splay\)的根的父親指向\(x\)
  • 再知道這幾個要點:
  • A、\(x\)與其實兒子在原圖中不一定有直接連邊
  • B、上文講到的維護樹鏈的信息都是維護實兒子的信息
  • C、\(x\)的實兒子信息包括了實兒子的虛兒子和實兒子的實兒子
  • 那麽在原圖中的子樹信息就可以這樣求:Access(x)後返回x虛兒子的信息
實現

\(Access\)的目的是使得x沒有實兒子,那麽虛兒子便是原子樹的信息
因為\(x\)的實兒子中有可能有點是原圖中的兒子,那麽只算虛兒子會算不全,都算會多算
以維護\(siz\)為例:
記錄每個點的\(Rs\)表示虛兒子信息,\(siz\)表示實兒子和虛兒子的信息
需要改動的地方只有\(Access\)\(link\)

核心代碼片段
//要改變的兩個操作
void Access(int x)
{
    for(int y=0;x;y=x,x=t[x].fa)
    {
        splay(x);
        t[x].Rs=t[x].Rs+t[rc].siz-t[y].siz;//把一個實兒子變成虛兒子要+t[rx].siz,把一個虛兒子變成實兒子要-t[y].siz
        rc=y;pushup(x);
    }
}
void link(int x,int y){makeroot(x);makeroot(y);t[x].fa=y;t[y].Rs+=t[x].siz;}//link要makeroot(y)因為連上x後y到該棵splay的根都有影響

四、做題經驗

1、辨別

如何看出一道題要用\(LCT\)————動態加/刪邊!

2、常數

只有加邊操作時,維護兩點是否聯通請用並查集
\(findroot\)在以下題目會TLE:溫暖會指引我們前行、長跑

代碼

Luogu LCT模板

// luogu-judger-enable-o2
//註釋詳盡版本
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<set>
using namespace std;
int read()
{
    char ch=getchar();
    int h=0;
    while(ch>‘9‘||ch<‘0‘)ch=getchar();
    while(ch>=‘0‘&&ch<=‘9‘){h=h*10+ch-‘0‘;ch=getchar();}
    return h;
}
const int MAXN=300001;
set<int>Link[MAXN];
int N,M,val[MAXN],zhan[MAXN],top=0;
struct Splay{int val,sum,rev,ch[2],fa;}t[MAXN];
void Print()
{
    for(int i=1;i<=N;i++)
        printf("%d:val=%d,fa=%d,lc=%d,rc=%d,sum=%d,rev=%d\n",i,t[i].val,t[i].fa,t[i].ch[0],t[i].ch[1],t[i].sum,t[i].rev);
}
void pushup(int x)//向上維護異或和
{
    t[x].sum=t[t[x].ch[0]].sum^t[t[x].ch[1]].sum^t[x].val;//異或和
}
void reverse(int x)//打標記
{
    swap(t[x].ch[0],t[x].ch[1]);
    t[x].rev^=1;//標記表示已經翻轉了該點的左右兒子
}
void pushdown(int x)//向下傳遞翻轉標記
{
    if(!t[x].rev)return;
    if(t[x].ch[0])reverse(t[x].ch[0]);
    if(t[x].ch[1])reverse(t[x].ch[1]);
    t[x].rev=0;
}
bool isroot(int x)//如果x是所在鏈的根返回1
{
    return t[t[x].fa].ch[0]!=x&&t[t[x].fa].ch[1]!=x;
}
void rotate(int x)//Splay向上操作
{
    int y=t[x].fa,z=t[y].fa;
    int k=t[y].ch[1]==x;
    if(!isroot(y))t[z].ch[t[z].ch[1]==y]=x;//Attention if()
    t[x].fa=z;//註意了
    /*
      敲黑板:這個時候y為Splay的根,把x繞上去後
      x的父親是z!表示這個splay所表示的原圖中的鏈的鏈頂的父親
      這正是splay根的父親表示的是鏈頂的父親的集中體現!
     */
    t[y].ch[k]=t[x].ch[k^1];t[t[x].ch[k^1]].fa=y;
    t[x].ch[k^1]=y;t[y].fa=x;
    pushup(y);
}
void splay(int x)//把x弄到根
{
    zhan[++top]=x;
    for(int pos=x;!isroot(pos);pos=t[pos].fa)zhan[++top]=t[pos].fa;
    while(top)pushdown(zhan[top--]);
    while(!isroot(x))
    {
        int y=t[x].fa,z=t[y].fa;
        if(!isroot(y))
            /*
              這個地方和普通Splay有所不同:
              普通的是z!=goal,z不是根的爸爸
              這個是y!=root,y不是根
              所以實質是一樣的。。。
             */
            (t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y);
        rotate(x);
    }
    pushup(x);
}
void Access(int x)
{
    for(int y=0;x;y=x,x=t[x].fa){splay(x);t[x].ch[1]=y;pushup(x);}
    /*
      Explaination:
      函數功能:把x到原圖的同一個聯通塊的root弄成一條鏈,放在同一個Splay中
      首先令x原先所在splay的最左端(x所在鏈的鏈頂)為u
      那麽x-u一定保留在x-root的路徑中,那麽直接斷掉x的右兒子
      然後y是上一個這麽處理的鏈的Splay所在的根
      在之前,y向x連了一條虛邊(y的fa是x,x的ch不是y)
      那麽只要化虛為實就可以了
     */
}
void makeroot(int x)//函數功能:把x拎成原圖的根
{
    Access(x);splay(x);//把x和根先弄到一起
    reverse(x);//然後打區間翻轉標記,應該在根的地方打但是找不到根所以要splay(x)
    /*
      這裏很神奇的一個區間翻轉標記,那麽從上往下是root-x,翻轉完區間就是x-root
      這樣子相當於(這裏打一個神奇的比喻)
      一根棒子上面有一些平鋪的長毛,原先是向上拉,區間翻轉後就向下拉
         |            ↑            |
     ----|----       /|\        \ \|/ /
     ----|----      / | \        \ | /
     ----|----     / /|\ \      \ \|/ /
     ----|----      / | \        \ | /
     ----|----     / /|\ \      \ \|/ /
     ----|----      / | \        \ | /
     ----|----     / /|\ \        \|/
         |            |            ↓
      哈哈哈誇我~
     */
}
int Findroot(int x)//函數功能:找到x所在聯通塊的splay的根
{
    Access(x);splay(x);
    while(t[x].ch[0])x=t[x].ch[0];
    return x;
}
void split(int x,int y)//函數功能:把x到y的路徑摳出來
{
    makeroot(x);//先把x弄成原圖的根
    Access(y);//再把y和根的路徑弄成重鏈
    splay(y);//那麽就是y及其左子樹存儲的信息了
    /*
      關於這裏為什麽要splay(y):
      可以發現,makeroot後x為splay的根
      但是Access之後改變了根(這就是為什麽凡是Access都後面跟了splay)
      所以要找到根最方便就是splay,至於splayx還是y,都可以
     */
}
void link(int x,int y)//函數功能:連接x,y所在的兩個聯通塊
{
    makeroot(x);//把x弄成其聯通塊的根
    t[x].fa=y;//連到y上(虛邊)
    Link[x].insert(y);Link[y].insert(x);
}
void cut(int x,int y)//函數功能:割斷x,y所在的兩個聯通塊
{
    split(x,y);
    t[y].ch[0]=t[x].fa=0;
    Link[x].erase(y);Link[y].erase(x);
    /*
      這裏會出現一個這樣的情況:
      圖中x和y並未直接連邊,但是splay中有可能直接相連
      所以一定要用set(map會慢)維護實際的連邊
      不然會出現莫名錯誤(大部分數據可以水過去,但是subtask...)
     */
}
int main()
{
    N=read();M=read();
    for(int i=1;i<=N;i++)
        t[i].sum=t[i].val=read();//原圖中結點編號就是Splay結點編號
    for(int i=1;i<=M;i++)
    {
        int op=read(),x=read(),y=read();
        if(op==0)//x到y路徑異或和
        {
            split(x,y);//摳出路徑
            printf("%d\n",t[y].sum);
        }
        if(op==1)//連接x,y
        {
            if(Findroot(x)^Findroot(y))
                link(x,y);//x,y不在同一聯通塊裏
        }
        if(op==2)//割斷x,y
        {
            if(Link[x].find(y)!=Link[x].end())
                cut(x,y);//x,y在同一聯通塊
        }
        if(op==3)//把x點的權值改成y
        {
            Access(x);//把x到根的路徑設置為重鏈
            splay(x);//把x弄到該鏈的根結點
            t[x].val=y;
            pushup(x);//直接改x的val並更新
        }
        //printf("i=%d\n",i);
        //Print();
    }
    return 0;
}

Link Cut Tree 總結