欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

算法训练 - 并查集

程序员文章站 2022-05-22 13:36:03
...

这个是我做 kuangbin带你飞专题训练的第三个专题,对应的是 kuangbin带你飞专题五,这个专题是并查集专题,包含14道题目。

并查集属于树形结构,是一种用来管理元素分组情况的数据结构。并查集可以高效的进行如下操作:

  • 查询元素 a 和元素 b 是否属于同一个组
  • 合并元素 a 和元素 b 所在的分组

值得注意的是,并查集虽然可以进行合并操作,但是不能进行分割操作。当然了这并不意味着包含分割操作的题目就不能用并查集解决(如 ZOJ-3261 Connections in Galaxy War)。



并查集的实现

下面是并查集实现的例子。在例子中,用编号代替每个元素。数组 par 表示的是父节点的编号,当 par[x] = x 时,x 是所在树的根

int par[MAX_N], rank[MAX_N];

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

int find(int x)
{
    if (x == par[x])
        return x;
    return par[x] = find(par[x]);
}

void unite(int x, int y)
{
    x = find(x);
    y = find(y);

    if (x != y)
    {
        if (rank[x] > rank[y])
            par[y] = x;
        else
        {
            par[x] = y;
            if (rank[x] == rank[y])
                rank[y] += 1;
        }
    }
}

bool same(int x, int y)
{
    return (find(x) == find(y));
}

以上就是最简单的并查集的实现,除了这样的,还有一种带权并查集。

经典例题 HDU-1213 How Many Tables

解题思路: 没什么好说的,就是以上并查集模板的简单应用

#include <cstdio>
#include <set>

using namespace std;

int const maxn = 1005;

int N,M;
int rankk[maxn], par[maxn];

// 并查集
void init(int n)
{
    for (int i = 1;i <= n;i++)
    {
        rankk[i] = 1;
        par[i] = i;
    }
}

int find_node(int x)
{
    if (x == par[x])
        return x;
    return par[x] = find_node(par[x]);
}

void unite(int x, int y)
{
    x = find_node(x);
    y = find_node(y);

    if (x != y)
    {
        if (rankk[x] > rankk[y])
            par[y] = x;
        else
        {
            par[x] = y;
            if (rankk[x] == rankk[y])
                rankk[x] += 1;
        }
    }
}


int main()
{
    int tcs;
    scanf("%d", &tcs);
    set<int> uniqu;

    for (int i = 1;i <= tcs;i++)
    {
        scanf("%d%d", &N, &M);
        init(N);

        int a, b;
        for (int i = 0;i < M;i++)
        {
            scanf("%d%d", &a, &b);
            unite(a, b);
        }

        uniqu.clear();
        for(int i = 1;i <= N;i++)
            uniqu.insert(find_node(i));

        printf("%d\n",uniqu.size());

        if (i < tcs)
            scanf("\n");
    }

    return 0;
}

并查集实现中的注意点

避免退化

在树形数据结构中,如果发生退化,那么复杂度就会变得很高。因此,有必要想办法避免退化的发生,在并查集中,有如下的方法可以避免退化:

  • 对于每棵树,记录这棵树的高度 rank
  • 合并时如果两棵树的高度(rank)不同,那么从高度较小的向高度较大的连边

路径压缩

此外,通过路径压缩,可以使并查集变得更加高效。路径压缩是:对于每个节点,一旦向上走到了一次根节点,那么就把这个点到父节点的边改为直接连向根。在此之上,不仅仅是所查询的节点,在查询过程中向上所经过的所有节点,都改为直接连到根上。这样再次查询这些节点的时候,很快就可以知道根是谁了。
再使用这种简化方法时,为了简单起见,即使树的高度发生了变化,也不修改 rank 的值。当然了有时候,rank 不仅仅作为树的高度,还可以根据题意,赋予特殊的含义,比如说 ZOJ-3261 Connections in Galaxy War.

并查集的结构

并查集也是使用树形结构实现的,不过不是二叉树。每一个元素对应一个节点,每个组对应一个棵树。在并查集中,哪个节点是哪个节点的父节点以及树的形状等信息无需多加关注,整体组成一个树形结构才是重要的。
所以,如果并查集中的元素十分稀疏,在进行离散化的时候,一般不必考虑各个元素的相对大小,只要可以符合题意组成一棵树即可。

