实用算法实现-第 17 篇 强连通分支

本文详细介绍了Kosaraju算法,包括其实现原理、复杂度分析以及在求解强连通分支和判断单向连通性问题中的应用。通过实例解析算法过程,并提供了程序实现。Kosaraju算法虽然需要对原图和逆图进行两次DFS,但复杂度仍为O(V+E)。文章还探讨了如何通过优化算法以提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

求强连通分量的著名算法:Kosaraju算法,Gabow算法和Tarjan算法。其中Kosaraju算法要对原图和逆图都进行一次DFS,而另外两种算法只要进行一次DFS即可。[i]文是介绍Gabow算法的论文。

17.1    Kosaraju算法

Kosaraju算法虽然要进行两次DFS,但是复杂度仍然是O(V+E),而且比较容易理解。

17.1.1   实例

PKU JudgeOnline, 2186, Popular Cows.

17.1.2   问题描述

有一群牛,总数为N。给出牛之间的M个仰慕关系,该关系可以传递,比如:1仰慕2,2仰慕3,那么1也仰慕3,如果一头牛被所有的牛都仰慕,那么它将是最受欢迎的牛,求出有多少牛是最受欢迎的。

先输入N、M,再输入M个仰慕关系。

17.1.3   输入

33

12

21

2 3

17.1.4   输出

1

17.1.5   分析

先对图求强连通分支,将所有强连通子图合并为一个结点,形成一个新图。

不难证明:在图中,如果将强连通分支看做一个结点,那么如果该结点出度不为0,则该SCC中的牛不是被所有牛仰慕的牛。反证法即可证明。

更进一步,不难证明:如果图中有牛被所有牛仰慕,那么有且只有一个出度为0的,且从图中的任何一个结点都能到达(连通性)的SCC,该SCC包含被所有牛仰慕的牛。反证法可以证明。

也可以知道:如果超过一个SCC的出度为0,那么连通性得不到保障。

可以证明:存在所有牛仰慕的牛,当且仅当出度为0的SCC有且只有一个。“仅当”很容易证明,下面证明“当”。假设只有一个SCC出度不为0时,但是却不存在所有牛仰慕的牛。那么必然意味着,有结点不能到达该SCC。因为如果所有结点都能到达该SCC,那么很自然的该SCC就是包含所有被其它所有牛仰慕的牛。在不能到达该SCC的结点中至少存在一个结点,其出度为0。因为如果所有结点的出度都不为0,那么所有结点必然形成一个环。形成环的结点在构造强连通分支的时候是要合并在一起的,矛盾。所以必然至少存在一个结点,其出度为0。假设和结果矛盾,故此,命题得证。

所以要求被所有牛仰慕的牛的个数,只需要求强连通分支,然后统计出度为0的个数。如果个数不为1则输出0。然后找到出度为0的那个SCC,。如果能就输出该SCC的牛的个数。

1.1.6   程序

#include <stdio.h>
#include <string.h>
#define G_size 100000 //边的最大个数
#define V_size 11000 //点的最大个数
 
typedef struct Graph
{
     int id;//记录了结点的序号
     int next;
}Graph;
 
typedef struct Edge
{
     int s, e;
}Edge;
 
EdgeE[G_size];
GraphGA[G_size];
GraphGT[G_size];
int N, M;
int G_end;
int order[V_size];
int id[V_size];//在逆序的时候记录了SCC的序号
int vis[V_size];//遍历图的时候使用
int in[V_size];//计算反向SCC图的入度,也就是逆向之前的SCC图出度
int cnt, scnt, pos;
 
void Insert(int s, int e) //建立原图和逆图
{
     int p;
     p = s;
     while(GA[p].next){
         p = GA[p].next;
     }
     GA[G_end].id = e;
     GA[p].next = G_end;
 
     p = e;
     while(GT[p].next){
          p= GT[p].next;
     }
     GT[G_end].id = s;
     GT[p].next = G_end;
 
     G_end++;
}
 
void DFST(int x) //对原图进行搜索
{
     int p, q;
     vis[x] = 1;
     p = GT[x].next;
     while(p){
         q = GT[p].id;
         if(!vis[q])
         {
              DFST(q);
         }
         p = GT[p].next;
     }
     order[cnt++] = x;
}
 
