强连通分量
强连通的定义是:有向图 G 强连通是指,G 中任意两个结点连通。
强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图。
定义来源OI Wiki
这里先介绍Tarjan算法
在深度优先搜索的基础上,我们添加两个新的数组:dfn和low
dfn是当前结点的时间戳(遍历到这个结点的时间),也就是遍历结点的次序
low记录当前结点能到达的时间戳最早的结点(初始默认为自身的dfn)
按照深度优先搜索遍历一个有向图
若一个结点u能到达其祖先结点v,也就是说当前结点u访问的结点v在之前已经被访问过(dfn[v] < low[u])
那么该结点的low[u]应改为dfn[v],但是v结点在之前也有可能会达到v自身的祖先结点,low[v]的值可能会更早,所以正确的改变应该是low[u] = low[v]
当一个结点无法继续遍历下去返回的时候,我们可以查看当前结点的dfn和low是否相等
若相等则表明该结点是一个强连通分量的起点,可以将所有low值为当前dfn值的结点记为同一个强连通分量中
此时我们可以借助栈来存储所有访问的结点(因为存储是按照访问次序存储,子结点入栈的时间在父节点后面,先进先出满足父节点找到所有儿子结点的条件)
当遍历返回时若当前结点u的low==dfn则把栈中所有low==当前dfn的结点v 与当前结点u 纳入同一强连通分量,直到在栈中找到u为止(此时再出栈就是u结点的父亲了)
void Tarjan(int u)
{
//tim为全局变量,每访问一个新的结点就自增一次,代表当前结点的时间戳
dfn[u] = low[u] = ++tim;
//手动模拟一个栈来记录深度优先遍历按次序入栈的结点
_stack[++top] = u;
//inst记录当前结点是否在栈中,方便后面判断结点是否访问到其祖先
inst[u] = true;
for(int i = head[u];i;i = edge[i].next)
{
int v = edge[i].to;
//若当前结点还没有被遍历到则从当前继续遍历
if(!dfn[v])
{
Tarjan(v);
//返回时更新low值,v是子节点,子节点能到达的low父结点u一定能到达,记录最小值即可
low[u] = min(low[u],low[v]);
}
else if(inst[v])
{
//如果v结点在栈中则表示v是u的祖先,同样记录最小值即可
low[u] = min(low[u],low[v]);
}
}
//若low == dfn,则表示当前结点不能访问到更早的结点,开始记录一个强连通分量
if(low[u] == dfn[u])
{
//tot代表当前强连通分量的id
tot++;
int v;
while(v = _stack[top--])
{
//栈中元素依次出栈,belong数组记录该结点属于哪一个强连通分量
belong[v] = tot;
inst[v] = false;
if(v == u) break;
}
}
}
缩点
缩点,就是把有向图图中的一个个环缩成一个个点,然后就变成了一个有向无环图,这样就会方便后续的某些操作
对于图中的某个强连通分量,我们可以用其中的任意一点来代表这整个分量,然后将其他的点去掉,这样就达到了缩点的目的
具体实现:先求出所有的强连通分量,然后按照强连通分量的个数重新建图,新的图中的每个点代表原来的一整个连通分量
//G->旧图,T->新图
Tarjan(1,1);//缩点
//按照旧图遍历,保证顺序
for(int u = 1;u <= n;u++)
{
for(int i = G_head[u];~i;i = G_edge[i].next)
{
int v = (G_edge[i].to);
//若当前两个点不属于同一强连通分量则连一条边
if(belong[u] != belong[v])
{
T_ADD(pu,pv);//新建图
}
}
}
//后续可以对新图进行一系列操作
割点
在一个无向连通图中,若删掉某个点后该图不再连通,则称该点为割点
这里的low数组代表最多经过一条非树边(也就是无向图中的环)所到达的最小的dfn
由于这里low数组的定义和前面的不太一样,所以当我们访问到已经访问过的结点v时,不能在用low[v]来更新low[u],而是要用dfn[v],因为无向图中访问是双向的,访问到一个已经访问过的结点(不考虑回边)就说明已经成了环,也就是上面所说的非树边
当前结点为u,目标结点为v,若v没被访问过,则先访问v,然后low[u] = min(low[u],low[v])
若v被访问过,则low[u] = min(low[u],dfn[v])
我们考虑割点的定义,这个点可以把一个连通分量分为两个连通分量,也就是说该点的low值是两个连通分量中较小的一个,进一步说明若有一点u,存在u的一个子结点v使得low[v] >= dfn[u],那么这个v就是割点,因为low[v] >= dfn[u]就代表着v无法不经过u来到达比dfn[u]更小的结点,所以删掉u之后v就不能到达u及u之前的点
第二种情况很简单,如果u是根节点并且u的孩子数量大于2,就说明该点也是割点
void Tarjan(int u,int pre)
{
int child = 0;
dfn[u] = low[u] = ++tim;
for(int i = head[u];i;i = edge[i].next)
{
int v = edge[i].to;
if(!dfn[v])
{
Tarjan(v,u);
if(u == root)
{
child++;
}
low[u] = min(low[u],low[v]);
//割点的第一种判断
if(low[v] >= dfn[u] && u != root)
{
iscut[u] = true;
}
}
else if(v != pre)
{
//这里是与前面不同的地方,最多只能走一条非树边
low[u] = min(low[u],dfn[v]);
}
}
//割点的第二种判断
if(u == root && child >= 2)
{
iscut[u] = true;
}
}
桥(割边)
原理基本和割点一样,但是判断上要改为low[v] > dfn[u],而且少了特判根结点的情况
因为这里如果low[v] == dfn[u]代表v可以回到u结点,如果v没有其他的路回到祖先结点或父结点u,那么这条(u,v)边就是割边
void Tarjan(int u,int pre)
{
dfn[u] = low[u] = ++tim;
for(int i = head[u];i;i = edge[i].next)
{
int v = edge[i].to;
if(!dfn[v])
{
Tarjan(v,u);
low[u] = min(low[u],low[v]);
//这里改为大于
if(low[v] > dfn[u])
{
iscut[u] = true;
}
}
else if(v != pre)
{
low[u] = min(low[u],dfn[v]);
}
}
}
模板题懒得放了,等做到好题了想起来再发吧