【C++】强连通分量

有向图的强连通分量。

例题

题目链接:http://poj.org/problem?id=2186
vjudge:https://vjudge.net/problem/POJ-2186

在这里插入图片描述

题目大意:

有一群牛,总数为N(N<=10000)。
题目数据为牛之间的关系,比如说1仰慕2,2仰慕3等等,设这种仰慕是可以传递的,如果1仰慕2,那么1也会同时仰慕2仰慕的那些牛。(关系数e<=50000)
如果一头牛被所有的牛都仰慕,那么它将是最受欢迎的牛。
问有多少牛是"最受欢迎的"。

样例数据:
在这里插入图片描述
本例数据中最受欢迎的牛为D牛,E牛。

怎么做?

显然,采用floyd传递闭包方法简单模拟可以解决此问题。时间复杂度o(n^3)。
枚举所有点,反向dfs深度优先遍历图。用邻接表存储边,每次搜索时间复杂度o(n+e),总复杂度o(n*(n+e))。
但是由于数据量巨大(n=10000) ,这两种做法超时的可能性很大。
那么有没有更好的做法呢?

分析

首先让我们考虑问题的简化版本——有向图没有环路的情况,也就是说:
不存在一条仰慕路线,可以从一头牛出发,再回到该牛。即不存在牛互相仰慕的情况。
如果有最受欢迎的牛存在,为保证该牛被所有的牛都仰慕,这个有向图将是连通图,最受欢迎的牛不会仰慕其他牛,即他的出度为零。
最受欢迎的牛被其他所有牛仰慕,即出度为零的点应有且仅有一个。

结论

这让我们就将简化版问题给解决了~
首先,统计出图中出度为0的点。
如果这样的点只有一个,那么从该点出发反向dfs遍历图。如果可以遍历所有点,那么说明这个点就是题目要求的点。
时间复杂度o(n+e)。

不要高兴得太早

上面的结论在有环图是否适用呢?

很可惜,结论不成立。
因为大牛可以互相仰慕,所以若最受欢迎的牛与其他牛互相仰慕,这些牛都是最受欢迎的牛。这样,满足条件的点出度可能不为0,也不止有一个。

怎么办呢?

现在,强连通分量算法出场的时候到了。

定义

下面给出强连通分量的定义:
有向图中, u可达v不一定意味着v可达u. 相互可达则属于同一个强连通分量
Strongly Connected Component,简称SCC

根据定义,在上题中,一组互相仰慕的牛就构成了一个强连通分量(一个大牛)。

缩点法

求出强连通分量有什么用呢?
我们可以将每个强连通分量看作一个内外隔绝的包裹,忽略包裹内部的冗余边,并将这个包裹同外部点的相连的边保留,将其打包压缩成一个新的点存储下来 。
最后再利用这些新的点重新建图 。
这就是缩点法。

原图构建新图

在这里插入图片描述
强连通分量A,B,C压缩成点S1
强连通分量F,G压缩成点S2
强连通分量D,E压缩成点S3

发现

新图与原图有什么不同?
因为强连通分量全部被压缩成点,新图将一定是一张有向无环图。
而对有向无环图的问题结论我们之前已经得出了。

新的结论

很容易得到新的结论:
利用强连通分量的缩点法构建新图,在新图中如果只有一个出度为0的点且该点与其他所有点连通,那么这个强连通分量中的所有点就是题目要求的点。

下面的问题,就是如何求强连通分量了。

强连通分量算法

求强连通分量有三种算法,分别是Kosaraju算法,Gabow算法和Tarjan算法,时间复杂度均为o(n+e)。
其中Kosaraju算法要对原图和逆图都进行一次DFS,另外两种算法只要DFS一次, Gabow算法是Tarjan算法的改进。

下面简单介绍Kosaraju算法。

Kosaraju算法

在这里插入图片描述
我们继续借助DFS,如上图所示,如果从f开始DFS,我们就可以得到包含f和g的一棵DFS树,然后从c出发,得到c和d和h,再从a出发,得到a和e和b,这样我们每次就可以得到一个SCC,如上图所示.

遗憾的是,并不是每个顺序都是“好用”的,如果一开始就不幸选择了从a开始进行遍历,这颗DFS树将包含整个图,也就是说,这次不明智的DFS把所有SCC混在了一起,什么也得到,很明显,我们希望按照SCC图拓扑顺序的逆序进行遍历,这样才能每次DFS得到一个SCC,而不会把两个或者多个SCC混在一起。

