强连通性(poj1236 poj2186)

看了poj 1236后没啥思路,网上搜了下,才大概知道了解法。要用到缩点,入度,出度的概念。打算先按照《数据结构与算法分析》里的算法写一个,要用两次DFS,估计性能比较差吧,先试试看。妈蛋,这一试写了2天,o(╯□╰)o,还好一次AC。

题目大意:有n个学校,学校之间可以传递信息,例如学校 a 可以传达信息给 b , 即a ——> b , 但 b 不一定 能传递信息给 a 。 告诉你每个学校能够向哪些学校传递信息,然后有两个问题:
   问题一:至少要向几个学校传递原始信息,才能保证所有学校都能收到信息。
   问题二:至少要添加多少组关系(每组关系类型如右:a 可以向 b 传递信息),才能保证给任意一个学校原始信息后,其他所有学校都能收到信息,也即使得整个图变成是强连通的。
   解题思路:这道题其实就是一个有n个顶点的有向图,先计算出强连通分量, 然后分别统计出入度为 0 和出度为 0 的强连通分量个数ansA 和 ansB,那么, 问题一的答案就是ansA , 问题二的答案就是max(ansA , ansB),但是有特例:当只有一个强连通分量时,问题二的答案就是0 。(其实原理还不懂o(╯□╰)o)


使用两次DFS,采用并查集存储强连通分量。刚刚通过了,一次AC,好开心!!就是代码有点长。

/*
poj 1236
time : 0ms
memory: 228k
此程序完全是借鉴《数据结构与算法分析》这本书的思路,数据结构是自己想的,用邻接表存储图(主要考虑到图为稀疏图),用最大堆完成第二次DFS的节点选取顺序,用并查集存储强连通分量。
本以为用了两次DFS,时间会很差,没想到时间还挺快的,试了下从网上down下来的用Tarjan算法,时间和内存都差不多。。可能与图的存储方式以及该题的样例大小有关吧
*/
#include <stdio.h>

const int MaxNode = 101;
const int MaxEdge = 10001;

typedef int PriorityQueue[MaxNode];		//优先队列(最大堆)用来第二次DFS的节点选取
typedef int DisjSet[MaxNode];			//用并查集存储强连通分量

struct NodeEntry
{
	int e, next;
};

int Index[MaxNode];				//通过第一次的DFS计算每个节点的序号,
int index;					
int Vis[MaxNode];				//是否已被访问
int Ptr[MaxNode];				//每个节点链表当前访问到的地方,避免重复扫描
int in[MaxNode];				//强连通分量入度(实际只有每个强连通分量的根节点有值)
int out[MaxNode];				//出度
NodeEntry G[MaxNode + MaxEdge];			//原图
NodeEntry G_r[MaxNode + MaxEdge];		//翻转后的图
PriorityQueue H;
DisjSet s;

int max_degree(int a, int b)
{
	return a > b ? a : b;
}

void Init(int N)
{
	int i, k, id;
	
	index = 1;
	for(k = 1; k <= N; ++k)
	{
		G[k].next = 0;
		G_r[k].next = 0;
		Vis[k] = 0;
		Ptr[k] = k;			//指针初始化
		H[k] = k;			//堆初始化
		s[k] = -1;			//并查集初始化
		in[k] = 0;			//出入度初始化为0,表示出入度分别为0,当不为0时,用1来表示
		out[k] = 0;
	}
	
	for(i = 1; i <= N; ++i)
	{		
		while(1)
		{
			scanf("%d", &id);
			if(!id)
				break;
			G[k].e = id;
			G[k].next = G[i].next;
			G[i].next = k;

			G_r[k].e = i;
			G_r[k].next = G_r[id].next;
			G_r[id].next = k++;
		}
	}
	
}

int Find(int v)
{
	if(s[v] < 0)
		return v;
	else
		return s[v] = Find(s[v]);
}

void Union(int root1, int root2)
{
	if(s[root1] < s[root2])
	{
		s[root1] += s[root2];
		s[root2] = root1;
	}		
	else
	{
		s[root2] += s[root1];
		s[root1] = root2;
	}
}

void PercolateDown(int N, int i)
{	
	int FirstElement = H[i];
	int child;
	for(; 2 * i <= N; i = child)
	{
		child = 2 * i;
		if(2 * i + 1 <= N && Index[H[2 * i + 1]] > Index[H[2 * i]])
			++child;
		if(Index[H[child]] > Index[FirstElement])
			H[i] = H[child];
		else
			break;
	}
	H[i] = FirstElement;
}

int DeleteMax(int N)
{
	int i, child;
	int MaxElement = H[1];
	int LastElement = H[N];

	for(i = 1; i * 2 <= N; i = child)
	{
		child = 2 * i;
		if(2 * i + 1 <= N && Index[H[2 * i + 1]] > Index[H[i * 2]])
			++child;
		if(Index[LastElement] < Index[H[child]])
			H[i] = H[child];
		else
			break;
	}
	H[i] = LastElement;
	return MaxElement;
}

