LeetCode 题解随笔:并查集

目录

零、前言

684. 冗余连接

685. 冗余连接 II[*]

 130. 被围绕的区域

 990. 等式方程的可满足性


零、前言

并查集解决的问题:集合问题,两个节点在不在一个集合,可以将两个节点添加到一个集合中。

并查集问题模板(来源:代码随想录):

int n = 1005; // 节点数量3 到 1000
int father[1005];

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}
// 并查集里寻根的过程
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]);
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
    u = find(u);
    v = find(v);
    if (u == v) return ;
    father[v] = u;
}
// 判断 u 和 v是否找到同一个根
bool same(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

并查集主要有三个功能:

  1. 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
  2. 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
  3. 判断两个节点是否在同一个集合,函数:same(int u, int v),就是判断两个节点是不是同一个根节点

注意find函数中的返回值father[u] = find(father[u]),具有路径压缩的功能。

684. 冗余连接

int n = 1001; // 节点数量 3 <= n <= 1000
    int father[1001];
    // 并查集寻根:u == father[u]说明无根,否则递归找根
    int find(int u) {
        return u == father[u] ? u : father[u] = find(father[u]);
    }
    // 判断是否同根
    bool isSame(int u, int v) {
        u = find(u);
        v = find(v);
        return u == v;
    }
    // 将边u -> v加入并查集
    void join(int u, int v) {
        u = find(u);
        v = find(v);
        if (u == v) return;
        // u和v这里已经改变为其根的值,在根节点进行连线
        father[u] = v;
    }
    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
        // 并查集初始化
        for (int i = 0; i < n; i++)  father[i] = i;
        for (vector<int> v : edges) {
            if (isSame(v[0], v[1]))   return v;
            else    join(v[0], v[1]);
        }
        return {};
    }

理解了并查集方法后, 本题只需从前向后遍历每一条边,边的两个节点如果不在同一个集合,就加入集合(即:同一个根节点);否则说明已成环,返回这条边即可。

685. 冗余连接 II[*]

static const int n = 1001; // 节点数量 3 <= n <= 1000
    int father[1001];
    // 并查集寻根:u == father[u]说明无根,否则递归找根
    int find(int u) {
        return u == father[u] ? u : father[u] = find(father[u]);
    }
    // 判断是否同根
    bool isSame(int u, int v) {
        u = find(u);
        v = find(v);
        return u == v;
    }
    // 将边u -> v加入并查集
    void join(int u, int v) {
        u = find(u);
        v = find(v);
        if (u == v) return;
        // u和v这里已经改变为其根的值,在根节点进行连线
        father[u] = v;
    }
    vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
        // 记录节点入度
        int inDegree[n] = { 0 };
        for (vector<int> v : edges) {
            inDegree[v[1]]++;
        }
        // 多余一条边,有两种情况:1.出现入度为2的点; 2.出现环
        // 处理出现入度为2点的情况,从后之前删除,判断是否满足树的条件
        vector<int> vec;
        for (int i = edges.size() - 1; i >= 0; i--) {
            if (inDegree[edges[i][1]] == 2) {
                vec.push_back(i);
            }
        }
        // 若为第一种情况,vec中会有两条边(这两个边出度为2),判断哪个可以删除
        if (!vec.empty()) {
            if (isTreeAfterRemoveEdge(edges, vec[0]))     return edges[vec[0]];
            else    return edges[vec[1]];
        }
        // 若为第二种情况,结构中有有向环,找到删除那条边可以成树的边
        return getRemovedEdge(edges);
    }
    bool isTreeAfterRemoveEdge(const vector<vector<int>>& edges, int deleteEdge) {
        // 初始化并查集
        for (int i = 0; i < n; i++)  father[i] = i;
        for (int i = 0; i < edges.size(); i++) {
            if (i == deleteEdge)     continue;
            // 移除目标边后,若两点所在边还有相同的根,则有有向环【且有向环只有唯一解,可以直接返回】
            else {
                if (isSame(edges[i][0], edges[i][1])) return false;
                join(edges[i][0], edges[i][1]);
            }
        }
        return true;
    }
    vector<int> getRemovedEdge(const vector<vector<int>>& edges) {
        // 初始化并查集
        for (int i = 0; i < n; i++)  father[i] = i;
        for (auto v : edges) {
            // 待添加的这条边已经构成有向环了,需要移除【且这种情况下只有唯一解,可以直接返回】
            if (isSame(v[0], v[1]))   return v;
            else    join(v[0], v[1]);
        }
        return {};
    }

并查集的基本原理与上一题是类似的,难点在于在有向图中,多余一条边的情况分解

  • 第一种情况:有一个点的入度为2(来源:代码随想录),这个点会对应两条边,以出现顺序从后之前的顺序(因为要返回靠后的结果),判断移除后这条边能否满足构成树的条件。

 第一种情况下的子情况1,这两条边为[1,2]和[2,3],移除前者没问题,移除后者结构中有环,加入[2,4]时isSame会返回true。因此结果只能是[1,2];

