NuptOJ1044连通 OR 不连通——并查集+逆序

74 篇文章 0 订阅
44 篇文章 1 订阅

连通 OR 不连通

Time Limit(Common/Java):1000MS/3000MS          Memory Limit:65536KByte
Total Submit:284            Accepted:57

Description

给定一个无向图,一共n个点,请编写一个程序实现两种操作:

D x y 从原图中删除连接x,y节点的边。

Q x y 询问x,y节点是否连通

Input

第一行两个数n,m(5<=n<=40000,1<=m<=100000)

接下来m行,每行一对整数 x y (x,y<=n),表示x,y之间有边相连。保证没有重复的边。

接下来一行一个整数 q(q<=100000)

以下q行每行一种操作,保证不会有非法删除。

Output

按询问次序输出所有Q操作的回答,连通的回答C,不连通的回答D

Sample Input

3 3
1 2
1 3
2 3
5
Q 1 2
D 1 2
Q 1 2
D 3 2
Q 1 2

Sample Output

C
C
D

Source

NUAA


分析:显然这类题目不能直接用二维数组的值为0或者1来判断两点是否连通。因为1-2,2-3,则1与3也是连通的。

用到并查集。只要判断2点的根是否相同来判断是否连通。

引用:http://www.cnblogs.com/cyjb/p/UnionFindSets.html


并查集的实现原理也比较简单,就是使用树来表示集合,树的每个节点就表示集合中的一个元素,树根对应的元素就是该集合的代表,如图 1 所示。

图 1 并查集的树表示

图中有两棵树,分别对应两个集合,其中第一个集合为  {a,b,c,d} ,代表元素是  a ;第二个集合为  {e,f,g} ,代表元素是  e

树的节点表示集合中的元素,指针表示指向父节点的指针,根节点的指针指向自己,表示其没有父节点。沿着每个节点的父节点不断向上查找,最终就可以找到该树的根节点,即该集合的代表元素。

现在,应该可以很容易的写出 makeSet 和 find 的代码了,假设使用一个足够长的数组来存储树节点(很类似之前讲到的静态链表),那么 makeSet 要做的就是构造出如图 2 的森林,其中每个元素都是一个单元素集合,即父节点是其自身:

图 2 构造并查集初始化

相应的代码如下所示,时间复杂度是  O(n)

1
2
3
4
5
6
const  int  MAXSIZE = 500;
int  uset[MAXSIZE];
 
void  makeSet( int  size) {
     for ( int  i = 0;i < size;i++) uset[i] = i;
}

接下来,就是 find 操作了,如果每次都沿着父节点向上查找,那时间复杂度就是树的高度,完全不可能达到常数级。这里需要应用一种非常简单而有效的策略——路径压缩。

路径压缩,就是在每次查找时,令查找路径上的每个节点都直接指向根节点,如图 3 所示。

图 3 路径压缩

我准备了两个版本的 find 操作实现,分别是递归版和非递归版,不过两个版本目前并没有发现有什么明显的效率差距,所以具体使用哪个完全凭个人喜好了。

1
2
3
4
5
6
7
8
9
10
int  find( int  x) {
     if  (x != uset[x]) uset[x] = find(uset[x]);
     return  uset[x];
}
int  find( int  x) {
     int  p = x, t;
     while  (uset[p] != p) p = uset[p];
     while  (x != p) { t = uset[x]; uset[x] = p; x = t; }
     return  x;
}

最后是合并操作 unionSet,并查集的合并也非常简单,就是将一个集合的树根指向另一个集合的树根,如图 4 所示。

图 4 并查集的合并

这里也可以应用一个简单的启发式策略——按秩合并。该方法使用秩来表示树高度的上界,在合并时,总是将具有较小秩的树根指向具有较大秩的树根。简单的说,就是总是将比较矮的树作为子树,添加到较高的树中。为了保存秩,需要额外使用一个与 uset 同长度的数组,并将所有元素都初始化为 0。


这道题目,用并查集就很方便了。首先保存输入的相连的边,再保存操作位D的边。一开始连通的边为m行输入的相连的边减去 D操作的边。

我们倒过来操作:

Q 1 2

D 3 2

