连通 OR 不连通
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;
}