第一种情况下的子情况2,这两条边为[1,3]和[2,3],移除这两者都可以(因为移除后结构中没有有向环了),返回这两条边的靠后者即可。

  • 第二种情况:结构中有有向环

这种情况遍历移除每一条边,判断移除后结构中是否还有环即可。【有环就意味着某条边加入时,其两个顶点值已经有公共父亲了

另外,一些使用 DFS 深度优先算法解决的问题,也可以用 Union-Find 算法解决

 130. 被围绕的区域

把那些不需要被替换的 O 看成一个拥有独门绝技的门派,它们有一个共同「祖师爷」叫 dummy,这些 O 和 dummy 互相连通,而那些需要被替换的 O 与 dummy 不连通。 只有和边界 O 相连的 O 才具有和 dummy 的连通性,他们不会被替换。(来源:labuladong)

 

class Solution {
public:
    class UF {
    public:
        vector<int> father;
        // 并查集寻根:无根/递归找根
        int find(int u) {
            return u == father[u] ? u : father[u] = find(father[u]);
        }
        // 判断是否同根
        bool isSame(int u, int v) {
            u = find(u);
            v = find(v);
            return u == v;
        }
        // 将u -> v加入并查集
        void join(int u, int v) {
            u = find(u);
            v = find(v);
            if (u == v) return;
            father[u] = v;
        }
    };
    
    void solve(vector<vector<char>>& board) {
        // 构建并查集
        UF uf;
        int m = board.size();
        int n = board[0].size();
        // 给不需要被替换的 O 赋予共同的虚父结点,并为其预留一个位置
        uf.father.resize(m * n + 1);
        for (size_t i = 0; i < m * n + 1; ++i)    uf.father[i] = i;
        int dummy = m * n;
        // 将首列和末列的 O 与 dummy 连通
        for (int i = 0; i < m; i++) {
            if (board[i][0] == 'O')
                uf.join(i * n, dummy);
            if (board[i][n - 1] == 'O')
                // 二维数组转化为一维数组
                uf.join(i * n + n - 1, dummy);
        }
        // 将首行和末行的 O 与 dummy 连通
        for (int j = 0; j < n; j++) {
            if (board[0][j] == 'O')
                uf.join(j, dummy);
            if (board[m - 1][j] == 'O')
                uf.join(n * (m - 1) + j, dummy);
        }
        // 方向数组 d 是上下左右搜索的常用手法
        int d[4][2] = { {1,0}, {0,1}, {0,-1}, {-1,0} };
        for (int i = 1; i < m - 1; i++)
            for (int j = 1; j < n - 1; j++)
                if (board[i][j] == 'O')
                    // 将此 O 与上下左右的 O 连通
                    for (int k = 0; k < 4; k++) {
                        int x = i + d[k][0];
                        int y = j + d[k][1];
                        if (board[x][y] == 'O')
                            uf.join(x * n + y, i * n + j);
                    }
        // 所有不和 dummy 连通的 O,都要被替换
        for (int i = 1; i < m - 1; i++)
            for (int j = 1; j < n - 1; j++)
                if (!uf.isSame(dummy, i * n + j))
                    board[i][j] = 'X';
    }
};

 990. 等式方程的可满足性

class Solution {
public:
    class UF {
    public:
        vector<int> father;
        // 并查集寻根:无根/递归找根
        int find(int u) {
            return u == father[u] ? u : father[u] = find(father[u]);
        }
        // 判断是否同根
        bool isSame(int u, int v) {
            u = find(u);
            v = find(v);
            return u == v;
        }
        // 将u -> v加入并查集
        void join(int u, int v) {
            u = find(u);
            v = find(v);
            if (u == v) return;
            father[u] = v;
        }
    };
    
    bool equationsPossible(vector<string>& equations) {
        UF uf;
        // 26个英文小写字母,初始化其并查集
        uf.father.resize(26);
        for (size_t i = 0; i < 26; ++i) {
            uf.father[i] = i;
        }
        // 根据相等公式,构建并查集关系
        for (auto equ : equations) {
            if (equ[1] == '=') {
                uf.join(equ[0] - 'a', equ[3] - 'a');
            }
        }
        // 根据不等公式,检查并查集关系
        for (auto equ : equations) {
            if (equ[1] == '!') {
                if (uf.isSame(equ[0] - 'a', equ[3] - 'a'))   return false;
            }
        }
        return true;
    }
};

核心思想是,将 equations 中的算式根据 == 和 != 分成两部分,先处理 == 算式,使得他们通过相等关系各自勾结成门派(连通分量);然后处理 != 算式,检查不等关系是否破坏了相等关系的连通性。 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值