void DFS_index(int N, int i)			//第一次DFS,计算每个节点的index
{
	Vis[i] = 1;
	while(G[Ptr[i]].next)	
	{			
		Ptr[i] = G[Ptr[i]].next;	//改变指针位置	
		if(!Vis[G[Ptr[i]].e])		//光这个函数我写加调试用了一天,起初直接 i = G[Ptr[i]].e,结果啊。。。
		{				//在一个子函数中i是不变的,表示在第i个节点链表的遍历
			DFS_index(N, G[Ptr[i]].e);
			Index[G[Ptr[i]].e] = index++;
		}
	}
}

void DFS(int N, int i)				//第二次DFS,合并强连通节点
{	
	int root1, root2;
	Vis[i] = 1;
	
	while(G_r[Ptr[i]].next)	
	{			
		Ptr[i] = G_r[Ptr[i]].next;		
		if(!Vis[G_r[Ptr[i]].e])
		{	
			root1 = Find(i);
			root2 = Find(G_r[Ptr[i]].e);
			Union(root1, root2);
			DFS(N, G_r[Ptr[i]].e);
		}
	}
}

int main()
{
	int i, j, N, root1, root2, k = 0;		//k用来计强连通分量的个数
	int Non_in_cnt = 0, Non_out_cnt = 0,		//Non_in_cnt,Non_out_cnt分别用来计出入度为0的连通分量的个数
	scanf("%d", &N);					
	Init(N);	
	for(i = 1; i <= N; ++i)
	{	
		if(!Vis[i])
		{
			DFS_index(N, i);
			Index[i] = index++;
		}
	}
	for(i = N / 2; i > 0; --i)
		PercolateDown(N, i);
	
	for(i = 1; i <= N; ++i)				//一定注意在第二次DFS之前再初始化一下
	{
		Vis[i] = 0;
		Ptr[i] = i;
	}

	j = N;
	while(j)
	{
		i = DeleteMax(j--);
		if(!Vis[i])
		{
			DFS(N, i);
		}
	}

	for(i = 1; i <= N; ++i)		//将出入度不为0的分量对应的in[],out[]改为1,遍历了所有点和边,感觉可以再优化下
	{
		Ptr[i] = i;
		root1 = Find(i);
		while(G[Ptr[i]].next)
		{
			Ptr[i] = G[Ptr[i]].next;
			root2 = Find(G[Ptr[i]].e);
			if(root1 != root2)
			{
				in[root2] = 1;
				out[root1] = 1;
			}
		}
	}

	for(i = 1; i <= N; ++i)			//计算出入度为0的分量的个数
	{
		if(s[i] < 0)
		{
			++k;
			if(in[i] == 0)
				++Non_in_cnt;
			if(out[i] == 0)
				++Non_out_cnt;
		}
	}

	if(k == 1)				//注意当整个图为强连通时,第二个问题的答案为0,这是个特殊情况
		printf("%d\n%d\n", Non_in_cnt, 0);
	else
		printf("%d\n%d\n", Non_in_cnt, max_degree(Non_in_cnt, Non_out_cnt));
	return 0;
}
心得:这道题花了我2天时间,那个DFS是无参考手写,花了一天时间,感觉自己对递归的理解还是不够,递归先要保证原始函数的完整,再将递归的发生看作是中断,加入到函数的适当位置。

调试的过程还学习到了printf()函数,开始打算用其查看整个过程的当前状态,就因为忽略了其刷新缓冲流的条件,printf()在很多地方并未实际输出到屏幕上,调试过程显得很诡异,哎。。现在论坛里看到如下:


用printf()输出时是先输出到缓冲区,然后再从缓冲区送到屏幕上。

1. 使用fflush(stdout)强制刷新。
2.缓冲区已满。
3.scanf()要在缓冲区里取数据时会先将缓冲区刷新。
4.\n,\r进入缓冲区时。
5.线程结束的时候,如果该线程里也有printf(....);

6. 程序结束时。


用了书上的算法后,打算再学习一下Tarjan算法。百度到一篇博文,感觉还不错,算法学习可以参见BYV的文章。该文真的讲的蛮好的,看了一会儿,发现Num(u),Low(u)等的变量,不就是对书上割点那部分内容的衍生吗(但是Low()的算法貌似略有不同),顿时有了兴趣。再看文章的过程中,我才发现,原来书上的算法叫做Kosaraju算法,也是一种常用的求强连通分量的算法,时间复杂度与Tarjan算法一样,都是O(N + M),且该算法更直观一些,不过Tarjan算法只用一次DFS,而且不用建立逆图,更简洁。在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求最近公共祖先的离线Tarjan算法。

Tarjan算法打算用在poj 2186这道题。

题意分析: 每个Cow都梦想成为牛群里最知名的奶牛,在一个有N(1 <= N <= 10,000) 头牛的牛群里,给出最多M(1 <= M <= 50,000) 个数对(A,B)。

