1. 程式人生 > 其它 >[圖論入門]虛樹

[圖論入門]虛樹

不知道虛樹到底應該放到 “演算法” 裡還是 “圖論” 裡...暫且當時圖論裡的吧。

#1.0 虛樹

#1.1 簡單介紹

虛樹,是對於一棵給定的節點數為 \(n\) 的樹 \(T\),構造一棵新的樹 \(T'\) 使得總結點數最小且包含指定的某幾個節點和他們的 \(\texttt{LCA}\)

一般用來優化樹形 \(\texttt{DP}\) 等。比如說,有一顆 \(n\) 個節點的樹,只有 \(m\) 個節點必須要考慮(\(m\) 可能遠小於 \(n\)),那麼我們完全可以不去考慮那些沒有任何用處的節點。像是樹形 \(\texttt{DP}\),我們產生貢獻、積累貢獻的地方有可能只有關鍵點與他們兩兩之間的 \(\texttt{LCA}\)

,那麼我們只需要存下關鍵點與 \(\texttt{LCA}\) 即可。

#1.2 實現思路

首先,將關鍵點按 \(\texttt{dfn}\) 序排序。然後我們需要一條單調棧來維護一條虛樹上的鏈,棧中的 \(\texttt{dfn}\) 序單調遞增。這意味著,棧中相鄰的節點在虛樹上也是相鄰的,且棧中一個節點屁股底下的就是自己在虛樹中的父親節點。

首先,為了方便後面處理,我們先強制把根節點加入棧中,那麼下面將依次將關鍵點加入棧。

第一種情況:當前節點與 stack[top]\(\texttt{LCA}\) 就是 stack[top] ,那麼說明他們在同一條鏈上,可以直接加入棧;

第二種情況:當前節點與棧頂元素的 \(\texttt{LCA}\)

不是棧頂元素,那麼說明他們不是同一條鏈上的(分叉了),將棧中的節點彈出並連邊,直到 stack[top - 1] 的深度比求得的 \(\texttt{LCA}\) 的深度要小,那麼說明異端岔開的鏈已經只剩棧頂一個了,檢查 stack[top - 1] 是否是 \(\texttt{LCA}\),如果不是,就需要將 \(\texttt{LCA}\) 向棧頂連一條邊,然後將棧頂彈出,加入 \(\texttt{LCA}\)。就可以將該節點加入棧了。

最後將棧中的節點全部彈出並連邊就好了。

注意一點,在虛樹建立後,進行操作的最後一個 \(\texttt{DFS}\) 的過程中,要清空鄰接表,但不要直接暴力 memset()

,那樣會使複雜度退化,可以直接將頭指標清空,如果是 vector 可以直接 erase()

#1.3 程式碼實現

inline void ins(int x){ //插入某個關鍵點
    if (!stp) {st[++ stp] = x;return;} //空棧,直接插入
    int ance = LCA(x,st[stp]);
    /*不在同一條鏈上的要彈出,在同一條鏈上的不會進入迴圈*/
    while (stp > 1 && d[ance] < d[st[stp - 1]])
      add(st[stp - 1],st[stp]),stp --;
    if (d[ance] < d[st[stp]]) add(ance,st[stp --]);
    if (!stp || st[stp] != ance) st[++ stp] = ance;
    st[++ stp] = x;
}

#2.0 例題

#2.1 P2495 [SDOI2011]消耗戰

普通的樹形 \(\texttt{DP}\) 並不難想,對於一個樹上的非關鍵點 \(x\),只有斷掉它連向父親的邊和斷掉連向子孫的邊兩種情況。斷上面的情況可以在其父節點處理,所以只用考慮斷連向子孫的邊。如果 \(x\) 的一個兒子是關鍵點,那麼這條邊必斷,如果一個兒子不是關鍵點,考慮是斷這個兒子上面還是下面,取最小值。

但是直接做時間複雜度爆炸,注意到 \(\sum k_i\leq5\times10^5\),再發現有許多點沒有用處,考慮建虛樹處理。

建完虛樹後,原來的 \(\texttt{DP}\) 策略似乎不太可行了,注意到在虛樹上的點除了關鍵點就是他們的 \(\texttt{LCA}\),且葉節點均為關鍵點。

發現,如果一個點是關鍵點,那麼,在它下面的關鍵點可以不加入虛樹,因為在該點之上必然存在刪除的邊,從而導致下面的點也不聯通。

按上面的想法建出虛樹,不妨設 \(f(x)\) 表示在虛樹上以 \(x\) 為根的子樹(不包含 \(x\))中的所有關鍵點與 \(1\) 號節點斷開需要的最小代價,顯然有

\[f(x)=\begin{cases}\min\left\{mn(x),\sum\limits_{y\in son_x}f(y)\right\},&|son_x|>0\\mn(x),&|son_x|=0\end{cases} \]

其中 \(mn(x)\) 表示從 \(x\)\(1\) 號節點路徑上的最小邊權。現在能有子節點的只有 \(\texttt{LCA}\) 了。考慮為什麼可以取 \(mn(x)\),對於 \(x\) 的父節點 \(fa_x\),如果連向 \(x\) 的邊 \((fa_x,x)\) 的邊權大於 \(mn(x)\),那麼由於和式其餘項相加大於等於 \(0\);所以必然仍取 \(mn(fa_x)\),如果 \((fa_x,x)=mn(x)\),正確性顯然。

const int N = 2000010;
const int INF = 0x3fffffff;

struct Edge{
    int u,v;
    ll w;
    int nxt;
};
Edge e[N],ne[N];

int cnt = 1,head[N],n,m,ncnt = 1,nhead[N];
int son[N],dfn[N],T,st[N],stp,f[N];
int fa[N],size[N],d[N],top[N],fl[N];
ll mn[N];

inline ll Min(const ll &a,const ll &b){
    return a < b ? a : b;
}

inline void ADD(const int &u,const int &v,const ll &w){
    e[cnt].u = u;e[cnt].v = v;e[cnt].w = w;
    e[cnt].nxt = head[u];head[u] = cnt ++;
}

inline void add(const int &u,const int &v){
    ne[ncnt].u = u;ne[ncnt].v = v;
    ne[ncnt].nxt = nhead[u];nhead[u] = ncnt ++;
}

inline int cmp(const int &a,const int &b){
    return dfn[a] < dfn[b];
}

inline void dfs1(int x,int _fa){
    size[x] = 1,d[x] = d[_fa] + 1,fa[x] = _fa;
    for (int i = head[x];i;i = e[i].nxt){
        if (e[i].v == fa[x]) continue;
        mn[e[i].v] = Min(mn[x],e[i].w);
        dfs1(e[i].v,x);
        size[x] += size[e[i].v];
        if (size[e[i].v] > size[son[x]])
          son[x] = e[i].v;
    }
}

inline void dfs2(int x,int t){
    top[x] = t;dfn[x] = ++ T;
    if (!son[x]) return;
    dfs2(son[x],t);
    for (int i = head[x];i;i = e[i].nxt){
        if (e[i].v == fa[x] || e[i].v == son[x])
          continue;
        dfs2(e[i].v,e[i].v);
    }
}

inline int LCA(int a,int b){
    while (top[a] != top[b]){
        if(d[top[a]] < d[top[b]])
          swap(a,b);
        a = fa[top[a]];
    }
    if (d[a] < d[b]) swap(a,b);
    return b;
}

inline void ins(int x){
    if (stp == 1) {st[++ stp] = x;return;}
    int ance = LCA(x,st[stp]);
    if (ance == st[stp]) return;
    while (stp > 1 && d[ance] < d[st[stp - 1]])
      add(st[stp - 1],st[stp]),stp --;
    if (d[ance] < d[st[stp]]) add(ance,st[stp --]);
    if (!stp || st[stp] != ance) st[++ stp] = ance;
    st[++ stp] = x;
}

inline ll DP(int x){
    if (!nhead[x]) return mn[x];
    ll res = 0;
    for (int i = nhead[x];i;i = ne[i].nxt)
      res += DP(ne[i].v);
    nhead[x] = 0;
    return Min(mn[x],res);
}

int main(){
    mset(mn,0x3f);scanf("%d",&n);
    for (int i = 1;i < n;i ++){
        int u,v;ll w;
        scanf("%d%d%lld",&u,&v,&w);
        ADD(u,v,w);ADD(v,u,w);
    }
    dfs1(1,0);dfs2(1,1);
    scanf("%d",&m);
    while (m --){
        int k,a[N];ncnt = 1;
        scanf("%d",&k);st[stp = 1] = 1;
        for (int i = 1;i <= k;i ++)
          scanf("%d",&a[i]);
        sort(a + 1,a + k + 1,cmp);
        for (int i = 1;i <= k;i ++) ins(a[i]);
        while (stp) add(st[stp - 1],st[stp]),stp --;
        printf("%lld\n",DP(1));
    }
    return 0;
}

參考資料

[1] 【洛穀日報#185】淺談虛樹 - SSerxhs

[2] 虛樹入門 - 自為風月馬前卒

[3] 虛樹 - OI Wiki