201509-4高速公路(tarjan算法求强联通分量)

强连通分量(取自百度百科):

    有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。

问题描述:

试题编号: 201509-4
试题名称: 高速公路
时间限制: 1.0s
内存限制: 256.0MB
问题描述:

问题描述

  某国有n个城市,为了使得城市间的交通更便利,该国国王打算在城市之间修一些高速公路,由于经费限制,国王打算第一阶段先在部分城市之间修一些单向的高速公路。
  现在,大臣们帮国王拟了一个修高速公路的计划。看了计划后,国王发现,有些城市之间可以通过高速公路直接(不经过其他城市)或间接(经过一个或多个其他城市)到达,而有的却不能。如果城市A可以通过高速公路到达城市B,而且城市B也可以通过高速公路到达城市A,则这两个城市被称为便利城市对。
  国王想知道,在大臣们给他的计划中,有多少个便利城市对。

输入格式

  输入的第一行包含两个整数n, m,分别表示城市和单向高速公路的数量。
  接下来m行,每行两个整数a, b,表示城市a有一条单向的高速公路连向城市b

输出格式

  输出一行,包含一个整数,表示便利城市对的数量。

样例输入

5 5
1 2
2 3
3 4
4 2
3 5

样例输出

3

样例说明


  城市间的连接如图所示。有3个便利城市对,它们分别是(2, 3), (2, 4), (3, 4),请注意(2, 3)和(3, 2)看成同一个便利城市对。

评测用例规模与约定

  前30%的评测用例满足1 ≤ n ≤ 100, 1 ≤ m ≤ 1000;
  前60%的评测用例满足1 ≤ n ≤ 1000, 1 ≤ m ≤ 10000;
  所有评测用例满足1 ≤ n ≤ 10000, 1 ≤ m ≤ 100000。

解题思路:

题目中给出了一个“便利城市对”的概念,简单地说就是A能经过某条路径到达B,B也能经过某条路径到达A,那么二者就构成了一个“便利城市对”,由于强连通分量中,每一对节点都是一个“便利城市对”,而一个强连通分量之外的任何一个节点都不能与该强连通图中的任何一个节点构成“便利城市对”。因此,本题可以转化为求出所有的强连通分量,若一个强连通分量的节点数为n,则其中的“便利城市对”个数为:n*(n-1)/2,将所有强连通分量的“便利城市对”数目求和就是总的结果。

算法:tarjan

算法思想:

首先需要两个数组:dfn[maxn](用于记录当前节点的搜索编号,即时间戳),low[maxn](用于记录当前节点或其子树能能够追溯到的最早的栈中节点的搜索编号),当dfn[u]==low[u]时,得到一个以u为根强连通分量。简单的说,就是当前搜索到的节点是之前已经被搜索过一次的,并且从上一次搜索到现在,还没有遇到当前这种搜到一次又走回来的情况,那么这个从第一次被搜索到现在走过的路径上的每一对节点都可以相互访问(可以想象成一个环),那么我们就得到了一个强连通分量。

举个百度百科上的栗子:

(1)

从1号节点开始深度搜索(DFS),首先dfn[1]=1(第一个时间戳),1号节点入栈,low[1]=1(该节点就是1或以1为根的子树在栈中最早的节点);接着一直深度遍历到节点6,low[6]=dfn[6]=4,此时已经无路可走,只能回溯,发现又回到了6,并且节点6就是6或者以6为根的子树在栈中的最早的节点,也就是说,以6为根,只能得到{6}这个强连通分量,6出栈;

(2)

接着对5号节点进行DFS,发现只能到达已经遍历过而且还不在栈中的节点6,而已经被遍历且不在栈中就以为着该节点已经属于某一个得到了的强连通分量,因此不再考虑这种节点。这样的话,节点5也是无路可走了,只能回溯,此时low[5]=dfn[5],与(1)同理,我们又得到了一个新的强连通分量{5},节点5出栈,回溯到节点3;

(3)