告诉你A认为B是有名的,并且名气可以传递,若果A认为B有名,B认为C有名,则A也会认为C有名,即使在输入的数对中没有给出这段关系。

你的任务是计算出被所有牛认为是有名的牛的个数.


看到Headacher的题解才恍然大悟。

思路分析:

1、假设a和b都是最受欢迎的cow,那么,a欢迎b,而且b欢迎a,于是,a和b是属于同一个连通分量内的点,所有,问题的解集构成一个强连通分量。
2、如果某个强连通分量内的点a到强连通分量外的点b有通路,因为b和a不是同一个强连通分量内的点,所以b到a一定没有通路,那么a不被b欢迎,于是a所在的连通分量一定不是解集的那个连通分量。

3、如果存在两个独立的强连通分量a和b,那么a内的点和b内的点一定不能互相到达,那么,无论是a还是b都不是解集的那个连通分量,问题保证无解。
4、如果图非连通,那么,至少存在两个独立的连通分量,问题一定无解。

原图将会是很大的,我们可以先找到图里的强连通分量,然后使用缩点可以将原图变成DAG,如果出度为 0 的强连通分量的数目只有一个,那么,该强连通分量即是我们所求的(原理可以自己画几个图试一试),就输出这个强连通分量内解的个数,若出度为 0 的强连通分量个数大于1,则无解。另外,出度为 0 的强连通分量个数不可能为 0(若为 0,即每个分量至少一条出边,那么DAG图一定有环,与DAG图自相矛盾)

/*
poj 2186
time: 94ms
memory:792k
*/
#include <stdio.h>

const int MaxN = 10010;
const int MaxM = 50010;

struct NodeEntry
{
	int e, next;
};

NodeEntry G[MaxN + MaxM];		//存储图
int Index;				//用于计算Num
int Cnt;				//计算强连通分量的个数
int Vis[MaxN];	
int Instack[MaxN];			//是否在栈里的flag
int Ptr[MaxN];				//当前节点邻接链表的指针位置,避免重复扫描
int Belong[MaxN];			//此处用Belong数组代替了并查集,Belong数组里存得数字是其所在强连通分量的编号
int Num[MaxN];
int Low[MaxN];
int Stack[MaxN];			//用Stack数组进行简单的栈使用
int Ptr_stack;				//当前栈顶位置

int min_Low(int a, int b)
{
	return b < a ? b : a;	
}

void Init(int N, int M)
{
	int i, j = N + 1, n1, n2;
	Ptr_stack = 0;
	Index = 0;
	Cnt = 1;
	for(i = 1; i <= N; ++i)
	{
		G[i].next = 0;
		Vis[i] = 0;
		Ptr[i] = i;
		Belong[i] = 0;
		Instack[i] = 0;
	}

	for(i = 1; i <= M; ++i)
	{
		scanf("%d%d", &n1, &n2);
		G[j].e = n2;
		G[j].next = G[n1].next;
		G[n1].next = j++;
	}
}

void Tarjan(int i)
{
	int j;
	Vis[i] = 1;
	Num[i] = Low[i] = ++Index;
	Stack[++Ptr_stack] = i;
	Instack[i] = 1;
	while(G[Ptr[i]].next)
	{
		Ptr[i] = G[Ptr[i]].next;
		if(!Vis[G[Ptr[i]].e])
		{
			Tarjan(G[Ptr[i]].e);
			Low[i] = min_Low(Low[i], Low[G[Ptr[i]].e]);
		}
		else if(Instack[G[Ptr[i]].e])
			Low[i] = min_Low(Low[i], Num[G[Ptr[i]].e]);
	}

	if(Num[i] == Low[i])
	{
		do
		{
			j = Stack[Ptr_stack--];
			Instack[j] = 0;
			Belong[j] = Cnt;
		}
		while(i != j);
		++Cnt;
	}
}

int main()
{
	int N, M, i, k, id;
	k = 0;
	scanf("%d%d", &N, &M);
	Init(N, M);
	for(i = 1; i <= N; ++i)
		if(!Vis[i])
			Tarjan(i);

	int *out = new int[Cnt];
	for(i = 1; i < Cnt; ++i)
		out[i] = 0;
	
	for(i = 1; i <= N; ++i)
	{
		Ptr[i] = i;
		while(G[Ptr[i]].next)
		{
			Ptr[i] = G[Ptr[i]].next;
			if(Belong[i] != Belong[G[Ptr[i]].e])
			{
				out[Belong[i]] = 1;
				break;
			}
		}
	}

	for(i = 1; i < Cnt; ++i)
	{
		if(!out[i])
		{
			id = i;
			++k;
		}
	}
	
	if(k > 1)
		printf("0\n");
	else
	{
		k = 0;
		for(i = 1; i <= N; ++i)
		{
			if(Belong[i] == id)
				++k;
		}
		printf("%d\n", k);
	}

	delete[] out;
	return 0;
}


你可能感兴趣的:(强连通性(poj1236 poj2186))