1. 程式人生 > >知識點:虛樹

知識點:虛樹

沒有 一定的 然而 一些事 記錄 turn 優先 push 只需要

目錄

  • 知識點概要
  • 知識點詳解
  • code

知識點概要

虛樹在競賽中出現的次數並不多,但其思想確實十分高妙的。對於一棵樹以及對於只涉及樹中一些關鍵點的詢問,我們只需利用這些關鍵點及關鍵點之間的\(LCA\)即可求出解。這便是虛樹的高妙之處,可以將復雜度優化到\(O(n*q)\)\((q\)為詢問次數\()\)級別。並且有些時候我們並不需要建出這棵虛樹,因為我們可以通過記錄\(Dfs\)時的信息來得到這整棵樹有用的信息。下面是\(luogu\)上某位\(dalao\)的高妙發言:

dfs原理
不知道大家發現一件事了沒有,我們關於"樹"的所有信息,絕大部分是通過dfs處理出來的,但是我們發現一件事,dfs其實在底層實現上,並不是大家腦海中想象的dfs。

當你的程序編譯出來之後,你覺得你在跑dfs,但是計算機並不這麽想,因為你甚至沒有建樹,實際上只有鄰接表而已,樹?不存在的,而你以為你在這個樹上進行了所謂的"深度優先搜索",而計算機並不這麽認為,它只是按一定的指令對一個棧進行了反復的push和pop,期間做一些事罷了(應該都知道遞歸函數的實現過程隱性的開了一個棧吧……)
水了這麽多,其實只是想說兩件事,第一,我們做dfs可以了解樹的信息,而且了解的很充分,第二,我們可以在不建樹的情況下做dfs,只要我們掌握了可以模擬dfs的信息即可,也就是說,在跑dfs的過程中,我們究竟對開出來的棧進行了什麽操作

知識點詳解

裸著講虛樹實際上確實比較的玄,接下來我們都以\(Bzoj2286\)

為例題來講。
考慮樸素的\(Dp\),我們假設沒有多組詢問,只處理一組詢問是,我們可以怎麽做?應該比較簡單的可以想到樹形\(Dp\)。對於每個點,我們記\(f[i]\)表示從\(i\)這個點到1,邊的最小權值。然後我們可以列出\(Dp\)方程:\(dp[i]=min(f[i],\sum dp[j])\)(\(j\)\(i\)的兒子)。這樣對於每一組的詢問,復雜度為\(O(n)\),總復雜度為\(O(n*m)\)
然後我們觀察一下這個轉移方程之後,發現對於某些點的\(Dp\)轉移實際上並沒有什麽作用,並且題目中說了\(\sum k \leq 500000\),通過這些信息,我們就可以考慮我們是否可以懟掉一些點,然後只通過選中的點進行\(Dp\)
並取得同樣的\(Dp\)結果,並且盡量最小化找到點的數量。這樣,虛樹的想法就應運而生了。
實際上,對於題目中的所給出的關鍵點,我們只需要知道這些點以及他們相互之間的\(LCA\)即可得到正確的答案了,而其他的點和邊就可以壓縮起來,然後我們就可以減少復雜度接近\(O(\sum k)\)級別的了。
但是我們又該如何求出兩兩關鍵點的\(LCA\)呢?顯然樸素的算法是不行的,然後我們就可以想到利用\(Dfs\)序了。首先我們將所有的關鍵點都拿出來,然後丟進一個棧中,每次新加進一個點的時候,就進行如下判斷(\(top\)表示棧頂元素,\(top-1\)表示棧中第二個元素,\(p\)表示新加進的點,\(Lca\)表示\(top\)\(p\)\(LCA\)):
1.如果\(Lca=top\),那麽說明新加進來的點與棧頂元素還是在同一顆子樹中,就可以直接把這個節點加入這個棧中。
2.如果\(Lca\neq top\),則說明新加進的節點與棧頂元素已經不再同一棵子樹中了,並且由於我們已經按照了\(dfn\)進行排序過了,所以這也說明了以\(Lca\)為根的子樹已經遍歷完畢了,現在開始我們需要重建所有在\(Lca\)節點一下的點之間的連邊。為什麽只要處理\(Lca\)以下的呢?還是因為我們之前按照\(dfn\)進行排序過了,所以當\(p\)\(top\)不在一棵子樹內的時候,就可以確定已經沒有任意兩個點的\(LCA\)在當前的\(Lca\)之下了。所以我們就需要對以\(Lca\)為根的子樹進行連邊了。然而連邊又有許多需要分類套路的地方:
1.\(dfn[top-1]>dfn[Lca]\)時,說明這個時候\(top-1\)仍然還在\(Lca\)下面,那麽我們直接在\(top-1\)\(top\)之間連一條邊,然後彈出棧頂元素就行了。
2.\(dfn[top-1]=dfn[Lca]\)時,說明這個時候\(top-1\)就是\(Lca\)了,那麽還是同樣的在\(top-1\)\(top\)之間連一條邊,彈出棧頂元素,然後就可以退出循環了(因為以\(Lca\)為根的子樹已經處理完了)。
2.\(dfn[top-1]<dfn[Lca]\)時,說明這個時候\(top-1\)已經在\(Lca\)上面了,那麽我們就可以在\(Lca\)\(top\)之間連邊,彈出棧頂元素,然後就可以退出循環了。
這樣我們就可以處理出對於每一個詢問只有與關鍵點有關的一棵虛樹了。但是註意我們之前說的,我們實際上並不需要把這棵樹建出來而得到這棵樹中的信息,所以秉持著極簡主義的我們就可以直接在構建虛樹的時候就完成這個\(Dp\)了。至於正確性,管他我們可以發現每一個點在出棧之後就不會再進棧了,說明這個點在出棧的時候已經完成了所有的連邊,那麽實際上我們就可以直接進行\(Dp\)轉移了。最後只需要預處理一下每個點到根路徑上的最小權值即可。
雖然虛樹理解比較難,代碼量也比較長,但是掌握了之後就會覺得比較簡單了,實際上就可以看作把一棵樹壓縮起來,註意處理好點與點之間的關系大致就沒有問題了。

