有向图——缩点

有向图缩点

关于有向图如何缩点的问题首先我们需要了解一个概念:
强连通与强连通分量
在一个有向图图中如果从任何一个点出发都能到达图中的所有点,即称这个图是强连通的。就好比坐公交车,不管你从哪个站点坐车你都能到达这个线路上的任意一个站,就算是之前的站点也可以坐一整圈到达。
只是这个要求一般难以达到,换做强连通分量则容易达到,一个有向图中取出部分点与边组成新的图,这图是强连通的则称为强连通分量。
强连通图:
在这里插入图片描述
若加一个点7进去
在这里插入图片描述
这时图就不是强连通的了,而是有两个强连通分量{7}与{1,2,3,4,5,6}。

我们将每个强连通分量的点集缩成一个点就是缩点了。如何求强连通分量我建议可以跟这两位大佬学习。
bilibili视频[算法]轻松掌握tarjan强连通分量
图论——强连通分量(Tarjan算法)
由于自身实力不够接下来只对自己的代码以及一些难点作出解释:
首先是对用到的数组及变量名作出解释

vector<int>g[N];//原图 
vector<int>s[N];//s[i]:编号为i的强连通分量点集 
vector<int>e[N];//缩点后的新图 
int n,m,cnt,top,inde;//cnt:顺序时间戳,top:栈顶元素下标, inde:强连通分量的编号 
int low[N],dfn[N],Id[N];//dfn:第一次到的时间 low:能访问到的最早的时间,Id编号 
int stack[N];//模拟栈 
bool vis[N];//标记元素是否在栈中 

dnf[N]记录的值不可改变,只由在递归遍历中的顺序决定,low[N]可以改变则由其能连通的最早(小)时间戳决定。

void tarjan(int sta)//求强连通分量 
{
	if(dfn[sta]) return ;//访问过直接返回
	vis[sta]=1; stack[top++]=sta;//标记入栈  
	dfn[sta]=low[sta]=++cnt;//时间戳 
	for(int i=0;i<g[sta].size();i++) 
	{
		int t=g[sta][i];
		if(!dfn[t])//新点未访问过 
		{
			tarjan(t);
			low[sta]=min(low[sta],low[t]);
		}
		else if(vis[t])//新点访问过且此时在栈中 
		low[sta]=min(low[sta],low[t]);
	}
	if(dfn[sta] == low[sta])//强连通分量的顶点
	{
		inde++;
		while(1){
			int x=stack[--top]; vis[x]=0;
			Id[x] = inde; s[inde].push_back(x);
			if(x==sta)break; 
		}
	} 
}

我们用stack模拟栈,来保存我们dfs过程中的遍历过的点,当我们找到一个完整的强连通分量时我们就把这一个点集出栈,并把它们都编号为inde存在Id数组中。
怎样才算找到一个完整的强连通分量呢,只有当我们回溯到强连通分量的顶点时(强连通分量的任何一个点都可以是顶点,这里顶点的含义是在dfs中第一个访问的点,其特点是dfn[sta]=low[sta],即它不能访问到时间戳早于它自身的点了)while循环的跳出条件也是将栈出到sta点(顶点)为止。
关于low的更新只有访问到的点仍在栈中才可以,在栈中的情况下再看low的大小。因为若是访问到的点不在栈中,说明它们不是同一个强连通分量中的点,即使更小更新也是毫无意义的。

建立缩点后新的图

void init()//缩点后建新图 
{
	for(int i=1;i<=n;i++){
		int x=Id[i];
		for(int j=0;j<g[i].size();j++)//遍历原图 
		{
			int y = Id[g[i][j]];
			if(x == y)continue;//如果两点Id编号不同说明不在同一个强连通分量中,建边 
			e[x].push_back(y);
		}
	} 
}

完整代码

#include<vector>
#include<stdio.h>
using namespace std;
const int N=1e4+3;
vector<int>g[N];//原图 
vector<int>s[N];//s[i]:编号为i的强连通分量点集 
vector<int>e[N];//缩点后的新图 
int n,m,cnt,top,inde;//cnt:顺序时间戳,top:栈顶元素下标, inde:强连通分量的编号 
int low[N],dfn[N],Id[N];//dfn:第一次到的时间 low:能访问到的最早的时间,Id编号 
int stack[N];//模拟栈 
bool vis[N];//标记元素是否在栈中 
void tarjan(int sta)//求强连通分量 
{
	if(dfn[sta]) return ;
	vis[sta]=1; stack[top++]=sta;//标记入栈  
	dfn[sta]=low[sta]=++cnt;//时间戳 
	for(int i=0;i<g[sta].size();i++) 
	{
		int t=g[sta][i];
		if(!dfn[t])//新点未访问过 
		{
			tarjan(t);
			low[sta]=min(low[sta],low[t]);
		}
		else if(vis[t])//新点访问过且此时在栈中 
		low[sta]=min(low[sta],low[t]);
	}
	if(dfn[sta] == low[sta])//强连通分量的顶点
	{
		inde++;
		while(1){
			int x=stack[top--]; vis[x]=0;
			Id[x] = inde; s[inde].push_back(x);
			if(x==sta)break; 
		}
	} 
}
void init()//缩点后建新图 
{
	for(int i=1;i<=n;i++){
		int x=Id[i];
		for(int j=0;j<g[i].size();i++)//遍历原图 
		{
			int y = Id[g[i][j]];
			if(x == y)continue;//如果两点Id编号不同说明不在同一个强连通分量中,建边 
			e[x].push_back(y);
		}
	} 
}

