学习博客
支配树学习笔记
与图论的邂逅03:Lengauer-Tarjan ← 原理介绍的不错
算法 - 支配树 (Lengauer Tarjan)
一些相关的定义
搜索树:有向无环图从根节点dfs时最先经过的边形成的树
在搜索树上的边:任意(u→v),均存在
d
f
n
u
<
d
f
n
v
dfn_u<dfn_v
dfnu<dfnv
不在搜索树上的边:任意(u→v),均存在
d
f
n
u
>
d
f
n
v
dfn_u>dfn_v
dfnu>dfnv
支配点,也叫必经点:
d
o
m
dom
dom
d
o
m
x
dom_x
domx :可以到达x的所有必经点的集合
最近支配点:
i
d
o
m
idom
idom
i
d
o
m
x
idom_x
idomx :dfn序离x最近的那个支配点
半支配点:
s
e
m
i
semi
semi
s
e
m
i
x
semi_x
semix :能通过走非树枝边到达x的深度最小的点的祖先y(不理解的看第2个学习博客)
s
e
m
i
x
semi_x
semix的性质:
- d f n s e m i x < d f n x dfn_{semi_x} < dfn_x dfnsemix<dfnx
- s e m i semi semi 不一定是支配点
- i d o m x idom_x idomx是 s e m i x semi_x semix在搜索树上的祖先
因为性质3(证明?),所以算法的关键是利用半支配点去求最近支配点
支配树求解的具体过程:
- 每计算一个点时,把这个点放进生成森林中,用并查集维护
- 根据半必经点定理,若dfn[x]>dfn[y],计算semi[y]时则需要考虑x祖先中dfn大于y的点
- 由于按时间戳从大到小的顺序计算,比y时间戳小的点还未加入生成森林,所以直接在生成森林中考虑x的祖先即可
- 令dfn[semi[x]]为x到其父亲的边的权值,用带权并查集可以求出边权的最小值
板子
题目:HDU4694
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n, m;
namespace DominantorTree {//支配树
// 建边
vector<int> pre[N];//前驱
vector<int> suf[N];//后继
vector<int> dom[N];//支配点集合 dom[x]表示所有可以到达x的支配点的集合
int fa[N];//节点在搜索树中的父亲
int dfn[N];
int semi[N];// dfn最小的半支配点
int idom[N];// dfn最大支配点 最近
int anc[N];// 并查集的祖先
int best[N];// 祖先链中dfn值最小的semi
int id[N];// 时间戳为x的原值
ll sum[N];// 每个点的支配点的编号之和
void insert(int u, int v) { // 建边 u->v
suf[u].push_back(v);
pre[v].push_back(u);
}
int num = 0;//时间戳
void init() {
for (int i = 1; i <= n; i++) {
suf[i].clear();
pre[i].clear();
dom[i].clear();
anc[i] = semi[i] = best[i] = i;
sum[i] = dfn[i] = id[i] = fa[i] = idom[i] = 0;
}
num = 0;
}
// dfs求时间戳
void dfs(int u) {
dfn[u] = ++num;
id[num] = u;
for (int v:suf[u]) {
if (!dfn[v]) {
dfs(v);
fa[dfn[v]] = dfn[u];
}
}
}
// 带权并查集
int find(int x) {
if (x == anc[x]) return x;
int y = find(anc[x]);
if (semi[best[x]] > semi[ best[anc[x]] ])
best[x] = best[anc[x]];
// 利用并查集求出集合里面dfn最小的点 best定义为祖先链中dfn值最小的semi 记录的是dfn不是点值
return anc[x] = y;
}
// 求出支配树
// 原理:
// 1.建出搜索树并算出每个点的时间戳
// 2.根据半必经点定理按时间戳从大到小计算出每个点的半必经点
// 3.根据必经点定理,通过算出的半必经点得出每个点的最近必经点
void Lengauer_Tarjan() {
for (int y = num; y > 1; y--) {
int x = fa[y];
//求出半支配点
for (int i = 0; i < pre[id[y]].size(); i++) {
int z = dfn[ pre[id[y]][i] ];
if (!z) continue;
find(z);
semi[y] = min(semi[y], semi[ best[z] ]);
}
dom[semi[y]].push_back(y);
anc[y] = x;
for (int i = 0; i < dom[x].size(); i++) {
int z = dom[x][i];
find(z);
idom[z] = (semi[ best[z] ] < x) ? best[z] : x; // 半必经点定理
}
dom[x].clear();
}
for (int i = 2; i <= num; i++) {
if (idom[i] != semi[i]) idom[i] = idom[ idom[i] ];
dom[ idom[i] ].push_back(i);
} // 必经点定理
idom[1] = 0;
}
}
using namespace DominantorTree;
void calc(int x) {
sum[id[x]] += id[x];
for (int i = 0; i < dom[x].size(); i++) {
int y = dom[x][i];
sum[id[y]] = sum[id[x]];
calc(y);
}
}
int main() {
while (cin >> n >> m) { // n个点 m条边
init();
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
insert(u, v);
}
dfs(n);//题意要求 根节点为n
Lengauer_Tarjan();
calc(1);
for (int i = 1; i < n; i++) {
cout << sum[i] << ' ';
}
cout << sum[n] << endl;
}
return 0;
}