畅通工程
Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 57335 Accepted Submission(s): 30658
Problem Description
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
Input
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。
Output
对每个测试用例,在1行里输出最少还需要建设的道路数目。
Sample Input
4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0
Sample Output
1
0
2
998
Hint
Hint
Huge input, scanf is recommended.
题目解析
就是一到比较直接的并查集的题目。
那并查集又是什么呢?
顾名思义
“并” 就是将两个不同集合的元素合并到一个集合里
而“查” 则是查询当前元素在哪个集合里。
于是这个算法就叫做并查集(哈哈,当然这么精辟的总结不是我想出来的啊~~)。
知识点
并查集
为了快速查出某一元素是属于哪一个集合,于是这个算法就应运而生,(当然更重要的是要为最小生成树而服务,这个我以后再讲)
这里的集合在计算机内如何去识别呢?
我们又怎样才能识别不同的元素是处于怎样的集合里呢?
为了解决这个问题,我们选中集合里面的某一个元素作为标志,只要这个标识性元素存在哪里哪里就是某个特定集合。
那么我们再进一步的进行抽象,利用我们接触到的数据结构(没学到也没关系,只是一个形象的比喻,方便理解)–我们把这个标识性的元素作为树的一个根节点,而属于这个集合内的其他元素便作为这颗树的分支节点,于是每个集合的界限就一目了然了。
说的很容易,但是你又怎么能知道每个分支节点(集合内的其他元素)的根节点(标志元素)是谁呢? 每个数自己也不会说话,就是会说话,计算机也不一定听得懂啊,计算机只懂得101010101,其他的对他来说都是外国语,需要别人帮助才能听得懂的啊。
这里就需要借助自己学过的知识来解决这个问题了,
我们先看看我们需要解决什么问题:
1. 我们需要能够从每个分支节点中得到根节点的信息,或者先求次之能够知道根节点信息的其他分支节点。
2. 在知道根节点的或者是其他分支节点信息的同时,还要知道当前节点是谁。
就这么两条,简单来说就是一个能够同时存储两条信息的一个东西
那我首先想到其实是结构体,你想存多少条信息都没问题。
但我们可以有一个更加简便的选择—–数组
我来告诉你为什么
数组下表可用于表示当前节点的信息,而该存储单元就可以用来存储根节点或其他分支节点的信息。
所以数组要比结构体要方便的多。
那好了,肯定会有人问,分直节点的存储单元可以用来存储可以表示根节点的信息,那根节点自己本身的存储单元要怎么用才能让他成为一个让计算机看起来的独一无二的标志呢?
那就是让他自己存储他自己的信息。这样再所有都存储根节点的分支节点中他显得就比较突出啦,为啥呢?因为他的下表和他自己存储的信息是一样啊。这样他不就显得容易辨认了吗。
那问题咱们也说的差不多了,我们来“Show you code”.
再梳理一下,我们现在看来要实现的是两个功能:
1.初期将同一个集合的元素进行录入我们暂且用 join 表示。
2.查找这个这个集合里面的标志元素是谁。我们用
find 表示。
根据模块化的编程思想,我们用两个函数来实现这两个功能。
int mark[num] // 用于记录刚才说到的信息
int find(int x) // 找到该元素所属的集合 (迭代版)
{
int r = x;
while(mark[r] != r) // 没有找到标志元素就接着找(反正我不累)
r = mark[r];
mark[x] = r; // 算了,为了下一次好找一些,我就直接把标志元素是谁
return r; // 这样天大的消息直接告诉你好了。我是雷锋
// 好啦 其实我是叫 路径压缩
}
int find(int x)
{
return mark[x] == x ? x : mark[x] = find( mark[x] ); // 别看我短,其实我也自带路径压缩。
}
void join(int x, int y) // 功能:将两个元素加入同一“集合 ”
{
int r1 = find( x ); // 先找到各自原本属于的集合(标志与元素)
int r2 = find( y );
if( r1 != r2)
mark[x]=r1; //( 更改标志元素 )
}
好啦,到此我们对这个问题就算给出了一个答案。
======================================================================================
跑了这么远,我们还是回到题目上来吧。
结合相应的知识点和题目要求,我们可以知道,这道题的本质就是利用并查集来查询根节点的个数(需要修建的路的条数,就等于根节点的个数再减一,因为作为根节点肯定是没有和其他节点有路可走的)
OK “ show you code ”
#include<stdio.h>
#include<algorithm>
#include<string.h>
#include<iostream>
using namespace std;
int mark[1003];
int find( int x)
{
return mark[x] == x ? x : mark[x] = find( mark[x] );
}
//int find(int x)
//{
// int r=x;
// while(mark[r] !=r)
// r=mark[r];
// return r;
//}
void join(int x, int y)
{
int r1 = find(x);
int r2 = find(y);
if(r1 != r2)
mark[r1] = r2;
}
int main()
{
int n,m;
while(scanf("%d", &n)!=EOF && n)
{
scanf("%d",&m);
for( int i = 1; i <= n; i++)
mark[i] = i;
for( int i = 0; i < m; i++)
{
int u, v;
scanf("%d %d", &u, &v);
if( find(u) != find(v))
join( u, v);
}
// int count=-1; // 统计根节点的个数
// for ( int i = 1; i <= n; i++)
// if( mark[i] == i ) // 利用标志元素的特别之处
// count++;
int a[1003]; // 这是我一开始想到的办法,比较的麻烦
memset( a, 0, sizeof(a));
for( int i = 1; i <= n; i ++)
{
a[i] = find( i );
}
sort( a+1, a+n+1);
int ans=0;
for( int i = 1; i < n; i ++)
{
if( a[i] != a[i+1])
ans++;
}
printf("%d\n" , ans );
}
return 0;
}
若有错误和不足之处欢迎指正~~