int main()
{
	int u,v,i;
	scanf("%d%d",&n,&m);
	for(i=0;i<m;i++)
	{
		scanf("%d%d",&u,&v);
		if(u==v)continue;
		g[u].push_back(v);
	}
	for(i=1;i<=n;i++) tarjan(i);
	init(); 
	return 0;
}

接下来我们来看看缩点实际用法
我是在学校校赛遇到这道题的E-有向图
当时的想法是用BFS把每个点作为起点跑一遍,这样的暴力做法时间复杂度在最坏的情况下(图是强连通的)是O(n*n).对于数据N=1e5的点与V=2e5的边的数据肯定过不了的。于是我就想到的缩点再用BFS暴力出奇迹 以点集为单位肯定能缩短不少时间吧。
上代码

#include<queue>
#include<vector>
#include<stdio.h>
#include<string.h>
using namespace std;
const int N=1e4+3;
vector<int>g[N];//原图 
vector<int>s[N];//s[i]:编号为i的强连通分量点集 
vector<int>e[N];//缩点后的新图 
int n,m,cnt,top,inde;//cnt:顺序时间戳,top:栈顶元素下标, inde:强连通分量的编号 
int low[N],dfn[N],Id[N];//dfn:第一次到的时间 low:能访问到的最早的时间,Id编号 
int stack[N];//模拟栈 
bool vis[N];//标记元素是否在栈中 
void tarjan(int sta)//求强连通分量 
{
	if(dfn[sta]) return ;
	vis[sta]=1; stack[top++]=sta;//标记入栈  
	dfn[sta]=low[sta]=++cnt;//时间戳 
	for(int i=0;i<g[sta].size();i++) 
	{
		int t=g[sta][i];
		if(!dfn[t])//新点未访问过 
		{
			tarjan(t);
			low[sta]=min(low[sta],low[t]);
		}
		else if(vis[t])//新点访问过且此时在栈中 
		low[sta]=min(low[sta],low[t]);
	}
	if(dfn[sta] == low[sta])//强连通分量的顶点
	{
		inde++;
		while(1){
			int x=stack[--top]; vis[x]=0;
			Id[x] = inde; s[inde].push_back(x);
			if(x==sta)break; 
		}
	} 
}
void init()//缩点后建新图 
{
	for(int i=1;i<=n;i++){
		int x=Id[i];
		for(int j=0;j<g[i].size();j++)//遍历原图 
		{
			int y = Id[g[i][j]];
			if(x == y)continue;//如果两点Id编号不同说明不在同一个强连通分量中,建边 
			e[x].push_back(y);
		}
	} 
}
int res; 
bool vis1[N];
void bfs(int x)
{
	queue<int>q;
	int sta=Id[x];//访问所属点集编号,以点集为单位 
	q.push(sta);
	vis1[sta]=1, res+=s[sta].size();
	while(!q.empty())
	{
		int t=q.front();
		q.pop();
		for(int i=0;i<e[t].size();i++)
		{
			if(!vis1[e[t][i]])
			{
				res+=s[e[t][i]].size();
				q.push(e[t][i]);
				vis1[e[t][i]]=1;
			}
		}
	}
}
 
int main()
{
	int u,v,i;
	scanf("%d%d",&n,&m);
	for(i=0;i<m;i++)
	{
		scanf("%d%d",&u,&v);
		if(u==v)continue;
		g[u].push_back(v);
	}
	for(i=1;i<=n;i++) tarjan(i);//每个点都需要跑一遍,因为不能保证1号点可以连通所有点 
	init();
	for(i=1;i<=n;i++)//用初始图的点开始,之后以点集为单位跑bfs缩短时间 
	{
		memset(vis1,0,sizeof(vis1));
		bfs(i);
	}
	printf("%d",res); 
	return 0;
}

实际上BFS并不是正解但谁叫我喜欢BFS呢教我的那位大佬使用的方法是拓扑排序,就具体跑本题来看可以节约一半的时间。
如果有机会以后会补上拓扑的解法。有机会就是遥遥无期

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值