Tarjin算法(例题:第八届蓝桥杯国赛C语言B组第4题)

虽然蓝(bao)桥(li)杯已经结束了,但是之前练习的时候做的一个题让我学到了一个新的算法——Tarjin算法。

例题:发现环(第八届蓝桥杯国赛C语言B组第4题)

小明的实验室有N台电脑,编号1~N。原本这N台电脑之间有N-1条数据链接相连,恰好构成一个树形网络。在树形网络上,任意两台电脑之间有唯一的路径相连。

不过在最近一次维护网络时,管理员误操作使得某两台电脑之间增加了一条数据链接,于是网络中出现了环路。环路上的电脑由于两两之间不再是只有一条路径,使得这些电脑上的数据传输出现了BUG。

为了恢复正常传输。小明需要找到所有在环路上的电脑,你能帮助他吗?

输入
-----
第一行包含一个整数N。
以下N行每行两个整数a和b,表示a和b之间有一条数据链接相连。

对于30%的数据,1 <= N <= 1000
对于100%的数据, 1 <= N <= 100000, 1 <= a, b <= N

输入保证合法。

输出
----
按从小到大的顺序输出在环路上的电脑的编号,中间由一个空格分隔。

样例输入:
5
1 2
3 1
2 4
2 5
5 3

样例输出:
1 2 3 5

资源约定:
峰值内存消耗 < 256M
CPU消耗  < 1000ms


一开始用的强行dfs就过了(代码下面给出)

搜了一下说还可以用Tarjin算法:

发现Tarjin算法本质上也是一种dfs。


Tarjin算法:

用途:用于计算有向图的强连通分量(本题为无向图)

先定义两个数组dfn和low

操作步骤:

1.对于第一次搜到的点,将其dfn与low值初始化为第一次找到的时间(步数),将其入栈

2.遍历与这个点(u)相邻的所有点(v),判断此点是否在栈内


在:low[u]=min(low[u],dfn[v]);

不在:Tarjin这个点,low[u]=min(low[u],low[v]);


3.如果一个点的dfn与low值相等,之前的点都是一个强连通分量中的,将之前的点都出栈


注意例题为无向图,注意tarjin时不能向回遍历(会死循环)。

其他就是找出所有强连通分量,其中个数大于1的就是要找的环,压入set中输出就行。

本题实现(343ms):

#include <cstdio>
#include <iostream>
#include <vector>
#include <cstring>
#include <stack>
#include <algorithm>
#include <set>
#define MAX 1000005
using namespace std;
int n,u,v,dfs_cnt=0,scc_cnt=0,index;
int dfn[MAX],low[MAX],belong[MAX],flag[MAX];
vector<int> G[MAX];
stack<int> s;
set<int> ans;

void tarjin(int u,int pre)
{
	dfn[u]=low[u]=++dfs_cnt;
	//初始化这个节点(步骤一)
	s.push(u); //入栈 
	for (int i=0;i<G[u].size();i++)
	{
		int v=G[u][i];
		if (v==pre) continue;
		//防止连回去造成死循环 
		if (!dfn[v]) //之前不在栈内的情况(步骤二)
		{
			tarjin(v,u);
			low[u]=min(low[u],low[v]);
		}
		else //之前在栈内的情况(步骤二)
			low[u]=min(low[u],dfn[v]);
	}
	if (low[u]==dfn[u]) //low与dfn相等 
	{
		scc_cnt++; //强连通分量的标号 
		while(1)
		{
			int x=s.top(); s.pop();
			belong[x]=scc_cnt; //将栈中u之前的数标记为标号 
			if (x==u) break; 
		}
	}
}

int main()
{
	memset(belong,0,sizeof(belong));
	memset(dfn,0,sizeof(dfn));
	memset(low,0,sizeof(low));
	memset(flag,0,sizeof(flag));
	scanf("%d",&n);
	for (int i=1;i<=n;i++)
	{
		scanf("%d%d",&u,&v);
		G[u].push_back(v);
		G[v].push_back(u);
	}
	tarjin(1,0); //令一开始的pre为0 
	for (int i=1;i<=n;i++)
	{
		flag[belong[i]]++;
		if (flag[belong[i]]>1)
		{
			index=belong[i];
			break;
		} //index用于标记这个强连通分量的标号 
	} //找到其中个数大于1个的强连通分量 
	for (int i=1;i<=n;i++)
		if (belong[i]==index)
			ans.insert(i); //用一个set来保存顺便排序 
	for (set<int>::iterator it=ans.begin();it!=ans.end();it++)
		printf("%d ",*it); //输出结果 
	return 0;
}


一开始的dfs事实上更快一点,上述的方法主要用了set,导致时间变长,如果改成数组实现,可以更快一点。

一开始的直接dfs的写法(312ms):

#include <cstdio>
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
int n,x,y,t=-1,flag=0,flag1=0,vis[1000005],ans[1000005],s=1;
vector<int> G[1000005];

void dfs(int pre,int x)
{
	for (int i=0;i<G[x].size();i++)
	{
		if (G[x][i]==pre) continue;
		//防止连回去造成死循环 
		if (vis[G[x][i]]&&!flag)
		{
			flag=1; //标记是否找到环 
			t=G[x][i];
			ans[x]=1;
			return;
		} //发现之前已经标记了说明已经找到了环 
		else if (!vis[G[x][i]]&&!flag)
		{
			vis[G[x][i]]=1;
			dfs(x,G[x][i]);
			vis[G[x][i]]=0;
			if (flag)
			{
				if (!flag1) ans[x]=1; //标记环上的数字 
				if (x==t) flag1=1; //已经标记完,flag1来表示标记完 
				return;
			}
		}
	}
}

int main()
{
	//freopen("Input.txt","r",stdin);  
	//freopen("Output.txt","w",stdout);
	memset(vis,0,sizeof(vis));
	scanf("%d",&n);
	for (int i=1;i<=n;i++)
	{
		scanf("%d%d",&x,&y);
		G[x].push_back(y);
		G[y].push_back(x);
	} //建立无向图 
	vis[1]=1; dfs(0,1);
	for (int i=1;i<=n;i++)
		if (ans[i]) printf("%d ",i);
	return 0;
}

posted on 2018-05-29 21:11  Radium_1209  阅读(137)  评论(0)    收藏  举报

导航