数据结构与算法实战 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 }