支配樹學習筆記

lalaouye發表於2024-07-15

先丟擲一個問題:給一個有向圖,問從 \(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)=\min(\{v|(v,u)\in E,v<u\}\cup\{sdom(w)|w>u\land\exists v>u,(u,v)\in E,\text{s.t.} w \text{是} v \text{的祖先或} w=v) \]

證明:前一半是顯然的,並且我們可以知道右邊的式子顯然要大於等於 \(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]);
} 

於是,我們便學會了支配樹。

相關文章