首先解释什么是支配树。
首先我们先看一下DAG,我们现在有一个DAG:
如果我们从1出发,对于图中的一个点
p
p
p,在从1到
p
p
p的不同路径中,总有一些点是必经的,例如图中的8号点,我们发现在从1到8的不管是那一条路径上,5都是必须经过的。(当然1号点和8号点也可以理解为必经的)。对于每一个点,如果把该点向上最近的一个必经点和该点连有向边。我们就得到了这个图的支配树。
还是那这个DAG举例,我们按照上述的方法连边(红线):
红边就是该DAG的支配树。对于一个点
p
p
p,从其到1的树链上的每一个点,都是1到
p
p
p的必经点。
我们仔细观察这个图,我们发现,对于一个点,他最近的必经点(支配树父亲)都是他的所有入点的“LCA”。例如,5号点的入点V= { 6,2,3 },其”LCA”为1。同理8号点,入点的“LCA”为5。那么我们怎么求这个所谓的“LCA”呢?
我们都知道,“LCA”的概念仅存在于树上,对于DAG,对于一个点集没有所谓固定的“LCA”(因为一个点可能不止一个父亲(入点))。但是对于上图,我们可以考虑,如果一个点的所有入点向上的支配树已经建成,那么该点的最近必经点就是这些入点在当前已建支配树上的LCA
有点绕。。。我们看例子,比如现在我们要确定5号点的最近必经点。且V= { 6,2,3 }向上的支配树已经建成。那么此时图是这样的:
从图上非常明显的知道:V= { 6,2,3 }的在已建成的支配树上的LCA为1。这时我们连边<1, 5>
对于其他的点,同理。所有我们知道,我们在依次建立支配树的边时,必须保证该点的所有入点的支配树都已建成。所以,我们必须先处理每个点的入点,在处理该点。。这不就是 拓扑排序么?我们可以按照拓扑顺序处理完这个DAG,建成支配树。下面看一个例题。
P2597 [ZJOI2012]灾难
相信这个题,已经在各种大佬博客、题解中看了不止一遍了。我们如果用上述方法建成该食物网的支配树。那么每个点的答案,就该点的支配树子树的大小了。
我们看代码:首先建图:
void add(int x, int y)
{
ver[++tot] = y;
ne[tot] = he[x];
he[x] = tot;
verf[tot] = x;
nef[tot] = hef[y];
hef[y] = tot;
}
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
int x;
while(scanf("%d", &x), x)
{di[i]++;add(x, i);}
}
可以看到,在建图的时候,我们统计入度,并同时建出反图(一会提到为什么)
然后我们拓扑排序,得到点的拓扑序:
void topu()
{
for (int i = 1; i <= n; i++) if (!di[i])
q.push(i);
while(q.size())
{
int now = q.front();
q.pop();
topp[++cnt] = now;//记录拓扑序
for (int i = he[now]; i; i = ne[i])
{
int y = ver[i];
di[y]--;
if (!di[y])
q.push(y);
}
}
}
然后我们我们按照拓扑顺序建立支配树:
void adda(int x, int y)
{
vera[++tot] = y;
nea[tot] = hea[x];
hea[x] = tot;
}
for (int i = 1; i <= n; i++)
{
int x = verf[hef[topp[i]]];//利用反图,找到topp[i]点的入点
for (int j = hef[topp[i]]; j; j = nef[j])
x = lca(x, verf[j]);//遍历所有入点,算出lca
adda(x, topp[i]);//联边建树
d[topp[i]] = d[x] + 1;
f[topp[i]][0] = x;
for (int j = 1; j <= 25; j++)
f[topp[i]][j] = f[f[topp[i]][j-1]][j-1];//边建树便记录f数组
}
此时hea系列数组记录的就是支配树信息。
最后我们dfs一下支配树,记录子树大小就可以了。
下面是完整ac代码:
#include <iostream>
#include <cstdio>
#include <queue>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include <map>
#include <cstdlib>
#define ll long long
using namespace std;
const int N = 2e5+5;
int tot, he[N], ver[N], ne[N];
int hef[N], verf[N], nef[N];
int tota, hea[N], vera[N], nea[N];
int f[N][30], d[N];
int di[N];
int n,m, cnt, topp[N];
queue<int> q;
void add(int x, int y)
{
ver[++tot] = y;
ne[tot] = he[x];
he[x] = tot;
verf[tot] = x;
nef[tot] = hef[y];
hef[y] = tot;
}
void adda(int x, int y)
{
vera[++tot] = y;
nea[tot] = hea[x];
hea[x] = tot;
}
void topu()
{
for (int i = 1; i <= n; i++) if (!di[i])
q.push(i);
while(q.size())
{
int now = q.front();
q.pop();
topp[++cnt] = now;
for (int i = he[now]; i; i = ne[i])
{
int y = ver[i];
di[y]--;
if (!di[y])
q.push(y);
}
}
}
int lca(int x, int y)
{
if (d[x] > d[y]) swap(x, y);
for (int i = 25; i >= 0; i--)
{
if (d[f[y][i]] < d[x]) continue;
y = f[y][i];
}
if (x == y) return x;
for (int i = 25; i >= 0; i--)
if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
return f[x][0];
}
int siz[N];
int dfs(int now)
{
siz[now] = 1;
for (int i = hea[now]; i; i = nea[i])
{
siz[now] += dfs(vera[i]);
}
return siz[now];
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
int x;
while(scanf("%d", &x), x)
{di[i]++;add(x, i);}
}
topu();
for (int i = 1; i <= n; i++)
{
int x = verf[hef[topp[i]]];
for (int j = hef[topp[i]]; j; j = nef[j])
x = lca(x, verf[j]);
adda(x, topp[i]);
d[topp[i]] = d[x] + 1;
f[topp[i]][0] = x;
for (int j = 1; j <= 25; j++)
f[topp[i]][j] = f[f[topp[i]][j-1]][j-1];
}
dfs(0);
for (int i = 1; i <= n; i++)
printf("%d\n", siz[i]-1);
return 0;
}
很好,我们现在探讨完DAG的情况了,终于进入最普遍的支配树的学习了。即:一般有向图的支配树与支配点(即必经点)。
下面有一个有向图:
(蓝色边为其dfs搜索树的边,为方便每个点的编号都是它的dfn)
这里我引入半支配点的概念:对于图,若存在一个简单路径<x,y>。如果从x到y所经历的所有点(除了x,y)的时间戳都大于y的时间戳,我们认为x为y的半支配点
例如图中的6号点,他的半支配点集为V = {4,7,8,1}。因为从这些点出发都可以找到一个简单路径到达6,且满足上述条件(如图红边所示):
对于每一个点,我们找到他的半支配点集中的dfn最小的一个,记做
s
e
m
i
[
x
]
semi[x]
semi[x]
然后对于每一个<
s
e
m
i
[
x
]
semi[x]
semi[x],
x
x
x>连边。(如图红边所示)
为什么要这么做?因为我们如果把所有非搜索树的边删去,链接<
s
e
m
i
[
x
]
semi[x]
semi[x],
x
x
x>,那么对于每个点,其支配性不变。而又因为,对于每个点
x
x
x,
s
e
m
i
[
x
]
semi[x]
semi[x]都是它搜索树上的祖先。
对于一个点
x
x
x找出所有的边
(
y
,
x
)
(y,x)
(y,x)中对应的
y
y
y
若
d
f
n
[
y
]
<
d
f
n
[
x
]
dfn[y]<dfn[x]
dfn[y]<dfn[x] 且
d
f
n
[
y
]
dfn[y]
dfn[y]比当前找到的
s
e
m
i
[
x
]
semi[x]
semi[x]的
d
f
n
dfn
dfn小 那么就用
s
e
m
i
[
x
]
=
y
semi[x]=y
semi[x]=y
若
d
f
n
[
y
]
>
d
f
n
[
x
]
dfn[y]>dfn[x]
dfn[y]>dfn[x]找打树上
y
y
y的一个祖先
z
z
z 并且
d
f
n
[
z
]
>
d
f
n
[
x
]
dfn[z]>dfn[x]
dfn[z]>dfn[x]比较
s
e
m
i
[
z
]
semi[z]
semi[z]同
s
e
m
i
[
x
]
semi[x]
semi[x]决定是否用前者更新后者。
这时,我们发现图变成了下面的样子:
没错,它变成了一个DAG。结合上述定理(支配性不变)。我直接按照DAG的支配树求法去求这个图的支配树。除此之外,但是我们有更好的办法。
对于一个点的最近支配点,我们记为
i
d
o
m
[
x
]
idom[x]
idom[x],我们知道,如果求出这个玩意,然后对于每个点建边<
i
d
o
m
[
x
]
,
x
idom[x],x
idom[x],x>就是支配树了。。那么我们怎么求
i
d
o
m
[
x
]
idom[x]
idom[x]呢?
(摘自洛谷)
我们令
P
P
P是从
s
e
m
i
[
x
]
semi[x]
semi[x]到
x
x
x的搜索树上路径点集(不包括
s
e
m
i
[
x
]
semi[x]
semi[x]) 而且
z
z
z是
P
P
P中
s
e
m
i
[
z
]
semi[z]
semi[z]最小的点。如果
s
e
m
i
[
z
]
=
s
e
m
i
[
x
]
semi[z]=semi[x]
semi[z]=semi[x]那么
i
d
o
m
[
x
]
=
s
e
m
i
[
x
]
idom[x]=semi[x]
idom[x]=semi[x]否则就是
i
d
o
m
[
x
]
=
i
d
o
m
[
z
]
idom[x]=idom[z]
idom[x]=idom[z]。
我们标注出每个点的支配点(逗号后面):
最后连边建成支配树:
万事大吉了。:
这里给出几个性质:
1、每个点的
s
e
m
i
semi
semi是唯一 的。
2、一个点的
s
e
m
i
semi
semi一定是其dfs树祖先。
3、x的半支配点不一定是x的支配点。
4、semi的深度大于idom的深度
5、如果节点U,V满足,V是U的祖先。那么:可以 得出V是
i
d
o
m
[
U
]
idom[U]
idom[U]的祖先,或者
i
d
o
m
[
U
]
idom[U]
idom[U]是
i
d
o
m
[
V
]
idom[V]
idom[V]的祖先。
P5180 【模板】支配树
下面是ac代码:
// % everyone
#include <cstdio>
#include<iostream>
#include<cstring>
#include <map>
#include <queue>
#include <set>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <vector>
#include <string>
#include <list>
#include <bitset>
#include <array>
#include <cctype>
#include <unordered_map>
#include <time.h>
namespace OI {
double start_time = 0.0;
void read_f(int flag = 0) { freopen("0.in", "r", stdin); if(!flag) freopen("0.out", "w", stdout); }
void fast_cin() { std::ios::sync_with_stdio(false); std::cin.tie(); }
void run_time() { std::cout << "\nESC in : " << ( clock() - start_time ) * 1000.0 / CLOCKS_PER_SEC << "ms" << std::endl; }
void ct() { start_time = clock(); return; }
}
using namespace OI;
template <typename T>
bool bacmp(const T & a, const T & b) { return a > b; }
template <typename T>
bool pecmp(const T & a, const T & b) { return a < b; }
#define ll long long
#define ull unsigned ll
#define _min(x, y) ((x)>(y)?(y):(x))
#define _max(x, y) ((x)>(y)?(x):(y))
#define max3(x, y, z) ( max( (x), max( (y), (z) ) ) )
#define min3(x, y, z) ( min( (x), min( (y), (z) ) ) )
#define pr make_pair
#define pb push_back
using namespace std;
const int N = 2e5+5;
int n, m;
struct Node
{
int tot = 0;
int ver[N<<1], he[N], ne[N<<1];
void add(int x, int y)
{
ver[++tot] = y;
ne[tot] = he[x];
he[x] = tot;
}
}a, b, c, d;
int dfn[N], id[N], fa[N], cnt;
void dfs(int u)
{
dfn[u] = ++cnt; id[cnt] = u;
for (int i = a.he[u]; i; i = a.ne[i])
{
int y = a.ver[i];
if (dfn[y]) continue;
fa[y] = u; dfs(y);
}
}
int semi[N], idom[N], bel[N], val[N];
int fi(int x)
{
if (x == bel[x]) return x;
int fs = fi(bel[x]);
if (dfn[semi[ val[bel[x] ] ] ] < dfn[ semi[val[x] ] ]) val[x] = val[bel[x]];
return bel[x] = fs;
}
void tarjan()
{
for (int s = cnt; s>1; s--)
{
int u = id[s];
for (int i = b.he[u]; i; i = b.ne[i])
{
int y = b.ver[i];
fi(y);
if (dfn[semi[val[y]]] < dfn[semi[u]]) semi[u] = semi[val[y]];
}
c.add(semi[u], u);
bel[u] = fa[u];
u = fa[u];
for (int i = c.he[u]; i; i = c.ne[i])
{
int y = c.ver[i];
fi(y);
if (semi[val[y]] == u) idom[y] = u;
else idom[y] = val[y];
}
}
for (int i = 2; i <= cnt; i++)
{
int u = id[i];
if (idom[u] != semi[u])
idom[u] = idom[idom[u]];
}
}
int ans[N];
void dfs_ans(int u)
{
ans[u] = 1;
for (int i = d.he[u]; i; i = d.ne[i])
{
int y = d.ver[i];
dfs_ans(y);
ans[u] += ans[y];
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int x, y; scanf("%d%d", &x, &y);
a.add(x, y);
b.add(y, x);
}
for (int i = 1; i <= n; i++) semi[i] = bel[i] = val[i] = i;
dfs(1);
tarjan();
for (int i = 2; i <= n; i++) d.add(idom[i], i);
dfs_ans(1);
for (int i = 1; i <= n; i++) printf("%d ", ans[i]);
puts("");
return 0;
}