图论 —— 支配树
【概述】
給定一個有向圖與一個起點 S,如果要去掉起點 S 到某個點 p 的中間的某個點 x?后無法到達,那么稱點 x 支配點 p,x 是 p 的一個支配點。
顯然支配點 x 可以有多個,支配點的集合記為 {Xp},對于起點以為的點,它們都有兩個平凡的支配點,一個是其自身,一個是起點。
在支配 p 的點中,若一個支配點 i≠p,滿足 i 被 p 剩下的所有非平凡的支配點支配,則稱 i 為 p 的最近支配點,記作:idom(p)
將圖的起點記作 root,那么除 root 外的每一點,均存在唯一的最近支配點,因此,連上所有 root 以外的 idom(u)->u 的邊,就能得到一棵樹,其中每個點支配它子樹中的所有點,這棵樹即為支配樹。
對于樹來說,其本身就是自己的支配樹
對于 DAG 來說,可以簡單的看做是拓撲排序加上 LCA 求最近公共祖先的問題
對于一般有向圖來說,采用?Lengauer Tarjan 算法來解決
【DAG 圖】
對于 DAG 圖來說,如果要詢問從點 x 到點 y 之間有多少個點被刪掉后,使得這兩個點中的任何一個無法到達另一個,那么我們可以建立支配樹來解決。
對于 DAG 圖,我們首先進行反向拓撲,然后按照拓撲序建立支配樹
由于某點的支配樹上的父親就是該點在 DAG 中的所有出點在支配樹上的 LCA,因此對于詢問 query(x,y),結果也就是 x 的深度與 y 的深度的和減去 LCA(x,y) 的深度
int n, m, q; vector<int> G[N]; //原圖 vector<int> Gf[N]; //反圖 int in[N]; int a[N], tot; int deep[N], dp[N][22]; void topSort() { //對反圖進行拓撲排序queue<int> Q;for (int i = 1; i <= n; i++)if (in[i] == 0)Q.push(i);while (!Q.empty()) {a[++tot] = Q.front();Q.pop();int now = a[tot];for (int i = 0; i < Gf[now].size(); i++) {int to = Gf[now][i];in[to]--;if (in[to] == 0)Q.push(to);}} } int getLCA(int x, int y) { //獲取LCAif (deep[x] < deep[y])swap(x, y);int cnt = deep[x] - deep[y];for (int i = 0; i < 20; i++)if ((1 << i) & cnt)x = dp[x][i];if (x == y)return x;for (int i = 19; i >= 0; i--) {if (dp[x][i] != dp[y][i]) {x = dp[x][i];y = dp[y][i];}}return dp[x][0]; } void init() {tot = 0;memset(in, 0, sizeof(in));memset(dp, 0, sizeof(dp));for (int i = 0; i <= n; i++) {G[i].clear();Gf[i].clear();} } int query(int x, int y) { return deep[x] + deep[y] - deep[getLCA(x, y)]; } int main() {init();scanf("%d%d", &n, &m);for (int i = 1; i <= m; i++) {int x, y;scanf("%d%d", &x, &y);G[x].push_back(y);Gf[y].push_back(x);in[x]++;}topSort();for (int i = 1; i <= n; i++) {int x = a[i];if (G[x].size() == 0) {G[0].push_back(x);deep[x] = 1;continue;}int y = G[x][0];for (int i = 1; i < G[x].size(); i++)y = getLCA(y, G[x][i]);deep[x] = deep[y] + 1;dp[x][0] = y;for (int i = 1; i < 20; i++)dp[x][i] = dp[dp[x][i - 1]][i - 1];}scanf("%d", &q);while (q--) {int x, y;scanf("%d%d", &x, &y);printf("%d\n", query(x, y));}return 0; }【Lengauer Tarjan 算法】
從點 y?出發到點 x,若存在這樣一條路徑:路徑上的點(不包含 x、y 點)的 dfn 均大于 dfn[x],此時,稱 y 是 x 的半支配點
用 sdom[x] 表示 x 的半必經點中 dfn 最小的點,這樣一來,當刪除原圖中的非樹邊,然后連接 (sdom[x],x) 后,不僅不會改變原圖中的支配點關系,而且還將原圖轉為一個 DAG 圖,這樣就可以使用 DAG 的做法了
但對于一個一般有向圖來說,除了上述方法外,利用?Lengauer Tarjan 算法可以快速構造支配樹的算法,其可在 O(nlogn) 的時間復雜度內構造一個支配樹。
1.算法原理
1)構建 dfs 樹
首先對圖進行 dfs 遍歷,求出 dfs 序
2)找半支配點
對于一個點 x,要找出所有的邊 (y,x) 中對應的 y,那么有:
- 若 dfn[y]<dfn[x] 且 dfn[y] 比當前找到的 sdom[x] 的 dfn 小,那么有 sdom[x]=y
- 若 dfn[x]>dfn[y],那么找到樹上 y 的一個滿足 dfn[z]>dfn[x]?的祖先 z,然后比較 dfn[sdom[z]] 與 dfn[sdom[x]] 的關系,來決定是否用前者更新后者
3)半支配點到支配點
對于點 x 來說,我們要找的是 idom[x],當求出 sdom[x] 后,我們需要利用 sdom[x] 來找出 idom[x]
令 P 是從 sdom[x] 到 x 的樹上路徑點集,且 P 中不包含 sdom[x],并設 y 是 P 中 dfn[sdom[y]] 最小的點
那么:如果 sdom[y]=sdom[x],則 idom[x]=sdom[x],否則 idom[x]=idom[z]
2.算法流程
Lengauer Tarjan 算法的實現一般采用帶權并查集
首先對原圖進行一遍 dfs 來建立 dfs 樹并求出 dfs 序,然后按照 dfs 序從大到小進行處理,利用帶權并查集尋找每個點的半支配點 sdom
每一次處理完畢一個點后,將這個點同其在 dfs 樹上的父親在并查集進行連邊,邊的權值是這個點到根結點的路徑上的所有點中最小的 dfn[sdom[x]] 對應的 x
然后根據 dfs 的入度建立 dfs 樹的反圖,并進行拓撲排序,建立支配樹
最后直接在支配樹上進行 dfs,根據求出的?sdom[i] 直接計算?idom[i] 即可
3.模版
struct MAP {struct Edge {int to, next;} edge[N << 1];int tot, head[N];void addEdge(int x, int y) {edge[++tot].to = y;edge[tot].next = head[x];head[x] = tot;} }; MAP G, GF; //原圖、反圖 MAP dfsTree, dfsTreeF; // dfs樹、dfs樹的反圖 MAP dominate; //支配樹MAP xx; int n, m; int father[N]; // dfs樹上的父節點 int dfn[N], id[N], tim; // dfs序、標號、時間戳void dfs(int x) {id[++tim] = x;dfn[x] = tim;for (int i = G.head[x]; i; i = G.edge[i].next) {int to = G.edge[i].to;if (!dfn[to]) {dfs(to);father[to] = x;dfsTree.addEdge(x, to);}} }int sdom[N]; //半支配點 int mn[N]; // mn[i]表示i點的dfs樹上的sdom最小的祖先,因此有sdom[mn[i]]=sdom[i] int anc[N]; // anc[i]代表i的祖先 int find(int x) { //路徑壓縮的帶權并查集if (x != anc[x]) {int t = anc[x];anc[x] = find(anc[x]);if (dfn[sdom[mn[x]]] > dfn[sdom[mn[t]]])mn[x] = mn[t];}return anc[x]; } void LengauerTarjan() { //尋找半支配點for (int i = 1; i <= n; i++) {anc[i] = i;sdom[i] = i;mn[i] = i;}for (int j = n; j >= 2; j--) {int x = id[j];if (!x)continue;int pos = j;for (int i = GF.head[x]; i; i = GF.edge[i].next) {int y = GF.edge[i].to;if (!dfn[y])continue;if (dfn[y] < dfn[x])pos = min(pos, dfn[y]);else {find(y); //尋找樹上y的一個滿足dfn[z]>dfn[x]的祖先zpos = min(pos, dfn[sdom[mn[y]]]);}}sdom[x] = id[pos];anc[x] = father[x];dfsTree.addEdge(sdom[x], x); //在dfs樹上連邊} }int deep[N], dp[N][25]; int getLCA(int x, int y) { //獲取LCAif (deep[x] < deep[y])swap(x, y);int del = deep[x] - deep[y];for (int i = 0; i <= 20; i++)if ((1 << i) & del)x = dp[x][i];if (x == y)return x;for (int i = 20; i >= 0; i--) {if (dp[x][i] != dp[y][i]) {x = dp[x][i];y = dp[y][i];}}return dp[x][0]; } void buildDominate(int x) { //建立支配樹int to = dfsTreeF.edge[dfsTreeF.head[x]].to;for (int i = dfsTreeF.head[x]; i; i = dfsTreeF.edge[i].next) {int y = dfsTreeF.edge[i].to;to = getLCA(to, y);}deep[x] = deep[to] + 1;dp[x][0] = to;dominate.addEdge(to, x);for (int i = 1; i <= 20; i++)dp[x][i] = dp[dp[x][i - 1]][i - 1]; } int in[N]; // dfs樹的入度 void topSort() {for (int i = 1; i <= n; i++) {for (int j = dfsTree.head[i]; j; j = dfsTree.edge[j].next) {int to = dfsTree.edge[j].to;in[to]++;dfsTreeF.addEdge(to, i); //對DFS樹的反圖建邊}}for (int i = 1; i <= n; i++) {if (!in[i]) {dfsTree.addEdge(0, i); // dfs樹建邊dfsTreeF.addEdge(i, 0); // dfs樹的反圖建邊}}queue<int> Q;Q.push(0);while (Q.size()) {int x = Q.front();Q.pop();for (int i = dfsTree.head[x]; i; i = dfsTree.edge[i].next) {int y = dfsTree.edge[i].to;if ((--in[y]) <= 0) {Q.push(y);buildDominate(y); //建立支配樹}}} }int idom[N]; void dfsDominate(int x) { //在支配樹上搜索idomidom[x] = 1;for (int i = dominate.head[x]; i; i = dominate.edge[i].next) {int y = dominate.edge[i].to;dfsDominate(y);idom[x] += idom[y];} } int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= m; i++) {int x, y;scanf("%d%d", &x, &y);G.addEdge(x, y);GF.addEdge(y, x);}dfs(1); // dfs,求出dfs序LengauerTarjan(); //計算半支配點sdomtopSort(); //根據dfs樹建立dfs樹的反圖,并對進行拓撲排序從而建立支配樹dfsDominate(0); //在支配樹上尋找答案for (int i = 1; i <= n; i++)printf("%d ", idom[i]);printf("\n");return 0; }【例題】
- Blow up the city(HDU-6604)(DAG 圖的支配樹):點擊這里
- 支配樹(洛谷-P5180)(一般有向圖的支配樹):點擊這里
總結
- 上一篇: Balanced Lineup(POJ-
- 下一篇: 线段(信息学奥赛一本通-T1429)