void DFSA(int x) //对逆图进行搜索
{
     int p, q;
     vis[x] = 1;
     id[x] = cnt;
     p = GA[x].next;
     while (p){
         q = GA[p].id;
         if(!vis[q])
         {
              DFSA(q);
         }
         p = GA[p].next;
     }
}
 
void Solve() //主要过程
{
     int s, e;
     int i;
 
     memset(GA, 0, sizeof(GA));
     memset(GT, 0, sizeof(GT));
     memset(E, 0, sizeof(E));
     G_end = N + 1;
 
     for (i = 0;i < M; i++)
     {
         scanf("%d%d", &s, &e);
         E[i].s = s - 1;
         E[i].e = e - 1;
         Insert(s - 1, e - 1);
     }
 
     memset(vis, 0, sizeof(vis));
     cnt = 0;
     for (i = 0;i < N; i++)
     {
         if(!vis[i])
         {
              DFST(i);
         }
     }
 
     memset(vis, 0, sizeof(vis));
     cnt = 0;
     for (i = N- 1; i >= 0; i--)
     {
         if(!vis[order[i]])
         {
              DFSA(order[i]);
              cnt++;
         }
     }
 
     for (i = 0;i < M; i++)
     {
         s = id[E[i].s];
         e = id[E[i].e];
         if (s!= e)
         {
              in[s]++;
         }
     }
     scnt = cnt;
     cnt = 0;
     for (i = 0;i < scnt; i++){
         if(in[i] == 0)
         {
              pos = i;
              cnt++;
         }
     }
     if (cnt !=1){
         printf("0\n");
     }
     else{
         cnt = 0;
         for (i= 0; i < N; i++){
              if(in[id[i]] == pos)
              {
                   cnt++;
              }
         }
         printf("%d\n",cnt);
     }
}
 
int main()
{
     while (EOF!= scanf("%d %d", &N, &M))
         Solve();
     return 0;
}


17.2    Kosaraju算法判断单向连通性

17.2.1   实例

PKU JudgeOnline, 2762, Going from u to v or from v to u?.

17.2.2   问题描述

给定N个点和这N个点之间的M个有向连接。如果两个点之间能从其中一个点到另一个点,那么这两个点就是单向连通的。问这N个点是不是都是单向连通的。

先输入测试个数。每个测试,先输入N、M,然后是M个连接。

17.2.3   输入

1

33

12

23

31

17.2.4   输出

Yes

17.2.5   分析

可以通过求强连通分支,简化图形,使得每两个结点只有单向连接。

然后使用DFS方法,对新图进行拓扑排序。

不难证明:如果原图是单向连通的,那么拓扑排序之后的结点必有指向下一个结点的连接。这是因为:假设没有这个连接,原图又是连通的,会拓扑排序的定义相违背。拓扑排序:对于有向无回路图,进行排序之和,如果包含边(u, v)那么u就出现在v之前。

同时不难证明:如果原图是单向连通的,那么新图有且只有一个结点的入度为0。首先,强连通分支将所有的回路聚合了,所以新图不存在回路,故此至少有一个结点的入度为0。其次,如果超过一个结点的入度为0,那么这两个结点肯定不能到达彼此。

故此,只需要先求强连通分支,建立新图,然后新图判断入度为0的结点个数是不是只有一个。然后以这个结点为根,DFS遍历新图,进行拓扑排序。最后,判断排序好的结点到下一个结点是不是有连接。

这里用到的强连通算法仍然是Kosaraju算法,但是由于超时,就对原来的程序进行了优化。主要的优化在于加入数组,记录每个结点的最后一个子结点的保存位置。典型的空间换时间。

1.2.6   程序

#include <stdio.h>
#include <string.h>
#include <iostream>
using namespace std;
#define G_size 10100 //边的最大个数
#define V_size 1010 //点的最大个数
 
typedef struct Graph
{
     int id;//记录了结点的序号
     int next;
}Graph;
 
typedef struct Edge
{
     int s, e;
}Edge;
 
EdgeE[G_size];
GraphGA[G_size];
GraphGT[G_size];
int N, M;
int G_end;
int order[V_size];
int id[V_size];//在逆序的时候记录了边所属的SCC的序号
int vis[V_size];//遍历图的时候使用
int in[V_size];//计算SCC图的入度
int cnt, scnt, pos;
int lastSonGA[V_size];
int lastSonGT[V_size];
void Insert(int s, int e) //建立原图和逆图
{
     int p;
     p = lastSonGA[s];
     GA[G_end].id = e;
     GA[p].next = G_end;
     lastSonGA[s] = G_end;
 
     p = lastSonGT[e];
     GT[G_end].id = s;
     GT[p].next = G_end;
     lastSonGT[e] = G_end;
 
     G_end++;
}
void DFST(int x) //对逆图进行搜索
{
     int p, q;
     vis[x] = 1;
     p = GT[x].next;
     while(p){
         q = GT[p].id;
         if(!vis[q])
         {
              DFST(q);
         }
         p = GT[p].next;
     }
     order[cnt++] = x;
}
 
