Union-Find Set 并查集 详解 [基本模板]

What to solve? Which regard is it? Where to optimizate? Meanwhile, it is worth nothing that we are careful.

/*
    name: Union-Find Set
    by Tangent Chang
    in 107/3/20 in the Republic of China
*/

这里写图片描述

解决了什么问题?
并查集,是一种树型的数据结构,用于处理一些不相交的合并问题。
解决了哪些方面的?
1.合并两个不相交集合;
2.判断两个元素是否属于同一个集合;
3.路径压缩,优化时间。
优化了什么地方?
1.采用路径压缩;
2.采用启发式合并:让深度较小的树成为深度较大的树的子树。
有哪些细节值得注意?
·非递归写法有时比递归写法要慢,所以两种写法都得会。
·连接、找父节点都要注意节点的移动和权值的变换,这个根据题目要求来编写。
/*
    name: Union-Find Set
    by Tangent Chang
    in 107.3.20 in the Republic of China
*/

int father[maxn];
int n;

// 有人喜欢把father[]换成fat[],pa[]或pre[]。

void Init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}

// 有人喜欢把初始化函数写成makeSet(),表示标记设置之意。

/*
// 递归(有速度加持)。
int getFather(const int &v) {
    if (father[v] == v)
        return v;
    else
        return getFather(father[v]);
}

// 递归另一种写法。
int getFather(const int &v) {
    if (father[v] != v) {
        int root = getFather(father[v]);
        return father[v] = root;
    }
    else
        return v;
}

// 非递归,且不带路径压缩。
int getFather(const int &v) {
    int r = v;
    while(r != father[r])
        r = father[r];
    return r;
}
*/

// 非递归,路径压缩
int getFather(const int &v) {
    int t1 = v, t2;
    while (v != father[v])
        v = father[v];
    while (t1 != father[t1]) { // 沿途上所有结点的父亲改成根。这一步是顺便的,不增加时间复杂度,却使得今后的操作比较快。这个优化称为路径压缩。
        t2 = father[t1];
        father[t1] = v;
        t1 = t2;
    }
    return v;
}

// 有人喜欢把getFather()换成Find(),findRoot()或findSet()。

// 归并:把节点i、节点j放到同一个根底下。
void merge(const int &i, const int &j) {
    int x = getFather(i);
    int y = getFather(j);
    if (x != y) // 可选,主要是为了防止getFather()路径压缩的时候出现死循环。
        father[x] = y; // 有向图注意顺序,该行代码含义:a->b。
}

// 查询:查询节点i跟节点j是否在同一根下。
bool judge(const int &i, const int &j) {
    if (getFather(i) == getFather(j))
        return true;
    else
        return false;
}

// 有人喜欢把merge()换成Union(),union_nodes()或connect(),还有一些喜欢把judge()的内容放到union()里写。

这里写图片描述

优化思路:

  merge函数可以采用启发式合并,思路就是把深度较小的那颗子树并到深度较大的那颗子树上。

int father[maxn];
int n;

void Init() {
    for (int i = 0; i <= n; ++i) {
        rank[i] = 0;
        father[i] = i;
    }
}

int getFather(const int &x) {
    int px = x , i ;
    while ( px != father[px])   // find root
        px = father[px]; 
    while ( x != px ) {         // path compression
        i = father [ x ];
        father [ x ] = px ;
        x = i;
    }
    return px ;
}

void merge(const int &x, const int &y) { // 下面还有一种写法
    x = getFather(x);
    y = getFather(y);
    if (rank[x] > rank[y])
        father[y] = x;
    else {
        father[x] = y;
        if (rank[x] == rank[y])
            rank[y]++;
    }    
}

bool judge(const int &i, const int &j) {
    if (getFather(i) == getFather(j))
        return true;
    else
        return false;
}

这里写图片描述

并查集可以离散化。

–有兴趣的coder可以自行百度。

这里写图片描述

并查集可以用类的思想去想,并添加一个深度数组rank[]。

就是启发式合并中用rank[i]表示i的秩,用秩来代替深度。
下面代码没写构造函数,有兴趣的coder自行百度。

这里写图片描述

int father[maxn], rank[maxn];

void Init(const int &v) {
    father[v] = -1;
    rank[v] = 0;
}

int getFather(const int &v) {
    int t1 = v;
    while (father[t1] != -1)
    t1 = father[t1];
    while (v!=t1) {
        int t2 = father[v];
        father[v] = t1;
        v = t2;
    }
    return t1;      
}    

