连通图学习

强连通分量

强连通的定义是:有向图 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]);
		}
	}
}

模板题懒得放了,等做到好题了想起来再发吧

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值