-------------------siwuxie095
路径压缩
基于 size 的优化和基于 rank 的优化,主要都是针对 Union 操作
的优化,实际上,Find 操作其实也是可以优化的
Find 操作就是从一个节点开始,通过它的指向父亲的指针,不断
向上追溯,直至根节点,也就是寻找根的过程
在这个过程中,其实已经将从这个节点开始,一直到根节点的每
个节点,都遍历了一遍
之前所实现的 Find 操作只是直接的遍历,没有做任何操作,但在
这个过程中,其实可以对节点做一些变化,让树的层数更少
具体方法:尝试着在执行 Find 操作(自底向上,不断溯源)时,
如果没有找到根节点,就想办法把这些节点向上挪一挪,称这个
过程为 路径压缩(Path Compression)
「即 判断当前节点是否为根,如果不是(即 没有找到),就把
以当前节点为根的子树整体向上挪,挪到当前节点的父亲的父亲
处,下次再判断当前节点的父亲(即 原来的祖先)是否为根 …
相当于跳了一步,所以叫 路径压缩」
假设存在并查集,如下:
如果要 find( 4 ),发现 parent[4] = 3,而 ≠ 4,判断 4 不是根节点
1)在原来的 Find 操作中,要继续向上找,判断 3 是不是根节点 …
2)而现在的路径压缩则是压缩一步,让 4 去连接它父亲的父亲,也
就是 2。此时,整棵树的层数就变少了,即 被压缩了
注意:这里是先判断是否为根,如果 4 已经是根了,自然无须去连接
它父亲的父亲,直接返回即可。如果 3 已经是根了,因为 3 的父亲是
3,所以让 4 去连接它父亲的父亲,依然成立
下面要考察的就是 4 的父亲 2,即 find( 2 ),这里没有考察 3,
相当于跳了一步
发现 parent[2] = 1,而 ≠ 2,即 2 也不是根节点,继续路径压
缩,让 2 去连接它父亲的父亲,也就是 0
下面要考察的就是 2 的父亲 0,即 find( 0 ),这里没有考察 1,
相当于又跳了一步
发现 parent[0] = 0,即 0 就是根节点,返回 0 即可
在 find( 4 ) 的过程中,成功把这颗树的根节点 0 返了回去,
同时,整棵树的层数也大大地减少了,这便是 路径压缩
路径压缩还可以继续优化,最优的情况(理论上),能将整个路径
压缩成 2 层,所有的节点都指向一个根,如下:
「注意:这种理论上的优化,实际用时更长(主因:递归的开销)」
程序 1:路径压缩的实现
UnionFind.h:
#ifndef UNIONFIND_H #define UNIONFIND_H
#include <iostream> #include <cassert> using namespace std;
//并查集:Quick Union + rank + path compression namespace UF {
class UnionFind {
private: int* parent; int* rank; // rank[i]表示以i为根的集合所表示的树的层数 int count;
public: UnionFind(int count) { this->count = count; parent = new int[count]; rank = new int[count]; //在初始情况下,并查集里的元素,两两之间互不连接 for (int i = 0; i < count; i++) { parent[i] = i; rank[i] = 1; } }
~UnionFind() { delete []parent; delete []rank; }
int size() { return count; }
int find(int p) {
assert(p >= 0 && p < count);
// path compression 1 while (p != parent[p]) { //路径压缩 parent[p] = parent[parent[p]]; p = parent[p]; }
return p; }
bool isConnected(int p, int q) { return find(p) == find(q); }
void unionElements(int p, int q) {
int pRoot = find(p); int qRoot = find(q);
if (pRoot == qRoot) { return; }
//rank小的那棵树的根节点指向rank大的那棵树的根节点 if (rank[pRoot] < rank[qRoot]) { parent[pRoot] = qRoot; } else if (rank[qRoot] < rank[pRoot]) { parent[qRoot] = pRoot; } // rank[pRoot] == rank[qRoot] else { //可互换 parent[pRoot] = qRoot; rank[qRoot] ++; }
}
void show() { for (int i = 0; i < count; i++) { cout << i << " : " << parent[i] << endl; } } }; }
//路径压缩:在寻找根的时候,两步一跳,比原来的 Find 操作要快, //与此同时,如果下一次要寻找这棵树上某个元素的根节点,由于层 //数变低,相应的速度也会快很多
#endif |
UnionFindTestHelper.h:
#ifndef UNIONFINDTESTHELPER_H #define UNIONFINDTESTHELPER_H
#include "UnionFind.h" #include <iostream> #include <ctime> using namespace std;
namespace UnionFindTestHelper {
void testUF(int n) { //设置随机种子 srand(time(NULL)); UF::UnionFind uf = UF::UnionFind(n);
time_t startTime = clock();
//先进行n次的并,即 Union 操作 for (int i = 0; i < n; i++) { int a = rand() % n; int b = rand() % n; uf.unionElements(a, b); }
//再进行n次的查,即 Find 操作 for (int i = 0; i < n; i++) { int a = rand() % n; int b = rand() % n; uf.isConnected(a, b); }
time_t endTime = clock();
//打印2*n个操作耗费的时间 cout << "UF, " << 2 * n << " ops, " << double(endTime - startTime) / CLOCKS_PER_SEC << " s" << endl; } }
#endif |
main.cpp:
#include "UnionFindTestHelper.h" #include <iostream> using namespace std;
int main() { //规模是一百万 int n = 1000000;
UnionFindTestHelper::testUF(n);
system("pause"); return 0; } |
运行一览:
程序 2:路径压缩的优化(在程序 1 的基础上,修改 UnionFind.h 即可)
UnionFind.h:
#ifndef UNIONFIND_H #define UNIONFIND_H
#include <iostream> #include <cassert> using namespace std;
//并查集:Quick Union + rank + path compression namespace UF {
class UnionFind {
private: int* parent; int* rank; // rank[i]表示以i为根的集合所表示的树的层数 int count;
public: UnionFind(int count) { this->count = count; parent = new int[count]; rank = new int[count]; //在初始情况下,并查集里的元素,两两之间互不连接 for (int i = 0; i < count; i++) { parent[i] = i; rank[i] = 1; } }
~UnionFind() { delete []parent; delete []rank; }
int size() { return count; }
int find(int p) {
assert(p >= 0 && p < count);
//path compression 2 if (p != parent[p]) { //这个版本的路径压缩实际上用时更长 //虽然理论上更优(采用递归) parent[p] = find(parent[p]); } //此时,parent[p]是整棵树的根节点 return parent[p]; }
bool isConnected(int p, int q) { return find(p) == find(q); }
void unionElements(int p, int q) {
int pRoot = find(p); int qRoot = find(q);
if (pRoot == qRoot) { return; }
//rank小的那棵树的根节点指向rank大的那棵树的根节点 if (rank[pRoot] < rank[qRoot]) { parent[pRoot] = qRoot; } else if (rank[qRoot] < rank[pRoot]) { parent[qRoot] = pRoot; } // rank[pRoot] == rank[qRoot] else { //可互换 parent[pRoot] = qRoot; rank[qRoot] ++; }
}
void show() { for (int i = 0; i < count; i++) { cout << i << " : " << parent[i] << endl; } } }; }
//路径压缩的最优的情况,能将整个路径压缩成 2 层,所有的节点 //都指向一个根 // //在这种情况下,搜索任何节点最多只要一步,就找到了根节点 // //但实际上用时更长一些,这个时间消耗主要是递归过程所产生的开销 // //不过,虽然递归过程产生了一些额外开销,但从逻辑上来讲,这样做 //是更优的,只是实践效果并不是特别的好 // //理论和实践通常会稍微有点出入,所以在实际的工作过程中也应该根据 //实际情况,灵活选择最合适的方式,而不一定是理论上最优的那个方式 // // // //此时,并查集的操作(Union和Find),时间复杂度近乎是O(1)的 // //注意:是近乎,不是完全。不难想象,经过路径压缩以后,在每次查找 //以后,每一位元素就离根节点非常近了,甚至在的这个优化版本的路径 //压缩,经过一次查询,查询路径上的所有节点,离根节点的距离,都变 //成 1 了。尽管如此,在这个过程中,依然有时间的消耗,并非每次操作 //都是 O(1) 级别的 // //对于这个时间的消耗,在数学上,数学家们发明了一个专门的函数来表达, //这个函数叫做 alpha
#endif |
运行一览:
【made by siwuxie095】