虛樹學習小結

虛樹一開始聽的時候以爲很高深,其實也是一個比較容易的東西。html

能夠稱它是個數據結構,也能夠稱它是個算法,反正比較好用啦~c++

定義

虛樹就是將原樹中的點集 \(S\) 拿出來,構成一棵新的並能保持原樹結構的一棵樹。git

保持結構,意味着對於 \(\forall x, y \in S\) ,他們的最近公共祖先 \(lca\) 也得出如今虛樹中來。算法

舉個栗子:數據結構

對於這顆樹來講post

咱們將 \(\{3, 6, 7\}\) 取出來變成一棵虛樹就是這樣的:優化

咱們保留了這些點的 \(lca\) 以及它自己,而後根據他們在原樹中的相對關係建了出來。ui

全部點對的 \(lca\) 個數是嚴格 \(< |S|\) 的,後面能利用構造的方式進行證實。spa

構建

首先咱們講全部可能出現的點拿出來,也就是 \(S\) 集合中點對的 \(lca\) ,以及 \(S\) 自己,咱們稱這些點爲關鍵點,他們構成了一個集合 \(T\)

  1. 咱們將全部點按照他們的 \(dfs\) 序進行排序,而後相鄰兩個求 \(lca\) 就是全部點對的 \(lca\) 了。

    不知道 \(dfs\) 序能看看我 這篇博客

    接下來咱們證實一下爲何這樣就是對的。

    證實:

    若是有點對 \((x, y)\) 排序後不是相鄰點對,他們的 \(lca\) 必然出如今別的裏面。

    如圖所示

    \(x, y\)\(lca\)\(1\) ,那麼選擇一個 \(dfs\) 序最大且在 \(dfs\) 序在 \(x\) 後面的 \(4\) 的子樹的點 \(a\)

    不難發現 \(a\)\(dfs\) 序下一個點只能存在與 \(2\) 的子樹當中,而這一對的 \(lca\)\(1\) ,就已經包括了 \(x, y\)\(lca\)

    同理,就算不存在 \(a\) ,咱們用 \(x\) 來替代 \(a\) 也能達到相同的效果。

    其餘狀況全均可以類比論證,那麼證畢。 怎麼以爲證得很僞啊

  2. 而後將這些點再按 \(dfs\) 序排序,而後用 std :: unqiue 去重。

  3. 用一個棧維護一條從根下來的關鍵點鏈,而後不斷對於這個棧進行操做,每次將新加進來的點與棧頂連一條邊。

    由於是按照 \(dfs\) 序進行排序,因此一條鏈上的點是按照從高到低一個個出現的。

    • 每次假設進來一個點 \(x\) ,咱們把這個點與棧頂進行比較,若是 \(x\) 在棧頂點的子樹中,連一條邊咱們就能夠直接入棧。
    • 不然咱們一直彈掉棧頂元素,直至知足上面的要求(或者棧爲空)

    判斷是否在子樹中,咱們能夠記一下這個點進來的時間戳(也就是他的 \(dfs\) 序)pre[u] 以及離開的時間戳 post[u] 若是這個 post[u] >= pre[v] ,那麼意味着 \(v\)\(u\) 的子樹中。(由於有按 pre 排序的前提)

    這個過程能夠形象地理解成有一條鏈從左往右不斷在晃,而後每一個點只須要連上他在這條鏈的父親就好了。

代碼

形象地看看代碼實現吧qwq。。(其實很短)而且由於已經有了順序,此處能夠只加單向邊了~

但須要注意的是,咱們經常要把原來的點和新產生的 \(lca\) 進行區分,這個咱們一開始打上標記就好了。

void Build() {
    sort(lis + 1, lis + k + 1, Cmp);
    for (int i = k; i > 1; -- i) lis[++ k] = Get_Lca(lis[i], lis[i - 1]);
    sort(lis + 1, lis + k + 1, Cmp); k = unique(lis + 1, lis + k + 1) - lis - 1;
    for (int i = 1; i <= k; ++ i) {
        while (top && post[sta[top]] < pre[lis[i]]) -- top;
        if (top) add_edge(sta[top], lis[i]); sta[++ top] = lis[i];
    }
}