于是,我们先对原图进行一次DFS,记录每个点结束递归(出栈)的时间,然后再按照出栈的顺序从晚到早依次对原图的逆图进行DFS,这样每次得到的连通块就是一个强连通分量。

算法步骤:

  • 对有向图进行DFS,记录下顶点变黑的时间A[i]。
  • 改变图G 的每一条边的方向,生成新图GT。
  • 按上次DFS 顶点变黑的时间A[i]由大到小顺序对GT进行DFS。遍历结果构成森林W 。
  • W 中每棵树的结点构成了有向图的一个强连通分量。

DFS:

  • 在演示之前,重新介绍DFS深度优先搜索为结点着色以表示结点的状态的过程。
  • 每个顶点开始均为白色。
  • 搜索中被发现时置为灰色。
  • 结束时又被置成黑色(即当其邻接表被完全检索之后)。

演示:
对G进行dfs,记录下顶点变黑时间A[i]。
在这里插入图片描述
得到A[i]从大到小的顺序:
F G A C D E B
改变图G 的每一条边的方向,生成新图GT
按A[i]由大到小顺序 F G A C D E B 对GT进行第二次DFS
得到森林w
FG||ACB||DE

根据森林w
FG||ACB||DE
由每个强连通分量为搜索树中的一棵子树。 得到已拓扑有序的强连通分量
S2||S1||S3

在这里插入图片描述
总结:
对某些题目,内部点之间的联系不会和外部点作用而影响到结果,那么强连通分量可以缩成一点,考虑为一个整体。
缩点可以简化构图(有环图变无环图),这样更容易发掘出各个强连通分量外部之间的规律。

void dfs(int k) {
	low[k]=dfn[k]=++cnt;
	s.push(k); //k点入栈
	for(int i=h[k]; i!=-1; i=e[i].nt) {
		int v=e[i].to;
		if(!dfn[v]) {
			dfs(v);
			low[k]=min(low[k],low[v]);
		} else if(!belong[v]) low[k]=min(low[k],low[v]);
		//如果v被标记,但是还不属于任何一个SCC,则v还在栈里
	}
	if(low[k]==dfn[n]) {
		scc++;
		for(;;) { //不断弹出栈里的点
			int x=s.top();
			s.pop();
			belong[x]=scc; //编号
			if(x==k) break;
		}
	}
}

Tarjan算法

伟大的Tarjan又给我们提供了一种求强连通分支的方法,通过dfn和low数组,轻而易举的解决了这个问题。

任何一个强连通分量,必定是对原图的深度优先搜索树的子树。若强连通分量C中第一个被发现的是x,则C中其他点都是x的后代。我们希望在x访问完成时立刻输出C,这样就可以在同一棵DFS树种区分开所有SCC了,因此问题关键,是判断一个点是否是一个SCC中最先被发现的点。

void dfs(int x) {
    dfn[x]=low[x]=++t;
    s[++top]=x;
    for(int i=h[x]; i!=-1; i=e[i].nt) {
        int v=e[i].to;
        if(!dfn[v]) {
            dfs(v);
            low[x]=min(low[x],low[v]);
        } else if(!bl[v]) low[x]=min(low[x],dfn[v]);
    }
    if(low[x]==dfn[x]) {
        scc++;
        while(1) {
            int v=s[top--];
            bl[v]=scc;
            sz[scc]++;
            if(v==x) break;
        }
    }
}

例题全代码:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))

using namespace std;

typedef long long ll;

int const N=10005;
int const M=50005;

struct Edge {
    int to,nt;
} e[M<<1];

int cnt,n,m,t,scc,top;
int h[N],bl[N],sz[N],dfn[N],d[N],s[N],low[N];

inline void add(int a,int b) {
    e[cnt].to=b;
    e[cnt].nt=h[a];
    h[a]=cnt++;
}

void dfs(int x) {
    dfn[x]=low[x]=++t;
    s[++top]=x;
    for(int i=h[x]; i!=-1; i=e[i].nt) {
        int v=e[i].to;
        if(!dfn[v]) {
            dfs(v);
            low[x]=min(low[x],low[v]);
        } else if(!bl[v]) low[x]=min(low[x],dfn[v]);
    }
    if(low[x]==dfn[x]) {
        scc++;
        while(1) {
            int v=s[top--];
            bl[v]=scc;
            sz[scc]++;
            if(v==x) break;
        }
    }
}

