先丟擲一個問題:給一個有向圖,問從 \(1\) 節點出發,求每個節點的受支配集。
這裡,支配的定義為:若從 \(1\) 結點出發到 \(v\) 節點的所有路徑中,都必須經過 \(u\) 節點,則稱 \(u\) 支配 \(v\)。
那麼受支配集意思就是對於 \(v\) 點滿足條件的 \(u\) 點的集合。那麼根據支配的定義,我們可以推出一個結論:如果 \(u_0\) 支配 \(v\),\(u_1\) 支配 \(v\),那麼必有 \(u_0\) 支配 \(u_1\),或 \(u_1\) 支配 \(u_0\)。這是顯然的, 因為如果它們兩個點不互相支配的話,那這兩個點都會存在另一條不經過自己並經過對方到達 \(v\) 的路徑,並不能滿足這兩個點是 \(v\) 的支配點。
也就是說,這是一個樹形結構。每個點的父親為第一個支配他的點,那麼這棵支配樹中每個點的祖先和自己都是支配自己的點。現在,我們考慮求出這棵支配樹。
考慮運用 dfs 序和生成樹完成該問題,為了方便,以下的任意節點 \(u\) 的編號為 \(dfn_u\)。
於是發明支配樹的強者建立了一個新的概念:半支配點。如果 \(u\) 半支配 \(v\),那麼 \(u\) 必須滿足存在一條到 \(v\) 的路徑使得,除了 \(u,v\) 兩點以外路徑中其它點 \(v_k\) 都要大於 \(v\),並且 \(u\) 是滿足上述條件的編號(dfs 序)最小的點。這跟支配點有什麼關係?帶著問題推導到後面就知道了。
我們先設點 \(u\) 的支配點為 \(idom(u)\),半支配點為 \(sdom(u)\)。
結論 0:
顯然能到達 \(u\) 的點集中,只有 \(u\) 的父親小於 \(u\),其餘均大於 \(u\)。
結論 1:
\(sdom(u)\) 必然小於 \(u\)。
證明:考慮 \(u\) 的父親就是滿足條件的點。
結論 2:
在生成樹中,\(idom(u)\) 必然是 \(u\) 的祖先(或本身)。
證明:考慮到從根到 \(u\) 就存在一條路徑。
結論 3:
在生成樹中,\(sdom(u)\) 必然是 \(u\) 的祖先。
證明:\(sdom(u)\) 小於 \(u\),而在構建生成樹的過程中,如果 \(v\) 小於 \(u\) 且存在 \(v\) 到 \(u\) 的路徑,那麼樹中 \(v\) 一定是 \(u\) 的祖先。
結論 4:
在生成樹中,\(idom(u)\) 必然是 \(sdom(u)\) 的祖先(或本身)。
證明:顯然。
透過這些結論,我們可以得到第一個定理:
證明:前一半是顯然的,並且我們可以知道右邊的式子顯然要大於等於 \(sdom(u)\)。
然後,我們在半支配路徑 \(sdom(u),v_1,v_2,\cdots,v_k,u\) 中找到 \(v_k\) 最小的祖先 (或 \(v_k\)),記為 \(v_j\),我們接下來證明 \(sdom(u)\) 是滿足 \(v_j\) 半支配點條件的點,即證明 \(\forall 1\le i<j,v_i>v_j\)。若存在 \(v_i<v_j\),那麼 \(v_i\) 顯然不能作為 \(v_j\) 的祖先,而 \(v_i\) 能到達 \(v_j\),則說明在建立生成樹時,只有 \(2\) 種情況,一種是 \(v_i\) 是 \(v_j\) 的祖先,\(v_i<v_j\),一種是 \(v_i\) 不為 \(v_j\) 祖先,且 \(v_i>v_j\)。而這兩種情況與剛剛的條件不存在交集,故不存在 \(v_i<v_j\)。
所以該式子中包含了 \(sdom(u)\),我們完成了證明。
於是,我們便有了一種快速求解半支配點的方法。我們按照 dfs 序從後往前做,利用帶權並查集維護剛剛的式子即可。
求出來半支配點,我們就可以求解支配樹了!
首先有個非常醜陋的方法,也比較難寫,非常不推薦。
就是我們在生成樹基礎上將 \(sdom(u)\) 向 \(u\) 連邊,這樣會形成一個 DAG,而在 DAG 上跑支配樹是簡單的。那麼為什麼這樣是對的呢?顯然,我們對這種做法的第一感覺是我們丟掉了一些關係,也就是說我們成為支配點的條件較原圖變得似乎更寬了。於是我們考慮嚴謹證明 DAG 上的每一個支配關係在原圖中照樣存在。
首先在建立生成樹時,如果存在從 \(v\) 到 \(u\) 的路徑,那麼只有 \(2\) 種情況,一種是 \(v\) 是 \(u\) 的祖先,\(v<u\),一種是 \(v\) 不為 \(u\) 祖先,且 \(v>u\)。
我們只考慮證明先經過一段樹邊,再經過一段非樹邊到 \(u\) 的情況(不經過 DAG 情況下的 \(idom(u)\))。其他情況沒有任何必要考慮,如果產生了多次樹邊和非樹邊的切換,為什麼前面冗餘的切換不能全走樹邊?所以只有這種情況有必要考慮。注意到,經過的這一段非樹邊可以形成一段符合條件的半支配路徑,而半支配邊已經建好了,且其支配點肯定是其半支配點的祖先,故在原圖中也不可能存在不經過 \(idom(u)\) 就能到達 \(u\) 的情況。
所以,我們可以在這樣一個 DAG 中求出支配樹。
而第二種方法才是主流方法。這裡需要另外兩個定理。
定理 1:對於任意節點 \(u\),設 \(v\) 是 \(sdom(u)\) 到 \(u\) 這條鏈上半支配點最小的點(不包括 \(u\) 和 \(sdom(u)\)),若 \(sdom(v)\ge sdom(u)\),則 \(idom(u)=sdom(u)\)。
(注意!鏈是指樹上的鏈,我學這個內容時理解錯了,卡了很久!)
證明:考慮 \(sdom(u)\) 子樹外一點 \(v\),若 \(v\) 能到達 \(u\),則 \(v>u>sdom(u)\)。所以,首先 \(v\) 不能為 \(sdom(u)\) 的祖先。然後,我們可以發現如果 \(v\) 能到達 \(u\),會先到達 \(sdom(u)\) 與 \(u\) 之間的路徑的一個點 \(w\),然而如果是這樣,\(sdom(w)\) 肯定會小於 \(sdom(u)\),這並不滿足條件。
定理 2:對於任意節點 \(u\),設 \(v\) 是 \(sdom(u)\) 到 \(u\) 這條鏈上半支配點最小的點,若 \(sdom(v)<sdom(u)\),則 \(idom(u)=idom(v)\)。
證明:首先,\(idom(u)\) 肯定是 \(idom(v)\) 的祖先(或 \(idom(v)\)),否則將存在一條路徑 \(1\rightarrow idom(v)\rightarrow v\rightarrow u\)。故我們需要證明不存在從 \(1\) 出發不經過 \(idom(v)\) 的路徑到 \(u\)。首先這條路徑末端肯定會在 \(u\) 與 \(sdom(u)\) 之間(其實是在 \(u\) 和 \(v\) 之間,因為如果在 \(v\) 上面就說明有不經過 \(idom(v)\) 到達 \(v\) 的路徑,這是不可能的),因為如果不在並直接到 \(u\),則 \(sdom(u)\) 將會被更新。然而如果在這條鏈之間,則相當與 \(sdom(u)\) 到 \(u\) 這條鏈之間存在一點的半支配點小於 \(idom(v)\),這絕對不可能。
於是,我們發現這樣子就可以轉移了。支配樹的程式碼實現非常容易,我們整個按 dfs 序從後往前轉移,\(idom\) 和 \(sdom\) 一起求。實現時,我們建立半支配樹,每到一個點我們先幫以自己為半支配點的節點求出 \(idom\),然後再求自己的 \(sdom\),多麼的無私啊,具體實現看程式碼:
#include <bits/stdc++.h>
#define rep(i, l, r) for (int i = l; i <= r; ++ i)
#define rrp(i, l, r) for (int i = r; i >= l; -- i)
#define eb emplace_back
#define inf 1000000000
#define linf 100000000000000
#define pii pair <int, short>
using namespace std;
constexpr int N = 2e5 + 5, P = 998244353;
inline int rd ()
{
int x = 0, f = 1;
char ch = getchar ();
while (! isdigit (ch)) { if (ch == '-') f = -1; ch = getchar (); }
while (isdigit (ch)) { x = (x << 1) + (x << 3) + ch - 48; ch = getchar (); }
return x * f;
}
int n, m;
vector <int> g[N], fg[N], vec[N];
int dfn[N], id[N], tim;
int idom[N], sdom[N], fa[N];
struct DSU
{
int fa[N], mn[N];
void init ()
{
rep (i, 1, n) fa[i] = mn[i] = i;
}
int find (int x)
{
if (x == fa[x]) return x;
int fx = find (fa[x]);
if (dfn[sdom[mn[x]]] > dfn[sdom[mn[fa[x]]]]) mn[x] = mn[fa[x]];
return fa[x] = fx;
}
} d;
void dfs (int u)
{
id[dfn[u] = ++ tim] = u;
for (auto v : g[u]) if (! dfn[v]) dfs (v), fa[v] = u;
}
int ans[N];
int main ()
{
n = rd (), m = rd ();
rep (i, 1, m)
{
int u = rd (), v = rd ();
g[u].eb (v);
fg[v].eb (u);
}
dfs (1);
rep (i, 1, n) sdom[i] = i;
d.init ();
rrp (i, 1, n)
{
int u = id[i];
if (! u) continue;
for (auto v : vec[u])
{
d.find (v);
if (sdom[d.mn[v]] == u) idom[v] = u;
else idom[v] = d.mn[v]; //為什麼可以這樣?考慮到最後的點一定會指向自己的sdom,遞迴過來即可
}
if (i == 1) continue;
for (auto v : fg[u])
{
if (! dfn[v]) continue;
if (dfn[v] < dfn[sdom[u]]) sdom[u] = v;
else
{
d.find (v);
if (dfn[sdom[u]] > dfn[sdom[d.mn[v]]]) sdom[u] = sdom[d.mn[v]];
}
}
vec[sdom[u]].eb (u);//建立半支配樹
d.fa[u] = fa[u];//更新父親,以便求祖先的最小sdom
}
rep (i, 1, n) if (idom[id[i]] ^ sdom[id[i]]) idom[id[i]] = idom[idom[id[i]]];
rrp (i, 2, n)
{
++ ans[id[i]];
ans[idom[id[i]]] += ans[id[i]];
}
++ ans[1];
rep (i, 1, n) printf ("%d ", ans[i]);
}
於是,我們便學會了支配樹。