有向图缩点
关于有向图如何缩点的问题首先我们需要了解一个概念:
强连通与强连通分量
在一个有向图图中如果从任何一个点出发都能到达图中的所有点,即称这个图是强连通的。就好比坐公交车,不管你从哪个站点坐车你都能到达这个线路上的任意一个站,就算是之前的站点也可以坐一整圈到达。
只是这个要求一般难以达到,换做强连通分量则容易达到,一个有向图中取出部分点与边组成新的图,这图是强连通的则称为强连通分量。
强连通图:
若加一个点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呢教我的那位大佬使用的方法是拓扑排序,就具体跑本题来看可以节约一半的时间。
如果有机会以后会补上拓扑的解法。有机会就是遥遥无期