目錄php
虛樹一開始聽的時候以爲很高深,其實也是一個比較容易的東西。html
能夠稱它是個數據結構,也能夠稱它是個算法,反正比較好用啦~c++
虛樹就是將原樹中的點集 \(S\) 拿出來,構成一棵新的並能保持原樹結構的一棵樹。git
保持結構,意味着對於 \(\forall x, y \in S\) ,他們的最近公共祖先 \(lca\) 也得出如今虛樹中來。算法
舉個栗子:數據結構
對於這顆樹來講post
咱們將 \(\{3, 6, 7\}\) 取出來變成一棵虛樹就是這樣的:優化
咱們保留了這些點的 \(lca\) 以及它自己,而後根據他們在原樹中的相對關係建了出來。ui
全部點對的 \(lca\) 個數是嚴格 \(< |S|\) 的,後面能利用構造的方式進行證實。spa
首先咱們講全部可能出現的點拿出來,也就是 \(S\) 集合中點對的 \(lca\) ,以及 \(S\) 自己,咱們稱這些點爲關鍵點,他們構成了一個集合 \(T\) 。
咱們將全部點按照他們的 \(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\) 也能達到相同的效果。
其餘狀況全均可以類比論證,那麼證畢。
怎麼以爲證得很僞啊
而後將這些點再按 \(dfs\) 序排序,而後用 std :: unqiue
去重。
用一個棧維護一條從根下來的關鍵點鏈,而後不斷對於這個棧進行操做,每次將新加進來的點與棧頂連一條邊。
由於是按照 \(dfs\) 序進行排序,因此一條鏈上的點是按照從高到低一個個出現的。
判斷是否在子樹中,咱們能夠記一下這個點進來的時間戳(也就是他的 \(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)\) 的,常數也不大。
給你一棵有 \(n\) 個點的樹,有 \(q\) 次詢問,每次給你 \(k\) 個點,而後兩兩都有一條通道。
詢問這 \(\displaystyle \binom {k}{2}\) 條通道中:
\(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; }
給你 \(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 很好寫的。。。
。。。。。。
給你一個有 \(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\) 是虛子樹的話,分兩種狀況。
而後遍歷完它全部兒子後,若是 \(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; }