数据结构与算法实战 5.2并查集与集合 (青岛大学 周强)

  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 }

 

posted @ 2021-07-26 14:47  WriteOnce_layForever  阅读(103)  评论(0)    收藏  举报