什么是支配树?支配树是什么?XD
对于一张有向图(可以有环)我们规定一个起点r(为什么是r呢?因为网上都是这么规定的),从r点到图上另一个点w可能存在很多条路径(下面将r到w简写为r->w)。
如果对于r->w的任意一条路径中都存在一个点p,那么我们称点p为w的支配点(当然这也是r->w的必经点),注意r点不讨论支配点。下面用idom[u]表示离点u最近的支配点。
对于原图上除r外每一个点u,从idom[u]向u建一条边,最后我们可以得到一个以r为根的树。这个树我们就叫它“支配树”。
这个东西看上去有点眼熟?
支配点和割点(删掉后图联通块数增加)有什么区别?
我们考虑问题给定一个起点r和一个终点t,询问删掉哪个点能够使r无法到达t。
很显然,我们删掉任意一个r->t的必经点就能使r无法到达t,删掉任意一个非必经点,r仍可到达t。
从支配树的角度来说,我们只需删掉支配树上r到t路径上的任意一点即可
从割点的角度来说,我们是不是只需要考虑所有割点,判断哪些割点在r->t的路径上即可?是否将某个割点删掉即可让r无法到达t?
这当然是不正确的,我们可以从两个方面来说明它的错误:
所以我们没有办法使用割点来解决这个问题。
对于一棵树,我们用r表示根节点,u表示树上的某个非根节点。很容易发现从r->u路径上的所有点都是支配点,而idom[u]就是u的父节点。
这个可以在 O(n) 的时间内实现。
因为是有向无环图,所以我们可以按照拓扑序构建支配树。
假设当前我们构造到拓扑序中第x个节点编号为u,那么拓扑序中第1 ~ X-1个节点已经处理好了,考虑所有能够直接到达点u的节点,对于这些节点我们求出它们在支配树上的最近公共祖先v,这个点v就是点u在支配树上的父亲。
如果使用倍增求LCA,这个问题可以在 O((n+m)log2n) 的时间内实现。
对于这两个问题我们能够很简便的求出支配树。
对于一个有向图,我们应该怎么办呢?
我们可以考虑每次删掉一个点,判断哪些点无法从r到达。
假设删掉点u后点v无法到达,那么点u就是r->v的必经点(点u就是v的支配点)。
这个方法我们可以非常简单的在 O(nm) 的时间内实现。
其中 n 是点数, m 是点数。
这里,我将介绍Lengauer-Tarjan算法。
这个算法能在很快的时间内求出支配树。
要介绍这个算法我们还需引入两个定理和一些概念
首先来介绍一些这个算法的大概步骤
我们用idom[x]表示点x的最近支配点,用semi[x]表示点x的半必经点。
那什么是半必经点呢?
对于一个节点 Y ,存在某个点 X 能够通过一系列点 pi (不包含 X 和 Y )到达点 Y 且 ∀i dfn[i]>dfn[Y] ,我们就称 X 是 Y 的半必经点,记做 semi[Y]=X
当然一个点 X 的“半必经点”会有多个,而且这些半必经点一定是搜索树中点 X 的祖先(具体原因这里不做详细解释,请自行思考)。
对于每个点,我们只需要保存其半必经点中 dfn 最小的一个,下文中用 semi[x] 表示点 x 的半必经点中 dfn 值最小的点的编号。
我们可以更书面一点的描述这个定理:
在这些必经点中,我们仅需要 dfn 值最小的
这个半必经点有什么意义呢?
我们求出深搜树后,考虑原图中所有非树边(即不在树上的边),我们将这些边删掉,加入一些新的边 {(semi[w],w)|w∈V and w≠r} ,我们会发现构建出的新图中每一个点的支配点是不变的,通过这样的改造我们使得原图变成了DAG
是否接下来使用DAG的做法来处理就可以做到 nlog2n 呢?我没试过,不过我有更好的方法。
一个点的半必经点有可能是一个点的支配点,也有可能不是。我们需要使用必经点定理对这个半必经点进行修正,最后得到必经点
对于一个点 X ,我们考虑搜索树上 semi[X] 到 X 路径上的所有点 p0,p1,p2,p3...pk 。对于所有 pi(1≤i<k) ,我们找出 dfn[semi[pi]] 最小的一个 pi 记为 Z
对于求半必经点与必经点我们都需要处理一个问题,就是对于一个节点 X 的前驱 Y ,我们需要计算 Y 在搜索树上所有 dfn 值大于 dfn[X] 的祖先中 semi 值最小的一个,我们可以按 dfn 从大到小的顺序处理,使用并查集维护,这样处理到节点 X 值时所有 dfn 值比 X 大的点都被维护起来了。
对于 Y 的所有满足条件的祖先,就是在并查集中 Y 的祖先,可以通过带权并查集的方法,维护祖先中的最小值,并记下 semi 最小的具体是哪个节点。
这样我们就能够在 O((n+m)×α(n)) 时间内解决这个问题。
这里提供 Codechef May15 中 GRAPHCNT 题目的代码
#include
#include
using namespace std;
typedef long long lld;
const int MaxN = 100000 + 10, MaxE = (5 * 100000) * 2 + MaxN;
int head[MaxN], pre[MaxN], dom[MaxN], to[MaxE], nxt[MaxE], top;
void addedge(int *h,int fr,int tt)
{
top ++;
nxt[top] = h[fr];
to[top] = tt;
h[fr] = top;
}
int n, m;
void init()
{
scanf("%d%d", &n, &m);
int a, b;
for(int i = 1; i <= m; ++i)
{
scanf("%d%d", &a, &b);
addedge(head, a, b);
addedge(pre, b, a);
}
}
int bcj[MaxN], semi[MaxN], idom[MaxN], best[MaxN], dfn[MaxN], id[MaxN], fa[MaxN], dfs_clock;
int push(int v)
{
if(v == bcj[v]) return v;
int y = push(bcj[v]);
if(dfn[semi[best[bcj[v]]]] < dfn[semi[best[v]]]) best[v] = best[bcj[v]];
return bcj[v] = y;
}//带权并查集路径压缩
void dfs(int rt)
{
dfn[rt] = ++dfs_clock;
id[dfs_clock] = rt;
for(int i = head[rt]; i; i = nxt[i])
if(!dfn[to[i]])
{
dfs(to[i]);
fa[to[i]] = rt;
}
}//求出dfs序
void tarjan()
{
for(int i = dfs_clock, u; i >= 2; --i)
{//按dfs序从大到小计算
u = id[i];
for(int j = pre[u]; j; j = nxt[j])//semi
{
if(!dfn[to[j]]) continue;
push(to[j]);
if(dfn[semi[best[to[j]]]] < dfn[semi[u]]) semi[u] = semi[best[to[j]]];
}
addedge(dom, semi[u], u);
bcj[u] = fa[u];u = id[i - 1];
for(int j = dom[u]; j; j = nxt[j])//idom
{
push(to[j]);
if(semi[best[to[j]]] == u) idom[to[j]] = u;
else idom[to[j]] = best[to[j]];
}
}
for(int i = 2, u; i <= dfs_clock; ++i)
{
u = id[i];
if(idom[u] != semi[u]) idom[u] = idom[idom[u]];
}
}
int sons[MaxN];
lld ans;
void calc_son()
{
for(int i = dfs_clock, u; i >= 2; --i)
{
u = id[i];
++ sons[u];
if(idom[u] != 1) sons[idom[u]] += sons[u];
else ans -= ((lld)sons[u] * (lld)(sons[u] - 1)) / 2ll;
}
}
void solve()
{
for(int i = 1; i <= n; ++i) bcj[i] = semi[i] = best[i] = i;
dfs_clock = 0;
dfs(1);
tarjan();
ans = ((lld)dfs_clock * (lld)(dfs_clock - 1)) / 2ll;
calc_son();
cout << ans << endl;
}
int main()
{
init();
solve();
return 0;
}