Q 1 2

D 1 2

Q 1 2

记录是C还是D,然后再逆序输出。就是需要的答案了。

因为要删除边的的反过来即为连通边(用并查集的合并即可实现)。删除边的操作在并查集里很难实现。


总结:所以一开始将边连通起来(一开始输入的m行相连的边减去 D操作的边):一开始这些边全部保存在数组line里面,对它进行从小到大排序。去掉相同的。

由于题目中:保证不会有非法删除

所以很显然D操作的边在之前的m行输入的相连的边中肯定出现过。由于已经排序好了,所以只要去掉相邻的相同的line[],剩下的用并查集的 并 连接起来。


对q行反过来分析,若为Q操作,则判断这2个点的根是否相同,相同即为连通,不同则为不连通;若为D操作,则连通这两点。

输出的话 再逆序 一下。


细节:有比较多的细节要注意。首先是从小到大排序。注意sort函数的写法。

其次数组要开的比较大。

    关于并查集的 Find() / Union / 还有初始化都有模板可以用。

输入字符的时候,也有细节要注意。因为之前输入会有一个 '\n' ,所以先要清空缓存或者用getchar() 。

排序之后去掉line数组相同的元素要小心。

     2个逆序需要注意。

#include<iostream>
#include<algorithm>
using namespace std;
#define SWAP(a, b) {int tmp=a; a=b; b=tmp;}

//连通 OR 不连通

struct LINE
{
	int x;
	int y;
	/*
	bool operator < (const LINE & tmp)const
	{
	if(tmp.x == x)
	return tmp.y > y;
	return tmp.x > x;
	}
	*/
}line[200000];
struct SAVE
{
	char sjh;
	int x;
	int y;
}save[200000];

int father[200000], len[200000];
char output[200000];

bool cmp(LINE a, LINE b)
{
	if(a.x == b.x) return a.y < b.y;
	return a.x < b.x;
}

int find(int x)
{
	if(x != father[x])
		father[x] = find(father[x]);
	return father[x];
}

void un(int x, int y)
{
	int a = find(x);
	int b = find(y);
	if(len[a] > len[b])
		father[b] = a;
	else
		father[a] = b;
	if(len[a] == len[b])
		len[a] ++;
}

int main()
{
	int n, m, a, b, q;
	char s;
	scanf("%d%d",&n,&m);
	for(int i=0;i<=n;i++) // init
	{
		father[i] = i;
		len[i] = 0;
	}
	for(int i=0;i<m;i++)
	{
		scanf("%d%d",&a,&b);
		if(a > b) SWAP(a, b); // a^=b^=a^=b;
		line[i].x = a;
		line[i].y = b;
	}
	scanf("%d",&q);

	for(int i=0;i<q;i++)
	{
		getchar(); // fflush(stdin);
		scanf("%c%d%d",&s,&a,&b);
		if(a > b) SWAP(a, b);
		if(s == 'D')
		{
			save[i].sjh = 'D'; save[i].x = a; save[i].y = b;
			line[m].x = a; line[m].y = b;
			m ++;
		}
		else
		{
			save[i].sjh = 'Q'; save[i].x = a; save[i].y = b;
		}
	}
	sort(line, line+m, cmp);

	for(int i=0;i<m;i++){
		if( i+1 < m && line[i].x == line[i+1].x && line[i].y == line[i+1].y)
		{
			line[i].x = line[i+1].x = -1;
			//edge[i].e = edge[i+1].e = -1;
		}
	}
	for(int i=0;i<m;i++){
		if(line[i].x != -1)
			if(find(line[i].x) != find(line[i].y))
				un(line[i].x, line[i].y);
	}
	int t = 0;

	for(int i=q-1;i>=0;i--) // 从后往前查看
	{
		if(save[i].sjh == 'Q')
		{
			if(find(save[i].x) == find(save[i].y))
				output[t++] = 'C';
			else
				output[t++] = 'D';
		}
		else
		{
			if(find(save[i].x) != find(save[i].y))
				un(save[i].x, save[i].y);
		}
	}
	for(int i=t-1;i>=0;i--)
		printf("%c\n",output[i]);
	return 0;
}


  

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值