應用

對於每次只拿一些特殊點出來,而後對於這些點進行 \(dp\) 或者其餘神奇操做的題。

虛樹經常是解決這些題的利器。但要注意點數和 \(\sum k\) 不能很大。

它的構建的複雜度是 \(O((\sum k) \times \log n)\) 的,常數也不大。

題目

LOJ #2219. 「HEOI2014」大工程

題意

給你一棵有 \(n\) 個點的樹,有 \(q\) 次詢問,每次給你 \(k\) 個點,而後兩兩都有一條通道。

詢問這 \(\displaystyle \binom {k}{2}\) 條通道中:

  1. 他們的距離和
  2. 他們之中距離最小的是多少
  3. 他們之中距離最大的是多少

\(n \le 10^6, \sum k \le 2 \times n\)

題解

每次考慮把那些點拿出來構造出虛樹。

注意此處那些虛樹的邊權要換成原樹中對應的那條鏈的邊權和。(也就是兩個 \(u, v\) 的深度之差)

而後咱們就轉化成求樹上最長鏈,最短鏈,以及全部鏈長度之和。

前面兩個能夠利用一個很容易的 \(dp\) 來解決。

首先考慮最長鏈,具體來講令 \(f_u\)\(u\) 向下延伸的最長鏈,\(f'_u\)\(u\) 向下延伸的次長鏈。

而後最長鏈就是 \(\max \{f_u + f'_u\}\)

其實這個 \(f'_u\) 並不須要顯式地記下來,只須要每次轉移上來的時候和原來的 \(f_u\) 算一遍,而後嘗試着更新便可。

最短鏈也是同理的。

而後對於全部鏈長度之和,這個很相似於 Wearry 當初出的那道題 [HAOI2018]蘋果樹

咱們仍然是考慮一條邊的貢獻,它的貢獻是邊兩邊的子樹點的乘積,再乘上這條邊的邊權。

而後就能夠順便記一會兒樹中關鍵點個數,而後轉移就能夠了qwq

複雜度是 \(O((\sum k) \log n)\)

代碼

/**************************************************************
    Problem: 3611
    User: zjp_shadow
    Language: C++
    Result: Accepted
    Time:4436 ms
    Memory:204588 kb
****************************************************************/
 
#include <bits/stdc++.h>
 
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)
 
using namespace std;
 
typedef long long ll; 
inline bool chkmin(ll &a, ll b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(ll &a, ll b) {return b > a ? a = b, 1 : 0;}
 
inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
    return x * fh;
}
 
void File() {
#ifdef zjp_shadow
    freopen ("3611.in", "r", stdin);
    freopen ("3611.out", "w", stdout);
#endif
}
 
const ll inf = 1e18;
 
const int N = 2e6, M = N << 1;
 
int Head[N], Next[M], to[M], val[M], e = 0;
inline void add_edge(int u, int v, int w) {
    to[++ e] = v; Next[e] = Head[u]; val[e] = w; Head[u] = e;
}
 
inline void Add(int u, int v, int w) {
    add_edge(u, v, w); add_edge(v, u, w);
}
 
#define Travel(i, u, v) for(register int i = Head[u], v = to[i]; i; v = to[i = Next[i]])
 
int dep[N], sz[N], fa[N], son[N];
void Dfs_Init(int u = 1, int from = 0) {
    sz[u] = 1; dep[u] = dep[fa[u] = from] + 1;
    Travel(i, u, v) if (v != from) {
        Dfs_Init(v, u), sz[u] += sz[v];
        if (sz[son[u]] < sz[v]) son[u] = v;
    }
}
 
int top[N], pre[N], post[N];
void Dfs_Part(int u = 1) {
    static int clk = 0; pre[u] = ++ clk;
    top[u] = son[fa[u]] == u ? top[fa[u]] : u;
    if (son[u]) Dfs_Part(son[u]);
    Travel(i, u, v) if (v != fa[u] && v != son[u]) Dfs_Part(v);
    post[u] = clk;
}
 
inline int Get_Lca(int x, int y) {
    for (; top[x] != top[y]; x = fa[top[x]])
        if (dep[top[x]] < dep[top[y]]) swap(x, y);
    return dep[x] < dep[y] ? x : y;
}
 
inline bool Cmp(const int &a, const int &b) {
    return pre[a] < pre[b];
}
 
ll Sum, Min, Max;
 
namespace Virtual_Tree {
 
    bitset<N> Tag;
    void Init() {
        Tag.reset(); Set(Head, 0); e = 0; 
        Sum = 0; Min = inf, Max = -inf;
    }
 
    int lis[N * 2], cnt = 0, k;
 
    void Build() {
        cnt = k = read();
        For (i, 1, k) Tag[lis[i] = read()] = true;
        sort(lis + 1, lis + k + 1, Cmp);
        For (i, 1, k - 1) lis[++ k] = Get_Lca(lis[i], lis[i + 1]); lis[++ k] = 1;
        sort(lis + 1, lis + k + 1, Cmp); k = unique(lis + 1, lis + k + 1) - lis - 1;
 
        static int Top, sta[N * 2]; Top = 0;
        For (i, 1, k) {
            while (Top && post[sta[Top]] < pre[lis[i]]) -- Top;
            if (Top) add_edge(sta[Top], lis[i], dep[lis[i]] - dep[sta[Top]]); sta[++ Top] = lis[i];
        }
    }
 
    void Clear() {
        For (i, 1, k) Tag[lis[i]] = false, Head[lis[i]] = 0; e = 0;
        Sum = 0; Min = inf, Max = -inf;
    }
 
    ll minv[N], maxv[N];
    int Dp(int u = 1) {
        int tot;
        if (Tag[u]) tot = 1, minv[u] = maxv[u] = 0;
        else tot = 0, minv[u] = inf, maxv[u] = -inf;
        Travel(i, u, v) {
            ll tmp = Dp(v); tot += tmp; Sum += 1ll * val[i] * (cnt - tmp) * tmp; 
            tmp = minv[v] + val[i]; chkmin(Min, minv[u] + tmp); chkmin(minv[u], tmp);
            tmp = maxv[v] + val[i]; chkmax(Max, maxv[u] + tmp); chkmax(maxv[u], tmp);
        }
        return tot;
    }
 
}
 
int main() {
 
    File();
 
    int n = read();
    For (i, 1, n - 1) {
        int u = read(), v = read(); Add(u, v, 0);
    }
    Dfs_Init(); Dfs_Part();
 
    Virtual_Tree :: Init();
    for (int m = read(); m; -- m) {
        Virtual_Tree :: Build(); Virtual_Tree :: Dp(); 
        printf ("%lld %lld %lld\n", Sum, Min, Max); 
        Virtual_Tree :: Clear();
    }
 
    return 0;
 
}

BZOJ 2286: [SDOI 2011]消耗戰

題意

給你 \(n\) 個點以 \(1\) 爲根的樹,每條邊有邊權 \(w\)

\(q\) 次詢問,每次詢問 \(k\) 個點,問這些點與根節點斷開的最小代價。

題解

顯然又把這些關鍵點拿出來建出虛樹。

而後咱們能夠用一個很顯然的 \(dp\) 來解決,

\(f_u\)\(u\) 子樹中全部關鍵點到根的路徑斷掉最小代價。

爲了方便轉移,咱們令 \(val_u\)\(u\) 到根節點路徑上邊權最小值,這個顯然能夠預處理。

若是這個點是一個關鍵點,那麼顯然有 \(f_u = val_u\) ,由於必選向上最小的邊,而下面的邊選的話只會增大代價。

若是這個點不是關鍵點,那麼就有 \(f_u = \min \{\sum_{v} f_v, val_u\}\) (此處 \(v\)\(u\) 在虛樹上的兒子)

這樣就能夠作完啦qwq

複雜度是 \(O((\sum k)\log n)\) 的。

代碼

本身寫吧qwq 很好寫的。。。

。。。。。。

LOJ #2496. 「AHOI / HNOI2018」毒瘤

題意

給你一個有 \(n\) 個點 \(m\) 條邊的聯通圖,求它的獨立集數量。

\(n \le 10^5, n - 1 \le m \le n + 10\)

題解

一道好題。

惋惜考試時候連狀壓都沒調出來,暴力滾粗啦TAT 惋惜惋惜真惋惜

首先考慮樹的時候怎麼作,令 \(f_{u, 0/1}\)\(u\) 選與不選對於 \(u\) 的子樹的方案數。

而後顯然有
\[ \begin{align} f_{u,0} &= \prod _v (f_{v, 0} + f_{v, 1})\\ f_{u,1} &= \prod _v f_{v, 0} \end{align} \]
咱們再考慮多了那些邊如何處理,不難發現就是這些邊連着的點(關鍵點)不能同時選擇。

因此對於這些點就有三種狀態 \((0, 0), (0, 1), (1, 0)\)

這樣能夠直接暴力枚舉這些狀態,而後到這些點的時候強制使這些關鍵點的 \(f_{u, 0/1} = 0~or~1\)

不難發現 \((0, 0)\)\((0, 1)\) 能夠合併到一塊兒(強制使得前面那個點不選)

\(S = m - (n - 1)\)

而後這個直接作就是 \(O(2 ^ S \times n)\) ,指望得分 \(75\sim 85pts\)

而後不難發現這個可使用虛樹進行優化,由於每次的關鍵點是比較少的。

咱們能夠考慮把這個關鍵點對應的虛樹建出來,而後爲了方便,一開始就把這些點對應的虛樹建出來就好了。

咱們能夠在 Dfs_Init() 中預處理出這個虛樹,只須要考慮它有至少有兩個子樹都有關鍵點,那麼它就是一個關鍵點。

不難發現這個關鍵點個數最多隻有 \(4S\) 個。而後咱們至關於把樹上一些鏈合併成了一條邊,而後對於剩下的點進行 \(dp\)

不難發現咱們能夠把 \(u, v\) 這兩個點的關係表示成 \(k_{0/1,0/1}\) 也就是 \(f_{v,0/1}\) 對於 \(f_{u,0/1}\) 的貢獻係數。

咱們就能夠考慮一開始處理出這個貢獻係數。

咱們令 \(g_{u,0/1}\)\(u\) 不考慮它虛子樹的方案數,這個轉移和上面 \(f\) 的轉移是相似的。

若是當前考慮的 \(v\) 是虛子樹的話,分兩種狀況。

  1. \(u\) 是一個關鍵點,咱們考慮連上 \(v\) 子樹中的那個最高的關鍵點,邊權就是以前的那個係數。
  2. \(u\) 不是一個關鍵點,那麼繼承 \(v\) 的轉移係數(此處轉移和 \(g\) 轉移相似)

而後遍歷完它全部兒子後,若是 \(u\) 是關鍵點,把它的 \(k\) 清空,從新爲下一條鏈作準備。

若是不是的話,注意要把 \(g\) 乘到 \(k\) 上去。(由於這部分系數須要轉移到後面去)

代碼

建議看看代碼,增強碼力QwQ

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
    return x * fh;
}

void File() {
#ifdef zjp_shadow
    freopen ("2496.in", "r", stdin);
    freopen ("2496.out", "w", stdout);
#endif
}

int n, m;

const int Mod = 998244353;

typedef long long ll;
typedef pair<ll, ll> PLL;

#define fir first
#define sec second
#define mp make_pair

inline PLL operator + (const PLL &a, const PLL &b) {
    return mp((a.fir + b.fir) % Mod, (a.sec + b.sec) % Mod);
}

inline PLL operator * (const PLL &a, const int b) {
    return mp(a.fir * b % Mod, a.sec * b % Mod);
}

inline PLL operator * (const PLL &a, const PLL b) {
    return mp(a.fir * b.fir % Mod, a.sec * b.sec % Mod);
}

inline void operator *= (PLL &a, const int &b) { a = a * b; }

inline void operator += (PLL &a, const PLL &b) { a = a + b; }

inline ll Calc(PLL a, PLL b) {
    PLL tmp = a * b; return (tmp.fir + tmp.sec) % Mod;
}

const int N = 1e5 + 1e3, M = N << 1;

PLL val0[M], val1[M];

struct Graph {

    int Head[N], Next[M], to[M], e;

    Graph() { e = 0; }

    void add_edge(int u, int v, PLL wa = mp(0, 0), PLL wb = mp(0, 0)) {
        to[++ e] = v; Next[e] = Head[u]; val0[e] = wa; val1[e] = wb; Head[u] = e;
    }

} G1, G2;

#define Travel(i, u, v, G) for(register int i = G.Head[u], v = G.to[i]; i; i = G.Next[i], v = G.to[i])

ll g[N][2], f[N][2]; PLL k[N][2];

bitset<N> key, vis;

int Build(int u = 1) {
    g[u][0] = g[u][1] = 1;
    int son = 0; vis[u] = true;
    Travel(i, u, v, G1) if (!vis[v]) {
        int to = Build(v);
        if (!to) {
            (g[u][0] *= (g[v][0] + g[v][1])) %= Mod,
            (g[u][1] *= g[v][0]) %= Mod;
        }
        else if (key[u]) 
            G2.add_edge(u, to, k[v][0] + k[v][1], k[v][0]);
        else 
            k[u][0] = k[v][0] + k[v][1], 
            k[u][1] = k[v][0], son = to;
    }

    if (key[u]) k[u][0] = mp(1, 0), 
                k[u][1] = mp(0, 1);
    else k[u][0] *= g[u][0], 
         k[u][1] *= g[u][1];
    return key[u] ? u : son;
}

int dfn[N], lv[N], rv[N], cnt = 0;
int Dfs_Init(int u = 1, int fa = 0) {
    static int clk = 0; int tot = 0; dfn[u] = ++ clk;
    Travel(i, u, v, G1) if (v != fa) {
        if (!dfn[v]) tot += Dfs_Init(v, u);
        else {
            key[u] = true;
            if (dfn[u] < dfn[v])
                lv[++ cnt] = u, rv[cnt] = v;
        }
    }
    key[u] = key[u] || (tot > 1);
    return tot || key[u];
}

bool Shall[N][2]; ll dp[N][2];

void Dp(int u = 1) {
    if(Shall[u][1]) dp[u][0] = 0; else dp[u][0] = g[u][0];
    if(Shall[u][0]) dp[u][1] = 0; else dp[u][1] = g[u][1];
    Travel(i, u, v, G2) {
        Dp(v); PLL tmp = mp(dp[v][0], dp[v][1]);
        (dp[u][0] *= Calc(val0[i], tmp)) %= Mod;
        (dp[u][1] *= Calc(val1[i], tmp)) %= Mod;
    }
}

int main () {

    File();

    n = read(); m = read();
    For (i, 1, m) {
        int u = read(), v = read();
        G1.add_edge(u, v); G1.add_edge(v, u);
    }
    Dfs_Init(); key[1] = true; Build();

    ll ans = 0;
    For (sta, 0, (1 << cnt) - 1) {
        For (i, 1, cnt)
            if ((sta >> (i - 1)) & 1)
                Shall[lv[i]][1] = Shall[rv[i]][0] = true;
            else
                Shall[lv[i]][0] = true;

        Dp(); (ans += dp[1][1] + dp[1][0]) %= Mod;

        For (i, 1, cnt)
            if ((sta >> (i - 1)) & 1)
                Shall[lv[i]][1] = Shall[rv[i]][0] = false;
            else
                Shall[lv[i]][0] = false;
    }

    printf ("%lld\n", ans);

    return 0;

}
相關文章
相關標籤/搜索