void merge(const int &a, const int &b) {
    int t1 = getFathet1(a);
    int t2 = getFathet1(b);
    if(rank[t1] > rank[t2])
        father[t2] = t1;
    else
        father[t1] = t2;
    if(rank[t1] == rank[t2])
        ++rank[t2];     
}

bool judge(const int &i, const int &j) {
    if (getFather(i) == getFather(j))
        return true;
    else
        return false;
}

这里写图片描述

/*
    另一种写法:
*/

int f[maxn], rank[maxn], num[maxn];

void Init() {
    for (int i = 0; i <= n; ++i) {
    rank[i] = 1;
    num[i] = 1;
    father[i] = i;
    }
}

// f[]数组存放根节点,rank[]数组来存放根节点的深度,num[]数组来存放节点个数,rank[]数组和num[]数组的初始化都应为1

// 启发式合并:
void merge(int x, int y)
{
    fx = getFather(x);
    fy = getFather(y);
    if (fx == fy) return;
    if (rank[fx] > rank[fy]) {
        father[fy] = fx;
        num[fx] += num[fy];
    }
    else {
        father[fx] = fy;
        num[fy] += num[fx];
        if (rank[fx] == rank[fy]) {
            ++rank[fy];
        }
    }
}

// 路径压缩:
int getFather(int x) {
    if(father[x] == x)
        return x;
    else
        return father[x] = getFather(father[x]);
}

这里写图片描述

// 仍有一种写法:

int father[maxn];

void Init() {
    for(int i = 0; i < n; ++i)
        father[i] = -1;
}

int getFather(int x) {
    if (father[x] < 0)
        return x;
    father[x] = getFather(father[x]);
    return father[x];
}

int getFather(int x) {
    int p = x, t;
    while (father[p] >= 0)
        p = father[p];
    while (x != p) {
        t = father[x];
        father[x] = p;
        x = t;
    }
    return x;
}
void merge(int x, int y) {
    x = getFather(x);
    y = getFather(y)
    if (x == y) return;
    if (father[x] < father[y]) {
        father[x] += father[y];
        father[y] = x;
    } else {
        father[y] += father[x];
        father[x] = y;
    }
}

bool judge(const int &i, const int &j) {
    if (getFather(i) == getFather(j))
        return true;
    else
        return false;
}

这里写图片描述

除了按秩合并,并查集还有一种常见的策略,就是按集合中包含的元素个数(即树中的节点数)合并,将包含节点较少的树根,指向包含节点较多的树根。这个策略与按秩合并的策略类似,同样可以提升并查集的运行速度,而且省去了额外的rank数组。(就像我们最初做的那样)

这里写图片描述

这样的并查集具有一个略微不同的定义,即若father的值是正数,则表示该元素的父节点(的索引);若是负数,则表示该元素是所在集合的代表(即树根),而且值的相反数即为集合中的元素个数。

这里写图片描述


这里写图片描述

当考察并查集某个节点的移动次数或者某点权值的时候,需要注意一下细节:

1.初始化
2.连接、找父节点都要注意节点的移动和权值的变换

int father[maxn];   // 存放每个节点的根节点
int num[maxn];      // 记录节点的转移次数
int cnt[maxn];      // 某点的权值

void Init() {
    for (int i = 1; i <= n; ++i) {
        father[i] = i;
    }
    memset(num, 0, sizeof(num));
    memset(cnt, 0, sizeof(cnt));
}

int getFather(const int &x) {
    if (father[x] != x){
        int tmp = father[x];
        father[x] = getFather(father[x]);
        num[x] += num[tmp]; // x点转移的次数是他的转移次数加上父节点的转移次数。
    }
    return father[x];
}

void merge(const int &x, const int &y) {
    int xx = getFather(x), yy = getFather(y);
    if (xx != yy){
        father[xx] = yy;
        ++num[xx];              // 开始转移了一次!
        cnt[yy] += cnt[xx];     // 权值转移了。
    }
}

bool judge(const int &i, const int &j) {
    if (getFather(i) == getFather(j))
        return true;
    else
        return false;
}

并查集简介

  并查集,就是Union-Find Set,也称不相交集合 (Disjoint Set)。她是一种树型的数据结构,用于处理一些不相交的合并问题。

这里写图片描述

总结

一般问题都是围绕着并查集的两个主要操作,合并和查找做文章,根据具体应用,增加一些信息,增加一些逻辑,例如转移次数,或者是根据问题特征选择使用合适的优化策略,按秩合并(启发式策略)以及路径压缩。

给我点个顶吧!d=====( ̄▽ ̄*)b

这里写图片描述

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值