1. 程式人生 > 實用技巧 >「虛樹」學習筆記

「虛樹」學習筆記

虛樹

虛樹的定義

虛樹:將樹上有用的節點建立新的圖,而捨去關鍵節點之間的沒有用處的節點
虛樹的用途:對於一些有關鍵點的圖而言,其餘沒有用處的節點在操作的時候會作出很多的冗餘操作,時間效率大大降低,而利用虛樹建圖就可以捨去沒有用的操作

前置知識1:\(dfs\)

\(dfs4序,顧名思義,就是在對圖做\)dfs\(的時候的順序。 舉個例子: ![](https://img2020.cnblogs.com/blog/1999076/202010/1999076-20201007162945768-640298406.png) 該圖中節點就是按\)dfs\(序編號的 我們可以利用\)dfs\(序找到一些很有用的性質: 1.\)dfs\(序較大的有兩種情況,一種是\)

dfs\(序大的在\)dfs\(序小的的子樹中,另一種是兩個點不再一顆子樹中(好像是廢話,樹上的點不都是這樣嗎)。 2.\)dfs\(序有連續性,在一個\)dfs$序小的節點後一段都在該節點的子樹中,在後面的建圖中用處很大。

來道例題(\(CF613D\; Kingdom \; and\; its \;Cities\)

題意:給定一棵樹, \(q\) 組詢問,每組詢問給定 \(k\) 個點,你可以刪掉不同於那 \(k\) 個點的 \(m\) 個點,使得這 \(k\) 個點兩兩不連通,要求最小化 \(m\),如果不可能輸出 −1。詢問之間獨立。
思路:
首先如果兩個節點都是關鍵點,並且兩個點相鄰,那麼就是無解的情況,否則都有解。那麼怎麼求最小的 \(m\)

呢?
一種方法可以暴力遍歷全圖,兩個節點之間只斷一個點,選擇那種可以切掉一個點可以將多個點都斷開連線的,比如這種:

我們只把1節點刪去就可以達到所有點都不聯通的目的。
暴力做的話,時間複雜度並不是很優秀。我們考慮只用關鍵點和一些必要的公共祖先去建樹,那麼虛樹的關鍵就在於如何去利用 \(dfs\) 序建圖。
首先對於關鍵點用 \(dfs\) 序排序,如果根節點不是關鍵點,把根節點也加進去。
當棧為空或棧中只有一個元素(即 \(top\) <=1, \(top\) 從0開始),直接把x壓入棧中
維護一個棧,顯然 \(dfs\) 序小的節點先進棧,記住, \(dfs\) 序小的在棧底。
如果 \(dfs\)
序大的節點在 \(dfs\) 序小的節點(即棧頂)的子樹中,那麼就直接扔進棧裡。

否則該節點就是在新的子樹中,是這種情況:

判斷依據就是看將當前點和棧頂的 \(lca\) 是不是棧頂元素,也就是圖中當前節點9和棧頂節點8的 \(lca\) 是不是8,如果是,那麼就直接推進棧裡;
不是的話,說明 \(x\)\(stk[top]\) 分屬 \(lca\) 的兩棵不同的子樹,而且\(stk[top]\)所在的子樹中已經構建完成了。所以我們把 \(lca\)\(stk[top]\) 所在的子樹彈棧,在彈棧的過程中建邊,直到 \(dfn[stk[top]]<=dfn[lca]<=dfn[stk[top-1]]\)(即\(lca\)在棧頂的兩元素的路徑上),或者棧內的元素小於兩個,可以自己模擬一下。
此時我們看\(lca\)是不是棧頂元素,如果是的話,將當前節點進棧,如果不是的話,從棧頂向\(lca\)連邊,彈出棧頂,將\(lca\)壓進棧,並將當前節點也進棧。
在列舉完關鍵點後,將棧內剩餘元素都建邊,彈棧。此時虛樹已經建好了,就可以用之前的做法在虛樹上操作了。
建圖程式碼(細品):

inline void ins(int x){
    if (tp == 0){
        stk[tp = 1] = x;
        return;
    }
    int LCA = lca(stk[tp], x);
    while ((tp > 1) && (deep[LCA] < deep[stk[tp - 1]])) {
        addedge(stk[tp - 1], stk[tp]);
        --tp;
    }
    if (deep[LCA] < deep[stk[tp]]) addedge(LCA, stk[tp--]);
    if ((!tp) || (stk[tp] != LCA)) stk[++tp] = LCA;
    stk[++tp] = x;
}

大體程式碼實現:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 50;
inline int read () {
    int x = 0, f = 1; char ch = getchar();
    for (;!isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
    for (; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
    return x * f;
}
int n, m, q;
struct Edge {
    int to, next;
} edge[maxn << 1];
int tot, head[maxn];
void addedge (int a, int b) {
    edge[++tot].to = b;
    edge[tot].next = head[a];
    head[a] = tot;
}
int siz[maxn], fa[maxn], deep[maxn], son[maxn];
void dfs1 (int u) {
    siz[u] = 1;
    for (register int i = head[u]; i; i = edge[i].next) {
        int v = edge[i].to;
        if (v == fa[u]) continue;
        deep[v] = deep[u] + 1;
        fa[v] = u;
        dfs1 (v);
        siz[u] += siz[v];
        if (siz[v] > siz[son[u]]) son[u] = v;
    }
}
int dfn_clock;
int dfn[maxn], top[maxn];
void dfs2 (int u) {
    dfn[u] = ++dfn_clock;
    if (son[u]) {
        top[son[u]] = top[u];
        dfs2 (son[u]);
        for (register int i = head[u]; i; i = edge[i].next) {
            int v = edge[i].to;
            if (v != fa[u] && v != son[u]) {
                top[v] = v;
                dfs2 (v);
            }
        }
    }
}
inline int lca (int x, int y) {
    while (top[x] != top[y]) {
        if (deep[top[x]] > deep[top[y]]) {
            x = fa[top[x]];
        } else {
            y = fa[top[y]];
        }
    }
    if (deep[x] < deep[y]) {
        return x;
    } else {
        return y;
    }
}
int tp;
int stk[maxn];
inline void ins(int x){
    if (tp == 0){
        stk[tp = 1] = x;
        return;
    }
    int LCA = lca(stk[tp], x);
    while ((tp > 1) && (deep[LCA] < deep[stk[tp - 1]])) {
        addedge(stk[tp - 1], stk[tp]);
        --tp;
    }
    if (deep[LCA] < deep[stk[tp]]) addedge(LCA, stk[tp--]);
    if ((!tp) || (stk[tp] != LCA)) stk[++tp] = LCA;
    stk[++tp] = x;
}
int ans;
int a[maxn];
bool cmp (int a, int b) {
    return dfn[a] < dfn[b];
}
void dfs3 (int u) {
    int x;
    if (siz[u]) {
        for (register int i = head[u]; i; i = edge[i].next) {
            int v = edge[i].to;
            dfs3 (v);
            if (siz[v]) {
                siz[v] = 0;
                ans++;
            }
        }
    } else {
        for (register int i = head[u]; i; i = edge[i].next) {
            int v = edge[i].to;
            dfs3 (v);
            siz[u] += siz[v];
            siz[v] = 0;
        }
        if (siz[u] > 1) {
            ans++;
            siz[u] = 0;
        }
    }
}
int main () {
    n = read();
    int from, to;
    for (register int i = 1; i < n; i++) {
        from = read(), to = read();
        addedge (from, to), addedge(to, from);
    }
    tot = 0;
    top[1] = 1;
    deep[1] = 1;
    dfs1 (1);
    dfs2 (1);
    memset (head, 0, sizeof head);
    memset (siz, 0, sizeof siz);
    tot = 0;
    q = read();
    while (q--) {
        m = read();
        for (register int i = 1; i <= m; i++) {
            a[i] = read();
            siz[a[i]] = 1;
        }
        bool judge = false;
        for (register int i = 1; i <= m; i++) {
            if (siz[fa[a[i]]]) {
                puts("-1");
                judge = true;
                break;
            }
        }
        if (judge == true) {
            memset (siz, 0, sizeof siz);
            continue;
        }
        ans = 0;
        sort (a + 1, a + 1 + m, cmp);
        if (a[1] != 1) {
            stk[tp = 1] = 1;
        }
        for (register int i = 1; i <= m; i++) {
            ins(a[i]);
        }
        if (tp) {
            while (--tp) {
                addedge (stk[tp], stk[tp + 1]);
            }
        }
        dfs3 (1);
        memset (siz, 0, sizeof siz);
        dfn_clock = 0;
        cout<<ans<<endl;
    }
    return 0;
}

例題2(涼宮春日的消失)

在觀察涼宮和你相處的過程中,\(Yoki\)產生了一個叫做愛的\(bugfeature\),將自己變成了一個沒有特殊能力的普通女孩並和你相遇。但你仍然不能扔下涼宮,準備利用\(Yoki\)留下的緊急逃脫程式回到原來的世界。這個緊急逃脫程式的關鍵就是將線索配對。
為了簡化問題,我們將可能的線索間的關係用一棵\(n\)個點的樹表示,兩個線索的距離定義為其在樹上唯一最短路徑的長度。因為你不知道具體的線索是什麼,你需要進行\(q\)次嘗試,每次嘗試都會選中一個大小為偶數的線索集合\(V\) ,你需要將線索兩兩配對,使得配對線索的距離之和不超過\(n\) 。如果這樣的方案不存在,輸出\(No\)

思路

一眼看到選關鍵點,顯然可以用虛樹搞,並且很顯然有一個性質,該條件只要關鍵點數是偶數,那麼一定存在方案。一個類似貪心的思想,可以在一顆子樹中找到配對的就在一顆子樹中解決,並且一顆子樹中最多隻會有一個點沒有找到配對,那麼把當前點扔到父節點中找配對,並且這個點選最靠上的,具體證明不證了,畫畫圖很顯然。
然後每次把關鍵點建一顆虛樹,然後進行上述操作搞搞就好了。
程式碼實現(為啥我的跑的這麼慢\(qwq\)



#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int maxn = 2e5 + 50;
inline int read () {
    int x = 0, f = 1; char ch = getchar();
    for (;!isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
    for (; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
    return x * f;
}
int n;
struct Edge {
    int from, to, next;
} edge[maxn << 1];
int tot, head[maxn];
inline void addedge (int a, int b) {
    edge[++tot].to = b;
    edge[tot].from = a;
    edge[tot].next = head[a];
    head[a] = tot;
}
deque<int> que[maxn];
bool col[maxn];
int f[maxn];
int son[maxn], siz[maxn], deep[maxn];
void dfs1 (int u) {
    siz[u] = 1;
    for (register int i = head[u]; i; i = edge[i].next) {
        int v = edge[i].to;
        if (v == f[u]) continue;
        f[v] = u;
        deep[v] = deep[u] + 1;
        dfs1 (v);
        siz[u] += siz[v];
        if (siz[v] > siz[son[u]]) son[u] = v;
    }
}
int dfn[maxn], dfn_clock;
int top[maxn];
void dfs2 (int u) {
    dfn[u] = ++dfn_clock;
    if (son[u]) {
        top[son[u]] = top[u];
        dfs2 (son[u]);
        for (register int i = head[u]; i; i = edge[i].next) {
            int v = edge[i].to;
            if (v != f[u] && v != son[u]) {
                top[v] = v;
                dfs2 (v);
            }
        }
    }
}
inline int lca (int x, int y) {
    while (top[x] != top[y]) {
        if (deep[top[x]] > deep[top[y]]) {
            x = f[top[x]];
        } else {
            y = f[top[y]];
        }
    }
    if (deep[x] < deep[y]) return x;
    return y;
}
int tp;
int stk[maxn];
inline void ins(int x)
{
    if (tp == 0)
    {
        stk[tp = 1] = x;
        return;
    }
    int ance = lca(stk[tp], x);
    while ((tp > 1) && (deep[ance] < deep[stk[tp - 1]]))
    {
        addedge(stk[tp - 1], stk[tp]);
        --tp;
    }
    if (deep[ance] < deep[stk[tp]]) addedge(ance, stk[tp--]);
    if ((!tp) || (stk[tp] != ance)) stk[++tp] = ance;
    stk[++tp] = x;
}
int a[maxn];
inline void dfs (int u) {
    for (register int i = head[u]; i; i = edge[i].next) {
        int v = edge[i].to;
        dfs(v);
        if (!que[v].empty()) {
            int a = que[v].front();
            que[v].pop_front();
            que[u].push_back(a);
        }
    }
    if (col[u]) que[u].push_front(u);
    while (!que[u].empty()) {
        int a = que[u].back();
        que[u].pop_back();
        if (!que[u].empty()) {
            int b = que[u].back();
            que[u].pop_back();
            printf("%d %d\n", a, b);
        } else {
            que[u].push_back(a);
            break;
        }
    }
}
bool cmp (int a, int b) {
    return dfn[a] < dfn[b];
}
int main () {
    n = read();
    int x, y;
    for (register int i = 1; i < n; i++) {
        x = read(), y = read();
        addedge (x, y), addedge (y, x);
    }
    int s;
    dfs1 (1);
    dfs2 (1);
    memset (head, 0, sizeof head);
    tot = 0;
    while (1) {
        s = read();
        if (s == 0) return 0;
        for (register int i = 1; i <= s; i += 1) {
            a[i] = read();
            col[a[i]] = true;
        }
        printf("Yes\n");
        sort (a + 1, a + 1 + s, cmp);
        if (a[1] != 1) {
            stk[tp = 1] = 1;
        }
        for (register int i = 1; i <= s; i++) {
            ins (a[i]);
        }
        if (tp) {
            while (--tp) {
                addedge (stk[tp], stk[tp + 1]);
            }
        }
        
        dfs (1);
        memset(col, 0, sizeof col);
        memset (head, 0, sizeof head);
        tot = 0;
    }
    return 0;
}

完結散花\(qwq\)