void DFSA(int x) //对原图进行搜索
{
     int p, q;
     vis[x] = 1;
     id[x] = cnt;
     p = GA[x].next;
     while (p){
         q = GA[p].id;
         if(!vis[q])
         {
              DFSA(q);
         }
         p = GA[p].next;
     }
}
int GSCC_end;
GraphGSCC[G_size];
int lastSonGSCC[V_size];
void InsertGSS(int s, int e) //建立原图和逆图
{
     int p;
     p = lastSonGSCC[s];
     GSCC[GSCC_end].id = e;
     GSCC[p].next = GSCC_end;
     lastSonGSCC[s] = GSCC_end;
     GSCC_end++;
}
int TopologcalOrder[V_size];
void DFS_GSS(int x)
{
     int p, q;
     vis[x] = 1;
     p = GSCC[x].next;
     while (p){
         q = GSCC[p].id;
         if(!vis[q])
         {
              DFS_GSS(q);
         }
         p = GSCC[p].next;
     }
     TopologcalOrder[cnt++] = x;
}
int linked[V_size][V_size];
void Solve() //主要过程
{
     int s, e;
     int p;
     int i;
     int fail;
     memset(GA, 0, sizeof(GA));
     memset(GT, 0, sizeof(GT));
     memset(E, 0, sizeof(E));
     for(i = 1;i <= N; i++){
         lastSonGT[i] = i;
         lastSonGA[i] = i;
     }
     G_end = N + 1;
     for (i = 1;i <= M; i++)
     {
         scanf("%d%d", &s, &e);
         E[i].s = s;
         E[i].e = e;
         Insert(s, e);
     }
 
     memset(vis, 0, sizeof(vis));
     cnt = 0;
     for (i = 1;i <= N; i++)
     {
         if(!vis[i])
         {
              DFST(i);
         }
     }
 
     memset(vis, 0, sizeof(vis));
     cnt = 0;
     for (i = N- 1; i >= 0; i--)
     {
         if(!vis[order[i]])
         {
              cnt++;
              DFSA(order[i]);
         }
     }
 
     scnt = cnt;
     for(i = 1;i <= cnt; i++){
         lastSonGSCC[i] = i;
     }
     GSCC_end = cnt + 1;
     memset(GSCC, 0, sizeof(GSCC));
     memset(in, 0, sizeof(in));
     for (i = 1;i <= M; i++)
     {
         s = id[E[i].s];
         e = id[E[i].e];
         if (s!= e)
         {
              InsertGSS(s, e);
              linked[s][e] = 1;
              in[e]++;
         }
     }
     cnt = 0;
     for (i = 1;i <= scnt; i++){
         if(in[i] == 0)
         {
              pos = i;
              cnt++;
         }
     }
     fail = 0;
     if (cnt !=1){
         fail = 1;
     }else{
         memset(vis, 0, sizeof(vis));
         cnt = 0;
         DFS_GSS(pos);
         for(i =scnt - 1; i > 0; i--)
         {
              s = TopologcalOrder[i];
              e = TopologcalOrder[i - 1];
              if(linked[s][e]== 0)// && linked[e][s] == 0)
              {
                   fail = 1;
                   break;
              }
         }
     }
     if(fail ==1)
     {
         printf("No\n");
     }else{
         printf("Yes\n");
     }
}
 
int main()
{
     int cases;
     scanf("%d",&cases);
     for(; cases> 0; cases--){
         scanf("%d%d", &N, &M);
         Solve();
     }
     return 0;
}

17.3    实例

PKU JudgeOnline, 2186, Popular Cows.

PKU JudgeOnline, 2762, Going from u to v or from v to u?.

本文章欢迎转载,请保留原始博客链接http://blog.csdn.net/fsdev/article

[i] Path-Based Depth-first Search for Strong and Biconnected Components.Harold N. Gabow. Information Processing Letters 74 (2000) 107-114.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值