支配树
jiazhaopeng · · 个人记录
支配树能够解决有向图必经点的问题。
定义,定理与约定
-
默认有一个“源点”
s 作为出发点。 -
默认所有节点编号与其
dfs 序相同。 -
定义
u “支配”v 表示如果想要经过v ,那么必须要经过u 。此时称u 是v 的“支配点”。 -
由支配关系所组成的树成为支配树。
-
众多定理...(待补)
DAG 的支配树的构造
外向树的支配树显然是其本身。
我们可以从 DAG 的源点开始,按照拓扑序遍历。当访问到一个点的时候,其最近支配点一定是其所有入边端点的支配树的 LCA(想走到
据此可以构建 DAG 的支配树。
相关题目:P2597 [ZJOI2012]灾难 ; CF757F Team Rocket Rises Again
关键代码:
inline void tuopu() {
que[++rear] = s;
while (front < rear) {
int cur = que[++front];
int lca = 0;//Attention!!
for (register unsigned int i = 0; i < iV[cur].size(); ++i) {
int to = iV[cur][i];
if (!lca) lca = to;
else lca = get_lca(lca, to);//Attention!! : to
}
f[cur][0] = lca;
for (register int i = 1; i <= 20; ++i)
f[cur][i] = f[f[cur][i - 1]][i - 1];
for (register unsigned int i = 0; i < V[cur].size(); ++i) {
int to = V[cur][i];
--d[to];
if (!d[to]) que[++rear] = to;
}
dep[cur] = dep[f[cur][0]] + 1;
}
}
普通有向图的支配树的构造
另一些定义及定理
-
如果有
u -> ... -> v ,且除了u, v ,其余 路径上的点 都满足其dfn 大于dfn[v] ,那么称dfs 序最小的那个u 为v 的“半支配点”,标记为u = sdom[v] 。 -
证明:至少
u 的 dfs 树上父亲fa 可以当作sdom[u] ,因此子树不可能;如果sdom[u] 在其它树上,那么至少能在树上找到一条路径anc -> sdom[u] -> u ,其中anc (u 的祖先)会作为sdom[u] 。
- 保留树边,删除非树边,连接所有的
sdom[cur] -> cur ,显然将形成一个 DAG,并且支配关系不变。
证明:不会
感性证明:我们站在 dfs 树为主体的角度看。显然
sdom[cur] 下面不会出现支配点,因为至少存在树上路径和sdom[cur] -> cur 路径能够到达cur 。所以sdom[cur] 下面的点到cur 的非树边都可以删除。至于sdom[cur] 的上面如果还有向cur 的连边的话,那么为什么sdom[cur] 不是那个祖先呢?
构造方法
需要构造出所有点的半支配点,然后删除原图边,半支配点向该点连边,转化为 DAG 上的问题。
如何构造半支配点?我们按编号从大到小枚举,对于每个点寻找反向边。首先如果对于当前节点
关键代码:
int sdom[N];
int fa[N], mn[N];
int find(int cur) {
if (fa[cur] == cur) return cur;//Attention!!!!!!!!!!!
int faa = fa[cur];
int anc = find(fa[cur]);
MIN(mn[cur], mn[faa]);//Attnetion!!!!!
fa[cur] = anc;
return anc;
}
inline void get_sdom() {
for (register int i = 1; i <= n; ++i) fa[i] = mn[i] = sdom[i] = i;
for (register int i = n; i > 1; --i) {
int res = n;
for (register unsigned int j = 0; j < iV[i].size(); ++j) {
int to = iV[i][j];
if (to < i) MIN(res, to);
else find(to), MIN(res, mn[to]);//Attention!!!!
}
sdom[i] = res; fa[i] = dfa[i];
MIN(mn[i], res);
}
sdom[1] = 0;
}
然后再建出 DAG 跑支配树即可。
模板
//P5180
int dfn[N], dcnt, mp[N], s;
int dfa[N];
void dfs_init(int cur) {
dfn[cur] = ++dcnt;
mp[dcnt] = cur;
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
if (!dfn[to]) dfs_init(to), dfa[dfn[to]] = dfn[cur];
}
}
int sdom[N];
int fa[N], mn[N];
int find(int cur) {
if (fa[cur] == cur) return cur;//Attention!!!!!!!!!!!
int faa = fa[cur];
int anc = find(fa[cur]);
MIN(mn[cur], mn[faa]);//Attnetion!!!!!
fa[cur] = anc;
return anc;
}
inline void get_sdom() {
for (register int i = 1; i <= n; ++i) fa[i] = mn[i] = sdom[i] = i;
for (register int i = n; i > 1; --i) {
int res = n;
for (register unsigned int j = 0; j < iV[i].size(); ++j) {
int to = iV[i][j];
if (!dfn[to]) continue;
if (to < i) MIN(res, to);
else find(to), MIN(res, mn[to]);//Attention!!!!
}
sdom[i] = res; fa[i] = dfa[i];
MIN(mn[i], res);
}
sdom[1] = 0;
}
int d[N];
int que[N], front, rear;
int f[N][21], dep[N];
inline int get_lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
for (register int i = 20; ~i; --i)
if (dep[f[u][i]] >= dep[v]) u = f[u][i];
if (u == v) return u;
for (register int i = 20; ~i; --i)
if (f[u][i] != f[v][i]) u = f[u][i], v = f[v][i];
return f[u][0];
}
inline void tuopu() {
que[++rear] = s;
dep[0] = -1;
while (front < rear) {
int cur = que[++front], lca = 0;
for (register unsigned int i = 0; i < iV[cur].size(); ++i) {
int to = iV[cur][i];
if (!lca) lca = to;
else lca = get_lca(lca, to);
}
f[cur][0] = lca; dep[cur] = dep[lca] + 1;//Attention!!!!!
for (register int i = 1; i <= 20; ++i)
f[cur][i] = f[f[cur][i - 1]][i - 1];
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
--d[to];
if (!d[to]) que[++rear] = to;
}
}
}
int siz[N];
void dfs_siz(int cur) {
siz[cur] = 1;
for (register int i= head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
dfs_siz(to);
siz[cur] += siz[to];
}
}
inline void work() {
memset(head, 0, sizeof(head));
ecnt = 0;
for (register int i = 2; i <= n; ++i) addedge(f[i][0], i);
dfs_siz(s);
}
int ans[N];
int main() {
read(n), read(m);
for (register int i = 1; i <= m; ++i) {
int u, v; read(u), read(v);
addedge(u, v);
}
dfs_init(1); s = dfn[1];
for (register int i = 1; i <= n; ++i) {
for (register int j = head[i]; j; j = e[j].nxt) {
int to = e[j].to;
aded(dfn[to], dfn[i]);
}
}
memset(head, 0, sizeof(head));
ecnt = 0;
for (register int i = 1; i <= n; ++i) {
for (register unsigned int j = 0; j < iV[i].size(); ++j) {
int to = iV[i][j];
addedge(to, i);
}
}
get_sdom();
memset(head, 0, sizeof(head));
ecnt = 0;
for (register int i = 1; i <= n; ++i)
iV[i].clear();
for (register int i = 2; i <= n; ++i)
addedge(sdom[i], i), ++d[i], aded(i, sdom[i]),
addedge(dfa[i], i), ++d[i], aded(i, dfa[i]);//Attention!!!!!!
tuopu();
work();
for (register int i = 1; i <= n; ++i)
ans[mp[i]] = siz[i];
for (register int i = 1; i <= n; ++i)
printf("%d ", ans[i]);//Attention!!!!!!!!!!!!!!
puts("");
return 0;
}
记忆方法
菜菜的我只好死记硬背
DAG 很好理解,就直接把所有前驱(入边)的 LCA 求一求即可。
有向图需要背过的是:
-
一个点的半支配点的定义:如果
sdom[v]=u,那么存在一条u 到v 的路径,路径上其余点的 dfn 都比v 大。如果有多个这样的u ,取dfn最小的那个。 -
结论:仅保留 dfs 树边和
sdom_x \to x 的边的图为 DAG,且这个 DAG 的支配树就是这张有向图的支配树 -
构造方法:按照 dfn 从大到小枚举点,枚举这个点的前驱(入边)。如果前驱 dfn 比它小,直接更新;否则用前驱的所有祖先(枚举顺序可以保证链上点 dfn 大于这个点的 dfn)的 sdom 来更新这个点的 sdom。需要支持动态加边,查链上最值,需要用边带权并查集。