code

#pragma GCC optimize (3)
#pragma GCC optimize ("inline")
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
bool Finish_read;
template<class T>inline void read(T &x){Finish_read=0;x=0;int f=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-')f=-1;if(ch==EOF)return;ch=getchar();}while(isdigit(ch))x=x*10+ch-'0',ch=getchar();x*=f;Finish_read=1;}
template<class T>inline void print(T x){if(x/10!=0)print(x/10);putchar(x%10+'0');}
template<class T>inline void writeln(T x){if(x<0)putchar('-');x=abs(x);print(x);putchar('\n');}
template<class T>inline void write(T x){if(x<0)putchar('-');x=abs(x);print(x);}
/*================Header Template==============*/
const int maxn=300000;
const int M=500005;
const ll inf=0x7f7f7f7f7f7f;
struct edge {
    int to,nxt,c;
}E[M];
int n,m,tot,dfnclck,q,top;
int head[maxn],dfn[maxn],p[maxn][22],deep[maxn],minn[maxn][22];
int k[maxn],isk[maxn];
ll f[maxn];
int st[maxn];
/*==================Define Area================*/
void Min(int &a,int b) {
    if(a>b) a=b;
}
 
void Min(ll &a,ll b) {
    if(a>b) a=b;
}
 
void addedge(int u,int v,int w) {
    E[++tot].to=v;E[tot].nxt=head[u];head[u]=tot;E[tot].c=w;
    E[++tot].to=u;E[tot].nxt=head[v];head[v]=tot;E[tot].c=w;
}
 
void dfs(int o,int fa,int dep) {
    dfn[o]=++dfnclck;deep[o]=dep;p[o][0]=fa;
    for(int i=head[o];~i;i=E[i].nxt) {
        int to=E[i].to;
        if(dfn[to]) continue;
        minn[to][0]=E[i].c;
        dfs(to,o,dep+1);
    }
}
 
int Lca(int a,int b) {
    if(deep[a]<deep[b]) swap(a,b);
    for(int i=20;~i;i--) if(deep[p[a][i]]>=deep[b]) a=p[a][i];
    for(int i=20;~i;i--) if(p[a][i]!=p[b][i]) a=p[a][i],b=p[b][i];
    return a==b ? a : p[a][0];
}
 
ll Getmin(int a,int b) {
    ll ans=inf;
    for(int i=20;~i;i--) {
        if(deep[p[a][i]]>=deep[b]) {
            Min(ans,minn[a][i]);
            a=p[a][i];
        }
    }
    return ans;
}
 
bool cmp(int a,int b) {
    return dfn[a]<dfn[b];
}
 
void Solve() {
    st[++top]=1;
    sort(k+1,k+1+q,cmp);
    for(int i=1;i<=q;i++) isk[k[i]]=1;
    for(int i=1;i<=q;i++) {
        int o=k[i];
        int L=Lca(o,st[top]);
        while(deep[L]<deep[st[top]]) {
            if(deep[st[top-1]]<=deep[L]) {
                f[L]+=min(isk[st[top]] ? inf : f[st[top]],Getmin(st[top],L));
                f[st[top]]=isk[st[top]]=0;
                top--;
                if(st[top]!=L) st[++top]=L;
                break;
            }
            else {
                f[st[top-1]]+=min(isk[st[top]] ? inf : f[st[top]],Getmin(st[top],st[top-1]));
                f[st[top]]=isk[st[top]]=0;
                top--; 
            }
        }
        if(st[top]!=o) st[++top]=o;
    }
    while(top>1) {
        f[st[top-1]]+=min(isk[st[top]] ? inf : f[st[top]],Getmin(st[top],st[top-1]));
        f[st[top]]=isk[st[top]]=0;
        top--;
    }
    printf("%lld\n",f[top--]);
    f[1]=0;
}
 
void Init() {
    dfs(1,0,1);
    for(int i=1;i<=20;i++) {
        for(int j=1;j<=n;j++) {
            p[j][i]=p[p[j][i-1]][i-1];
            minn[j][i]=min(minn[j][i-1],minn[p[j][i-1]][i-1]);
        }
    }
}
 
signed main() {
    // freopen("1.in","r",stdin);
    // freopen("3.out","w",stdout);
    memset(minn,0x3f,sizeof minn);
    memset(head,-1,sizeof head);
    read(n);
    for(int i=1;i<n;i++) {
        int u,v,w;
        read(u);read(v);read(w);
        addedge(u,v,w);
    }
    Init();
    read(m);
    for(int i=1;i<=m;i++) {
        read(q);
        for(int i=1;i<=q;i++) {
            read(k[i]);
        }
        Solve();
    }
    return 0;
}

知識點:虛樹