经典例题 POJ 1733 Parity game

解题思路:这道题是带权并查集的应用,首先要会推导路径压缩和合并分组时的公式,其次,这道题的数据比较稀疏,因此需要离散化,如上述所说,各个节点之间的关系并不严格,所以只要用 map 简单的离散化一下就可以了。

#include <cstdio>
#include <map>
#include <string>
#include <iostream>
#include <algorithm>

using namespace std;

int const maxn = 10010;
int N, M, mi = 1;

// 并查集
struct node{
    int par, rea;
}game[maxn];

void init(void)
{
    for (int i = 0;i < maxn;i++)
    {
        game[i].par = i;
        game[i].rea = 0;
    }
}

int find_node(int x)
{
    if (game[x].par == x)
        return x;
    int tmp = game[x].par;

    game[x].par = find_node(game[x].par);
    game[x].rea = game[tmp].rea ^ game[x].rea;

    return game[x].par;
}

bool unite(int x,int y, int rea)
{
    int px = find_node(x);
    int py = find_node(y);

    if (px != py)
    {
        game[py].par = px;
        game[py].rea = game[x].rea ^ game[y].rea ^ rea;

        return true;
    }
    else
    {
        if ((game[x].rea^game[y].rea) == rea)
            return true;
        return false;
    }
}


int main()
{
    scanf("%d%d", &N, &M);
    init();
    map<int, int> mp;
    int a, b, x, y, ans;
    bool flag = true;
    string des;
    ans = M;
    for (int i = 0;i < M;i++)
    {
        scanf("%d%d", &a, &b);
        cin >> des;
        if (!flag)
            continue;
        x = min(a, b);
        y = max(a, b);
        x -= 1;
        int tmp_rea = (des=="even"?0:1);

        if (mp.find(x) == mp.end())
            mp[x] = mi++;
        int mx = mp[x];
        if (mp.find(y) == mp.end())
            mp[y] = mi++;
        int my = mp[y];

        flag = unite(mx, my, tmp_rea);
        if (!flag)
            ans = i;
    }
    printf("%d\n", ans);

    return 0;
}

并查集的综合应用

并查集作为一种数据结构,很少会单独考察,通常是和其他的算法一同考察。

并查集与动态规划

经典例题 POJ-1417 True Liars

解题思路: 这道题比较有难度,关于并查集的部分倒是不难想。根据题意可以知道,说 yes 的人与被他说的人是同类,说 no 的人与被他说的人不是同类,这里可以用带权并查集,权值是当前节点与父节点的关系,关系有两种,一种是 0 表示同类,另一种是 1 表示不是同类。路径压缩时权值的的改变的方式是,当前节点与新父节点的关系等于 当前节点与旧父节点的关系加上旧父节点与旧…爷爷节点的关系 模2。在合并分组时权值改变的方式是,节点 x 加上节点 y 的权值再加上描述中两个节点之间的关系模2。
另一个知识点是动态规划,在经过以上步骤之后,现在有很多个连通分量,每个连通分量里面应该有两类,但是不知道哪一类是好人哪一类是坏人,所以要在每一个连通分量中取一类,最终组成一组,看看这组的人数是不是与好人人数相同。如果这样的取法仅有一种,则代表有正解。用 dp[i][j] 来表示从前 i 个连通分量中取 j 个人,一共有多少种取法,初始化 dp[0][0] = 1,余下所有为 0.确定有正解之后,再用 dp这个数组来逆推从每一个联通分量中选择了哪一类


#include <cstdio>
#include <cstring>
#include <map>

using namespace std;

int const maxp = 605;

int dp[maxp][maxp], bag[maxp][2];

// 映射
map<int, int> cc;
int cnt;

void cc_insert(int x, int rea)
{
    if (cc.find(x) == cc.end())
        cc[x] = ++ cnt;
    bag[cc[x]][rea] += 1;
}

// 并查集
struct node{
    int par, rea;
}s[maxp];

void init(int n)
{
    for (int i = 0; i <= n;i++)
    {
        s[i].par = -1;
        s[i].rea = 0;
    }
}

