File Transfer
We have a network of computers and a list of bi-directional connections. Each of these connections allows a file transfer from one computer to another. Is it possible to send a file from any computer on the network to any other?
Input Specification:
Each input file contains one test case. For each test case, the first line contains N (2), the total number of computers in a network. Each computer in the network is then represented by a positive integer between 1 and N. Then in the following lines, the input is given in the format:
I c1 c2
where I
stands for inputting a connection between c1
and c2
; or
C c1 c2
where C
stands for checking if it is possible to transfer files between c1
and c2
; or
S
where S
stands for stopping this case.
Output Specification:
For each C
case, print in one line the word "yes" or "no" if it is possible or impossible to transfer files between c1
and c2
, respectively. At the end of each case, print in one line "The network is connected." if there is a path between any pair of computers; or "There are k
components." where k
is the number of connected components in this network.
Sample Input 1:
5
C 3 2
I 3 2
C 1 5
I 4 5
I 2 4
C 3 5
S
Sample Output 1:
no
no
yes
There are 2 components.
Sample Input 2:
5
C 3 2
I 3 2
C 1 5
I 4 5
I 2 4
C 3 5
I 1 3
C 1 5
S
Sample Output 2:
no
no
yes
yes
The network is connected.
集合的定义和并查操作
这道题目需要用到集合去解决,下面先给出集合的一般表示方法,以及并查操作。
1 // 集合的表示 2 struct SetType { 3 int data; 4 int parent; 5 }; 6 7 // 集合的查找操作 8 int findRoot(SetType *set, int x) { 9 int i = 0; 10 // MAXSIZE是全局变量,为集合的最大长度 11 while (i < MAXSIZE && set[i].data != x) { 12 i++; 13 } 14 15 if (i >= MAXSIZE) return -1; 16 17 while (set[i].parent >= 0) { 18 i = set[i].parent; 19 } 20 21 return i; 22 } 23 24 // 集合的并操作 25 void UnionSet(SetType *set, int x1, int x2) { 26 int root1 = findRoot(set, x1); 27 int root2 = findRoot(set, x2); 28 29 // 这里对集合的表示进行改进,集合的根的parent值不再用-1表示,而是用该集合所包含元素个数的负数来表示 30 // 同时,把包含元素少的集合并到元素多的集合,这样就可以防止集合的高度过高,从而提高了查找的效率 31 if (root1 != root2) { 32 if (set[root1].parent > set[root2].parent) {// 第一个集合的根的parent值小于第二个集合的根的parent值,意味着第一个集合的元素个数少于第二个集合 33 set[root2].parent += set[root1].parent; // 先更新第二个集合中的元素个数,也就是两个集合元素的总个数 34 set[root1].parent = root2; // 把第一个集合并到第二个集合 35 } 36 else { // 否则,第一个集合的根的parent值大于第二个集合的根的parent值,意味着第二个集合的元素个数少于第一个集合 37 set[root1].parent += set[root2].parent; // 先更新第一个集合中的元素个数,也就是两个集合元素的总个数 38 set[root2].parent = root1; // 把第二个集合并到第一个集合 39 } 40 } 41 }
给出一个例子方便理解:
解题思路
File Transfer这道题目需要用到集合来解决。不过我们可以对集合的定义进行简化。
我们并不需要把电脑的编号作为集合元素里的data,而是直接把电脑编号映射为数组的下标,同时数组里面直接存放它的parent,这样就无需定义集合这个结构体,用一个数组就可以解决了。同时,在查找操作中,我们直接通过下标来找到这个元素的位置,从而减少一次用来查找元素位置的循环。
所以我们程序的框架就是,先让用户输入计算机的个数,来建立一个对应大小的集合。然后根据用户输入的指令来进行一系列对应的操作。其中查找和合并操作和上面的代码大同小异。
下面给出AC代码:
1 #include <cstdio> 2 #include <iostream> 3 4 void unionSet(int *set); 5 void check(int *set); 6 int findRoot(int *set, int num); 7 8 int main() { 9 int n; 10 scanf("%d", &n); 11 12 int set[n + 1]; // 直接把电脑编号映射为数组的下标,所以数组长度要+1 13 set[0] = n; // set[0]用来记录当前独立集合的个数,一开始每一个元素都是一个独立的集合,初始化为n 14 std::fill(set + 1, set + n + 1, -1); // 每一个元素都是单独的一个集合,为每个元素的parent值初始化为-1,表示该集合中就只有这一个元素 15 16 char ch; 17 while ((ch = getchar()) != 'S') { // 当用户的输入不为'S',持续循环读入用户的操作 18 if (ch == 'I') unionSet(set); 19 else if (ch == 'C') check(set); 20 } 21 22 if (set[0] == 1) printf("The network is connected."); // 当set[0] == 1表示所有的元素都属于同一个集合 23 else printf("There are %d components.", set[0]); // 当set != 1表示这些元素构成的集合个数多于1,不连通 24 25 return 0; 26 } 27 28 void unionSet(int *set) { 29 int num1, num2; 30 scanf("%d %d", &num1, &num2); 31 int root1 = findRoot(set, num1); 32 int root2 = findRoot(set, num2); 33 34 // 合并的操作与之前的一样,把元素个数少的集合并到元素个数多的集合中 35 if (root1 != root2) { 36 if (set[root1] > set[root2]) { 37 set[root2] += set[root1]; 38 set[root1] = root2; 39 } 40 else { 41 set[root1] += set[root2]; 42 set[root2] = root1; 43 } 44 45 set[0]--; // 每一次合并表示一个集合变成了另外一个集合的元素,所以集合的个数要-1 46 } 47 } 48 49 void check(int *set) { 50 int num1, num2; 51 scanf("%d %d", &num1, &num2); 52 int root1 = findRoot(set, num1); 53 int root2 = findRoot(set, num2); 54 55 printf("%s\n", root1 == root2 ? "yes" : "no"); 56 } 57 58 int findRoot(int *set, int num) { 59 // 查找变得很容易,只要发现set[num] < 0就说明找到该集合的根了 60 while (set[num] > 0) { 61 num = set[num]; 62 } 63 64 return num; 65 }
其中在合并的操作中不一定要按照集合元素个数的大小来合并,也可以按照集合的高度来合并,把高度小的并到高度大的,这样就可以防止树的高度增加。
对应的unionSet函数修改如下:
1 void unionSet(int *set) { 2 int num1, num2; 3 scanf("%d %d", &num1, &num2); 4 int root1 = findRoot(set, num1); 5 int root2 = findRoot(set, num2); 6 7 if (root1 != root2) { 8 // 注意数组中下标为根存放的值是负数 9 // 合并的规则是把小树贴到大树上 10 if (set[root1] > set[root2]) { // 说明root2对应的树高大于root1对应的树高 11 set[root1] = root2; // 把第一个集合并到第二个集合 12 } 13 else { // 否则,root1对应的树高大于root2对应的树高 14 // 如果root1对应的树高与root2对应的树高相同,则root1对应的树高+1 15 if (set[root1] == set[root2]) set[root1]--; 16 set[root2] = root1; // 把第二个集合并到第一个集合 17 } 18 } 19 }
我们这里仍然采用按规模大小来合并的规则,是为了下面实现路径压缩这一操作。
算法改进
我们来对查找函数来进行改进,来减少查找的次数。这种方法就叫路径压缩,具体代码如下:
1 int findRoot(int *set, int num) { 2 if (set[num] < 0) return num; 3 else return set[num] = findRoot(set, set[num]); 4 }
这是用递归实现的,如果set[num] < 0,说明num就是根的下标,我们返回即可。如果不是,我们先递归去找到num所属集合的根,再把set[num]赋值为找到的集合的根,也就是说在数组中,下标为num直接存放集合的根,这样下次再查找num时只用查找1次就可以了。
这个递归过程是这样的:
最后附上改进后的AC代码:
1 #include <cstdio> 2 #include <iostream> 3 4 void unionSet(int *set); 5 void check(int *set); 6 int findRoot(int *set, int num); 7 8 int main() { 9 int n; 10 scanf("%d", &n); 11 12 int set[n + 1]; 13 set[0] = n; 14 std::fill(set + 1, set + n + 1, -1); 15 16 char ch; 17 while ((ch = getchar()) != 'S') { 18 if (ch == 'I') unionSet(set); 19 else if (ch == 'C') check(set); 20 } 21 22 if (set[0] == 1) printf("The network is connected."); 23 else printf("There are %d components.", set[0]); 24 25 return 0; 26 } 27 28 void unionSet(int *set) { 29 int num1, num2; 30 scanf("%d %d", &num1, &num2); 31 int root1 = findRoot(set, num1); 32 int root2 = findRoot(set, num2); 33 34 if (root1 != root2) { 35 if (set[root1] > set[root2]) { 36 set[root2] += set[root1]; 37 set[root1] = root2; 38 } 39 else { 40 set[root1] += set[root2]; 41 set[root2] = root1; 42 } 43 44 set[0]--; 45 } 46 } 47 48 void check(int *set) { 49 int num1, num2; 50 scanf("%d %d", &num1, &num2); 51 int root1 = findRoot(set, num1); 52 int root2 = findRoot(set, num2); 53 54 printf("%s\n", root1 == root2 ? "yes" : "no"); 55 } 56 57 int findRoot(int *set, int num) { 58 if (set[num] < 0) return num; 59 else return set[num] = findRoot(set, set[num]); 60 }View Code
参考资料
浙江大学——数据结构:https://www.icourse163.org/course/ZJU-93001?tid=1461682474