接着对节点3进行DFS,到达节点4,low[4]=dfn[4]=5,4入栈,接着搜索到节点6,在(2)中我们说过,6已经出局了,故此路不通,因此4搜索到节点1,此时发现,节点1虽然被遍历过了,但是它还在栈中,这时节点1是节点4的一颗子树,那么我们就要修改一下low[4]了,比较dfn[1]和当前的low[4]发现dfn[1]更小(说明存在比自己更早被搜索到,但尚未出栈的,也就是说,这个更早的节点一定可以与自己共存在一个强连通分量里),因此low[4]被更新为1,此时low[4]!=dfn[4],故还未得到新的强连通分量,暂时不出栈,回溯到节点3,由于4是3的子树,4在走了一圈后,他的low有所改变(相当于4拓展了新的子树),那么4的子树也必然是3的子树,因此需要把low[3]和low[4]比较,low[3]被更新为更小的1,此时3也已经无路可走,并且low[3]!=dfn[3],同样是尚未得到新的强连通分量,也不出栈,继续回溯到节点1;

(4)

节点1继续DFS,走到尚未被访问的节点2,low[2]=dfn[2]=6节点2继续DFS,走到已经被访问的节点4,并且发现节点4在栈中,由于4是2的子树,low[2]与dfn[4]相比,dfn[4]更小,因此low[2]被更新为5,节点4无路可走,回溯到2,节点2同样无路可走,并且low[2]!=dfn[2]因此尚未得到新的强连通分量,2不出栈,继续回溯到1,此时1也已经无路可走,检查发现low[1]==dfn[1],说明自己回到了自己,又一个新的强连通分量诞生了!,开始退栈到节点1。

由于全部节点已经搜索完毕,因此得到了全部的强连通分量:{1,3,4,2},{5},{6};

注意:因为上图中,所有的强连通分量都是互相连接的,因此可以搜索到全部节点,如果不是这样,比如这道CCF题,那就需要人为遍历所有为被访问过的节点!

总结一下tarjan的思想:从一个没被访问过的节点出发,进行DFS,如果不能走了,就检查当前节点是否是自己或是以自己为根的子树中进入栈最早,并且还在栈中的,如果是,那就是自己回到了自己,得到了一个新的强连通分量,退栈到自己的节点号。然后继续回溯,如果回溯到的节点也无路可走,那就重复刚才的动作,如果有路可走,那就一定要坚持啊!!!因为这是为了找出和自己在同一个强连通分量的所有节点,一个都不能放弃!!!(以上过程需要对所有尚未被访问的节点实施!)

算法:

#include <iostream>
#include <vector>
#include<stack> 
using namespace std;
stack<int> S;
vector<int> edge[10005];
int dfn[10005],low[10005],sum=0,index=0;
bool in_stack[10005];
void tarjan(int u)
{
	index++;
	dfn[u]=index;
	low[u]=index;
	S.push(u);//把此节点压栈 
	in_stack[u]=true;
	for(int i=0;i<edge[u].size();i++)
	{
		int v=edge[u][i];//当前节点的邻接节点
		if(dfn[v]==0)//如果当前节点从未被访问过
		{
			tarjan(v);//深度搜索该邻接节点 
			low[u]=min(low[v],low[u]);//更新low[u]为当前节点以及当前节点子树能追溯到的栈中的最早的节点 
		} 
		else//如果该节点被访问过
		{
			if(in_stack[v])//如果该节点在栈中,说明遇到了之前路径上的节点 
			{
				low[u]=min(low[u],dfn[v]);//更新low[u]为当前节点以及当前节点子树能追溯到的在栈中的最早的节点
			} 
			//如果该节点不在栈中,那就说明遍历到了之前已经作为强连通分量中的节点 
		} 
	}
	//前方无路可走 
	if(low[u]==dfn[u])//找到了最近一次自己返回到自己的情况,得到了一个强连通分量,开始出栈 
	{
		int count=0;
		while(true)
		{
			count++;
			if(S.top()==u) 
			{
				in_stack[u]=false;
				S.pop();
				break;
			}
			else 
			{
				in_stack[S.top()]=false;
				S.pop();
			}
		}
		sum+=count*(count-1)/2;
	} 
}
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=0;i<m;i++)
	{
		int a,b;
		scanf("%d%d",&a,&b);
		edge[a].push_back(b);
	}
	for(int i=1;i<=n;i++)
	{
		dfn[i]=0;
		low[i]=0;
		in_stack[i]=false;
	}
	//in_stack[1]=true;
	for(int i=1;i<=n;i++)
	{
		if(dfn[i]==0) //只要当前节点没被访问过,那就从该节点开始,求强联通分量 
			tarjan(i);
	}
	printf("%d\n",sum);
	return 0;
}

运行结果:

发布了28 篇原创文章 · 获赞 6 · 访问量 2472

猜你喜欢

转载自blog.csdn.net/Kobe_1314/article/details/88599909