int find_node(int x)
{
    if (s[x].par == -1)
        return x;
    int root = find_node(s[x].par);
    s[x].rea = (s[x].rea + s[s[x].par].rea)%2;

    return s[x].par = root;
}

void unite(int x, int y, int rea)
{
    int px = find_node(x);
    int py = find_node(y);

    if (px != py)
    {
        s[px].par = py;
        s[px].rea = (s[x].rea + s[y].rea + rea)%2;
    }
}

bool same(int x, int y)
{
    return (find_node(x) == find_node(y));
}


int main()
{
    int n, p1, p2;

    while (scanf("%d%d%d", &n, &p1, &p2), n+p1+p2)
    {
        if(n==0&&p1==0&&p2==0)
            break;

        cc.clear();
        init(p1+p2);

        for (int i = 1;i <= n;i++)
        {
            int xi, yi;
            char ans[8];
            scanf("%d%d%s", &xi, &yi, ans);

            int tmp_rea;
            if (ans[0] == 'y')
                tmp_rea = 0;
            else
                tmp_rea = 1;
            if (!same(xi, yi))
                unite(xi, yi, tmp_rea);
        }
        // 将连通分量映射到 map 上,重新编号
        cnt = 0;
        memset(bag, 0, sizeof(bag));
        for (int i = 1;i <= p1+p2;i++)
        {
            int fi = find_node(i);
            cc_insert(fi, s[i].rea);
        }

        memset(dp, 0, sizeof(dp));
        dp[0][0] = 1;
        for (int i = 1;i <= cnt;i++)
        {
            for (int j = 0;j <= p1;j++)
            {
                if (j >= bag[i][0])
                    dp[i][j] = dp[i-1][j-bag[i][0]];
                if (j >= bag[i][1])
                    dp[i][j] += dp[i-1][j-bag[i][1]];
            }
        }

        // 逆推路径并输出
        if (dp[cnt][p1] == 1)
        {
            int choose[cnt+1], p = p1;
            memset(choose, -1, sizeof(choose));
            for (int i = cnt;i > 0 && p > 0;i--)
            {
                if (dp[i][p] == dp[i-1][p-bag[i][0]])
                    choose[i] = 0;
                else if (dp[i][p] == dp[i-1][p-bag[i][1]])
                    choose[i] = 1;
                p -= bag[i][choose[i]];
            }

            for (int i = 1;i <= p1+p2;i++)
            {
                int fa = find_node(i);
                int num = cc[fa];
                if (s[i].rea == choose[num])
                    printf("%d\n", i);
            }
            printf("end\n");
        }
        else
            printf("no\n");
    }

    return 0;
}

并查集与贪心算法

经典例题:POJ-1456 Supermarket

解题思路: 先将给的数据按照 px 从大到小排序,再先按照最大售卖期限初始化并查集,然后对于结构体数组中的每个成员,如果它售卖期限在并查集中的父节点大于 0,那么 i 将会在这一天售卖,同时将其父节点减一,代表和 i 的期限在同一天的商品,最晚会在这一天之前卖出。如此,便可求出最大利润。

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

int const maxn = 10005;

struct node{
    int px, dx;

    bool operator < (const node &c)
    {
        return px > c.px;
    }
}prdct[maxn];

// 并查集
int par[maxn];

void init(int n)
{
    for (int i = 1;i <= n;i++)
        par[i] = i;
}

int find_node(int x)
{
    if (x == par[x])
        return x;
    return par[x] = find_node(par[x]);
}


int main()
{
    int N;
    while (~scanf("%d", &N))
    {
        int maxd = 0;
        for (int i = 0;i < N;i++)
        {
            scanf("%d%d", &prdct[i].px, &prdct[i].dx);
            maxd = max(maxd, prdct[i].dx);
        }
        sort(prdct, prdct+N);

        init(maxd);
        int ans = 0;
        for (int i = 0;i < N;i++)
        {
            int ddl = find_node(prdct[i].dx);

            if (ddl > 0)
            {
                ans += prdct[i].px;
                par[ddl] = ddl - 1;
            }
        }

        printf("%d\n", ans);
    }

    return 0;
}
相关标签: 算法 并查集