1 *****不相交集*****(Disjoint Set)
2 先来看等价类:
3
4
5 不相交集也叫“并查集”,指的是这个集合更多的操作是并和查,不是集合叫并查集- -
6 集合,一般的集合,主要考虑集合有什么元素,根据集合查元素
7 不相交集,主要考虑,给定一个元素,去找这个元素在哪个集合.
8
9 因为我们要根据任意一个元素找到属于的集合,如果用树的话每次都要从树根出发找到元素
10 所以我们考虑用线性结构去做并查集,直接就能找到某个元素,比如数组
11
12 查找和并
13 实际上 我们希望并查集里面所有的元素,除了树根以外都是直接指着树根,只有两层!!!
14 否则的话,就会让树越来越高,查找会变得很低效
15 有三种方法:Union by size 把小树并到大树上去
16 Union by height(tank) 把矮树并到高树上
17 Path compression 路径压缩 + unionBySize
18 什么叫路径压缩? 比如 c->b->a->root
19 find(C)压缩为 c->root 省略掉路过的不是root的点
20
21 利用C++的stl的set实现并查集
22
23 #include<iostream>
24 #include<set>
25 #include<map>
26
27 using namespace std;
28
29 template<class T>
30 struct DisjointSet{//用类也行 只是老师用了结构体- -
31 int *parent;//创建时再动态分配空间大小 存父节点下标
32 T* data;//存节点的数据
33 int capacity;//最大容量
34 int size;//为什么需要这个size?因为我们会动态去删除增加元素,不一定就是固定死了就是capacity
35 map<T, int> m;//数据 数据下标
36 //构造函数
37 DisjointSet(int max=10){//最大容量,没有赋值就默认为10
38 capacity = max;
39 size = 0;
40 parent = new int[max + 1];
41 data = new T[max + 1];
42 //我们定义,用0表示没有父亲 用1往下的数字表示它的父亲是哪个位置的点
43 //所以0号元素我们不用,上面得+1
44 }
45 ~DisjointSet(){
46 delete [] parent;
47 delete [] data;
48 }
49 //插入 因为有容量限制 可能会失败 要返回插入成功or失败
50 //插入到最后那个元素的后面 也就是最新的空位置的开端
51 bool insert(T x){
52 //我们规定按照插入顺序占据数据空间
53 //注意!!0号位置我们不存东西
54 if(size == capacity){
55 return false;
56 }
57 size ++;//新插入一个 ++
58 data[size] = x;
59 parent[size] = -1;//新插入时候我们默认每个元素都是独立子集 没有父节点
60 m[x] = size;//实际上map里面不是一个数组,但是可以这样用
61 return true;
62 }
63 void print(){
64 for(int i = 1; i <= size; i ++){
65 //元素的下标 元素的父亲 元素本身
66 cout << i << "\t";
67 }
68 cout << endl;
69 for(int i = 1; i <= size; i ++){
70 //元素的下标 元素的父亲 元素本身
71 cout << parent[i] << "\t";
72 }
73 cout << endl;
74 for(int i = 1; i <= size; i ++){
75 //元素的下标 元素的父亲 元素本身
76 cout << data[i] << "\t";
77 }
78 cout << endl;
79 }
80 int find(T x){
81 /*
82 int i;//元素是几号
83 i = getIndex(x);
84 //找元素i的父亲
85 parent[i];
86 顺序查找的效率是O(N) 效率比较低 我们可以用map(映射 key-value的形式)
87 map底层用散列or红黑树实现
88 map <data, (data's)index>
89 插入的时候应该就要确定map 所以应该是struct的变量
90 */
91 //测试
92 //cout << m[x] << endl; 这样写的话,如果查找的元素不存在,map会自动插入然后和0号位置绑定,是错误的
93 //故m[x]只能用于确定x是存在的时候才能使用
94 //测试
95 //因为map里面有一个template,所以不能直接map<T, int>::iterator it;
96 int i;
97 typename map<T, int>::iterator it;
98 it = m.find(x);
99 if(it == m.end()) return -1;//找不到
100 int rt;//root
101 rt = it->second;//second就是数据的下标
102 cout << x << "的父节点下标" << i << endl;
103 //找树根
104 while(parent[rt] > 0)//=0才是树根嘛,大于0就铁定不是树根啦
105 {
106 rt = parent[rt];//一直找,找到父亲为止
107 }
108 //while 退出时候,i必定<=0,即找到了树根
109 /***实现路径压缩***/
110 //为什么不用递归呢?实际上如果传入的是节点的位置的话,递归比较好做的
111 //但是这里传进的是节点的值,难以回溯,所以我们在这里只能用循环去实现路径压缩
112 //从x .... 到rt(树根)为止,路上碰到的每一个数据都让他直接指向rt
113 //it是x的下标,rt是树根的下标
114 int tmp;
115 for(int i = m[x]; i != rt; i = tmp){
116 tmp = parent[i];//不能改变树根,所以采用了临时变量
117 parent[i] = rt;
118 }
119 return rt;
120 }
121 //并操作
122 //直接并两个树根 并两棵树
123 /*void unionSet(int r1, int r2){//谨记!!C++ 和C里面有一个数据结构是Union
124 parent[r2] = r1;
125
126 }*/
127
128 //直接并两个树根不太高效 改为路径压缩+小树并大树(按照size合并的操作
129 void unionSet(T x, T y){
130 cout << "运行并操作" << endl;
131 int rx, ry;//x's root, y's root
132 rx = find(x);
133 ry = find(y);
134 //可能元素不存在
135 if(rx == -1 || ry == -1){
136 return ;//不存在
137 }
138 //同一个集合也不需要合并
139 if(rx == ry) {
140 return;
141 }
142 //为了利用size 我们在insert中改为树根为-1
143 //我们规定,root是size的相反数
144 //rx 和 ry都是负数,当rx < ry的时候,rx这棵树大一点
145 if(parent[rx] < parent[ry]){//把ry集合合并到rx集合中去
146 parent[rx] +=parent[ry];//把元素个数合并
147 parent[ry] = rx;
148
149 }
150 else{//ry这棵树大一点 合并rx到ry集合中去
151 parent[ry] += parent[rx];
152 parent[rx] = ry;
153
154 }
155 cout << "运行并操作结束" << endl;
156 }
157 //unionTest
158 /*void Test(){
159 unionSet(1, 3);
160 unionSet(2, 4);
161 unionSet(4, 5);
162 print();
163 }*/
164 };
165
166 int main(){
167 DisjointSet<int> s;
168 s.insert(11);
169 s.insert(22);
170 s.insert(66);
171 s.insert(-5);
172 s.insert(123);
173 s.print();
174 //Find 找这个元素的父子集是谁
175 //根据数据找下标(父亲) 父亲再继续找到0为止
176 //使用者只会知道元素数据 所以参数就是元素数据
177 //返回下标就可以 用户得到下标 就能快速找到这个数据
178 s.unionSet(11, 66);
179 s.print();
180 cout << "-------------" << endl;
181 s.unionSet(22, 11);
182 s.print();
183
184 return 0;
185 }
186
187 //实例:推断学生所属学校 集合并查集结合使用
188 题目:某个比赛现场有来自不同学校的N名学生,给出M对“两人同属一所学校”的关系,
189 请推断学校数量,并给出人数最多的学校的学生名单。
190 输入格式: 先输入一个在[2, 1000]的整数N,然后是N个用空格间隔的姓名。
191 接下来一行是正整数M,M行,每行两个人名,表示同属一个学校。
192
193 输出格式:先输出学校的数量,在下一行输出人数最多的学校的学生名单。
194
195 输入样例:
196 8
197 Bill Ellen Ann Chris Daisy Flin Henry Grace
198 5
199 Ann Chris
200 Ellen Chris
201 Daisy Flin
202 Henry Ellen
203 Grace Flin
204 利用上面写过的DisjointSet完成
205
206 #include<iostream>
207 #include<set>
208 #include<map>
209
210 using namespace std;
211
212 template<class T>
213 struct DisjointSet{//用类也行 只是老师用了结构体- -
214 int *parent;//创建时再动态分配空间大小 存父节点下标
215 T* data;//存节点的数据
216 int capacity;//最大容量
217 int size;//为什么需要这个size?因为我们会动态去删除增加元素,不一定就是固定死了就是capacity
218 map<T, int> m;//数据 数据下标
219
220 /*因为并查集中每个元素在find完成之后都是指向父节点,所以很难说找到一个集合出来,
221 那么我们就得从根节点往下一个一个的找,然后才能找到全部的集合元素出来,极其麻烦
222 那么我们就需要结合原生的set一起用*/
223 set<T> *mates;//同学集合 用指针一样是为了动态分配空间大小
224 //构造函数
225 DisjointSet(int max=10){//最大容量,没有赋值就默认为10
226 capacity = max;
227 size = 0;
228 parent = new int[max + 1];
229 data = new T[max + 1];
230 //我们定义,用0表示没有父亲 用1往下的数字表示它的父亲是哪个位置的点
231 //所以0号元素我们不用,上面得+1
232
233 mates = new set<T>[max + 1];//同学集合
234 }
235 ~DisjointSet(){
236 delete [] parent;
237 delete [] data;
238 }
239 //插入 因为有容量限制 可能会失败 要返回插入成功or失败
240 //插入到最后那个元素的后面 也就是最新的空位置的开端
241 bool insert(T x){
242 //我们规定按照插入顺序占据数据空间
243 //注意!!0号位置我们不存东西
244 if(size == capacity){
245 return false;
246 }
247 size ++;//新插入一个 ++
248 data[size] = x;
249 parent[size] = -1;//新插入时候我们默认每个元素都是独立子集 没有父节点
250 m[x] = size;//实际上map里面不是一个数组,但是可以这样用
251 //因为默认刚加入的时候自己是一个独立集合 所以同学只有自己
252 mates[size].insert(x);
253 return true;
254 }
255 void print(){
256 for(int i = 1; i <= size; i ++){
257 //元素的下标 元素的父亲 元素本身
258 cout << i << "\t";
259 }
260 cout << endl;
261 for(int i = 1; i <= size; i ++){
262 //元素的下标 元素的父亲 元素本身
263 cout << parent[i] << "\t";
264 }
265 cout << endl;
266 for(int i = 1; i <= size; i ++){
267 //元素的下标 元素的父亲 元素本身
268 cout << data[i] << "\t";
269 }
270 cout << endl;
271 }
272 int find(T x){
273 /*
274 int i;//元素是几号
275 i = getIndex(x);
276 //找元素i的父亲
277 parent[i];
278 顺序查找的效率是O(N) 效率比较低 我们可以用map(映射 key-value的形式)
279 map底层用散列or红黑树实现
280 map <data, (data's)index>
281 插入的时候应该就要确定map 所以应该是struct的变量
282 */
283 //测试
284 //cout << m[x] << endl; 这样写的话,如果查找的元素不存在,map会自动插入然后和0号位置绑定,是错误的
285 //故m[x]只能用于确定x是存在的时候才能使用
286 //测试
287 //因为map里面有一个template,所以不能直接map<T, int>::iterator it;
288 int i;
289 typename map<T, int>::iterator it;
290 it = m.find(x);
291 if(it == m.end()) return -1;//找不到
292 int rt;//root
293 rt = it->second;//second就是数据的下标
294 cout << x << "的父节点下标" << i << endl;
295 //找树根
296 while(parent[rt] > 0)//=0才是树根嘛,大于0就铁定不是树根啦
297 {
298 rt = parent[rt];//一直找,找到父亲为止
299 }
300 //while 退出时候,i必定<=0,即找到了树根
301 /***实现路径压缩***/
302 //为什么不用递归呢?实际上如果传入的是节点的位置的话,递归比较好做的
303 //但是这里传进的是节点的值,难以回溯,所以我们在这里只能用循环去实现路径压缩
304 //从x .... 到rt(树根)为止,路上碰到的每一个数据都让他直接指向rt
305 //it是x的下标,rt是树根的下标
306 int tmp;
307 for(int i = m[x]; i != rt; i = tmp){
308 tmp = parent[i];//不能改变树根,所以采用了临时变量
309 parent[i] = rt;
310 }
311 return rt;
312 }
313 //并操作
314 //直接并两个树根 并两棵树
315 /*void unionSet(int r1, int r2){//谨记!!C++ 和C里面有一个数据结构是Union
316 parent[r2] = r1;
317
318 }*/
319
320 //直接并两个树根不太高效 改为路径压缩+小树并大树(按照size合并的操作
321 void unionSet(T x, T y){
322 cout << "运行并操作" << endl;
323 int rx, ry;//x's root, y's root
324 rx = find(x);
325 ry = find(y);
326 //可能元素不存在
327 if(rx == -1 || ry == -1){
328 return ;//不存在
329 }
330 //同一个集合也不需要合并
331 if(rx == ry) {
332 return;
333 }
334 //为了利用size 我们在insert中改为树根为-1
335 //我们规定,root是size的相反数
336 //rx 和 ry都是负数,当rx < ry的时候,rx这棵树大一点
337 if(parent[rx] < parent[ry]){//把ry集合合并到rx集合中去
338 parent[rx] +=parent[ry];//把元素个数合并
339 parent[ry] = rx;
340 //实例新增
341 mates[rx].insert(mates[ry].begin(), mates[ry].end());
342 //因为ry已经放进去了rx,所以ry清空得了,省空间
343 mates[ry].clear();
344 }
345 else{//ry这棵树大一点 合并rx到ry集合中去
346 parent[ry] += parent[rx];
347 parent[rx] = ry;
348 mates[ry].insert(mates[rx].begin(), mates[rx].end());
349 //因为rx已经放进去了ry,所以ry清空得了,省空间
350 mates[rx].clear();
351 }
352 cout << "运行并操作结束" << endl;
353 }
354 };
355
356 int main(){
357 int M,N;
358 cin >> N;
359 DisjointSet<int> s(N);
360 string name;
361 for(int i = 0; i < N; i ++){
362 cin >> name;
363 s.insert(name);
364 }
365 cint >> M;
366 string name2;
367 for(int i = 0; i < M; i ++){
368 cin >> name >> name2;
369 s.unionSet(name, name2);
370 }
371 //结构体的属性都是Public的 main可以直接调用
372 int maxid = 0;//最大值的树根是哪个
373 int maxsize = 0;//最大人数
374 int numOfSchools = 0;//学校数量
375 for(int i=1; i<=N; i++){
376 if(s.mates[i].size()>0) numOfSchools++; //发现一所学校,则数量+1
377 if(s.parent[i] < maxsize) {
378 maxsize = s.parent[i];
379 maxid = i;
380 }
381 }
382 cout << numOfSchools << endl; //打印学校的数量
383 for(auto x : s.mates[maxid]) //打印人数最多的学校的学生名单
384 cout << x << ' ';
385 cout << endl;
386 return 0;
387 }