int main() {
    scanf("%d%d",&n,&m);
    ms(-1,h);
    while(m--) {
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    for(int i=1; i<=n; i++) if(!dfn[i]) dfs(i);
    for(int i=1; i<=n; i++) for(int j=h[i]; j!=-1; j=e[j].nt) {
        int x=i,y=e[j].to;
        if(bl[x]==bl[y]) continue;
        d[bl[x]]++;
    }
    int t=n+1;
    for(int i=1; i<=scc; i++) if(d[i]==0) {
        if(t==n+1) t=i;
            else t=0;
    }
    cout << sz[t] << endl;
    return 0;
}

信息传递(例题)

「NOIP2015提高」信息传递
题目链接:
LOJ:https://loj.ac/problem/2421
UOJ:http://uoj.ac/problem/146
Luogu:https://www.luogu.com.cn/problem/P2661
计蒜客:https://nanti.jisuanke.com/t/T2026
51Nod:https://www.51nod.com/Challenge/Problem.html#problemId=2849
vjudge:https://vjudge.net/problem/LibreOJ-2421
在这里插入图片描述
分析:把整个传递的过程看成是一个有向图,每个点最多只有一条出边,每个点可能有多条入边,缩点以后,我们发现答案就在每个强连通分支里面,又由于边的性质(每个点最多只有一条出边,可能有多条入边),那么每个强连通分支一定是一个简单的环(没有环套环).

方法1:可以直接o(n)循环找环
方法2:先拓扑排序,然后剩下的都是环
方法3:求每个点数不为一的强连通分支里面的最小点数

其他题目:

HDU 1269 判断一个图是否是强连通的
链接:http://acm.hdu.edu.cn/showproblem.php?pid=1269
https://vjudge.net/problem/HDU-1269

HDU 3072 强连通后缩点,DAG的最小树形图
链接:http://acm.hdu.edu.cn/showproblem.php?pid=3072
https://vjudge.net/problem/HDU-3072

POJ 2186 求多少点可以被其他点到达(明星奶牛)
链接:http://poj.org/problem?id=2186
https://vjudge.net/problem/POJ-2186

HDU 5934 缩点后找入读为0的强连通块中最小的值累加
链接:http://acm.hdu.edu.cn/showproblem.php?pid=5934
https://vjudge.net/problem/HDU-5934

HDU 2767 求最少加几条边才能使得有向图变成强联通,先强联通缩点,然后求出DAG,入度为0的点的个数是a个,入度为b的点是b个,那么答案就是max(a,b)
链接:http://acm.hdu.edu.cn/showproblem.php?pid=2767
https://vjudge.net/problem/HDU-2767

UVA 11324 the largest clique 缩点以后变成DAG,发现每个强连通分量要么都选,要么都不选,那么在dag里面,求弱连通的最大点数可以用dp解决
链接:https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=2299
https://vjudge.net/problem/UVA-11324

POJ 3592
链接:http://poj.org/problem?id=3592
https://vjudge.net/problem/POJ-3592

NOIP2009提高 最优贸易
链接:
LOJ:https://loj.ac/problem/2590
Luogu:https://www.luogu.com.cn/problem/P1073
其他OJ不给了。

POJ 2553 The Bottom of a Graph(alpc OJ 1274)(基础)
链接:http://poj.org/problem?id=2553
https://vjudge.net/problem/POJ-2553

POJ 1236 Network of Schools (基础)
链接:http://poj.org/problem?id=1236
https://vjudge.net/problem/POJ-1236

POJ 2762 Going from u to v or from v to u? (中等,弱连通分量 )
链接:http://poj.org/problem?id=2762
https://vjudge.net/problem/POJ-2762

POJ 3160 Father Christmas flymouse(难,DP题)
链接:http://poj.org/problem?id=3160
https://vjudge.net/problem/POJ-3160

POJ 1904 King‘s Quest (难,推荐,非缩点,匹配思想与强连通分量的转化)
链接:http://poj.org/problem?id=1904
https://vjudge.net/problem/POJ-1904

发布了62 篇原创文章 · 获赞 75 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Ljnoit/article/details/104490128