强连通分量
在有向图G中,如果每一个点对 (ui,vi),都既存在从 ui 到 vi 的路径,也存在从 vi 到 ui 的路径,则图G成为强连通图,强连通分量定义为有向图中的极大强连通子图。
容易证明,强连通分量是对有向图点集的一种划分。
强连通分量的求法
对于一个有向图G,想求出G有多少强连通分量,以及每个强连通分量包含哪些顶点,一般我们采用Tarjan算法。(实际还有一种叫Kosaraju的算法,但因为它的时间复杂度常数大,因此没有Tarjan优秀)。
两个重要属性
由于执行Tarjan算法的需要,我们赋予有向图G的每个结点两个属性,用数组表示出来,一个叫 dfn[ ],一个叫 low[ ]。含义如下:
dfn[i]:表示顶点 i 在深度优先搜索树中,被搜索到的次序。它有一个专业术语叫 “时间戳”。
low[i]:表示顶点 i 通过自身或其子节点能回溯到的最小的时间戳。
算法思想:
算法基于深度优先搜索,首先每访问一个新节点,初始化 dfn[i] = low[i] = 访问次序,并把该结点保存在一个栈内。在回溯的过程中更新low[ ] 的值。使得 low[i] 为顶点 i 通过后继能回到的最早的时间戳。
如果更新后的 low[i] 还是等于 dfn[i],则说明:在搜索树中,以顶点 i 为根节点的子树中的顶点属于一个强连通分量。此时需要将栈中 i 顶点以及 i 顶点上方的所有顶点出栈并染色。
代码
void Tarjan(int u)
{
dfn[u] = ++deep;
low[u] = deep;
stack[++top] = u;
vis[u] = 1;
int sz = G[u].size();
for(int i=0;i<sz;i++)
{
int v = G[u][i];
if(dfn[v]==0)
{
Tarjan(v);
low[u] = min(low[u], low[v]);
}
else
{
if(vis[v]==1)
low[u] = min(low[u], low[v]);
}
}
if(dfn[u]==low[u])
{
color[u] = ++sum;
Color[sum].push_back(u);
vis[u] = 0;
while(stack[top]!=u)
{
color[stack[top]] = sum;
Color[sum].push_back(stack[top]);
vis[stack[top]] = 1;
top--;
}
top--;
}
}
由于原图不一定连通,所以要枚举所有结点,如果dfn[i] = 0 则执行Tarjan。
每个结点访问一次,因此这个算法的时间复杂度是线性的。
缩点
由强连通分量的性质:“任意两个结点可以相互到达。” 可知,如果能到达强连通分量的任何一个顶点,就可以到达该强连通分量的任何结点。在研究某些问题(尤其DP居多),习惯上就直接把强连通分量视为一个顶点。我们叫 “缩点” 操作,其本质是根据Tarjan求强连通分量后得到的染色信息重新构图。
易知经过缩点后得到的图为DAG
//获得重构图,a[],和b[]表示一条边连接的两个顶点
//我的方法是我自己的习惯,应该比较麻烦,对不同的题目可能有更好的处理方法
void Get_TG()
{
for(int i=1;i<=r;i++)
{
if(color[a[i]]!=color[b[i]])
{
int sz = TG[color[a[i]]].size(), ok = 0;
for(int j=0;j<sz;j++)
{
if(TG[color[a[i]]][j]==color[b[i]])
{
ok = 1;
break;
}
}
if(!ok)
{
TG[color[a[i]]].push_back(color[b[i]]);
rdu[color[b[i]]]++;
}
}
}
}
例题
缩点+简单DP:洛谷P3387 【模板】缩点.
缩点:洛谷P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G.
缩点:洛谷P1262 间谍网络.
缩点+简单DP:洛谷P3627 [APIO2009]抢掠计划.
缩点+DAG最长链计数:洛谷P2272 [ZJOI2007]最大半连通子图.