[leetcode] 并查集(Ⅱ)

Posted sinkinben

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[leetcode] 并查集(Ⅱ)相关的知识,希望对你有一定的参考价值。

最长连续序列

题目[128]:??链接

解题思路

节点本身的值作为节点的标号,两节点相邻,即允许合并(x, y)的条件为x == y+1

因为数组中可能会出现值为 -1 的节点,因此不能把 root[x] == -1 作为根节点的特征,所以采取 root[x] == x 作为判断是否为根节点的条件。默认较小的节点作为连通分量的根。

此外,使用 map<int, int> counter 记录节点所在连通分量的节点个数(也是merge 的返回值)。

class Solution
{
public:
    unordered_map<int, int> counter;
    unordered_map<int, int> root;
    int longestConsecutive(vector<int> &nums)
    {
        int len = nums.size();
        // use map to discard the duplicate values
        for (int x : nums)
            root[x] = x, counter[x] = 1;
        int result = len == 0 ? 0 : 1;
        for (int x : nums)
        {
            if (root.count(x + 1) == 1)
                result = max(result, merge(x, x + 1));
        }
        return result;
    }
    int find(int x)
    {
        return root[x] == x ? x : (root[x] = find(root[x]));
    }
    int merge(int x, int y)
    {
        x = find(x);
        y = find(y);
        if (x != y)
        {
            root[y] = x;
            counter[x] += counter[y];
        }
        return counter[x];
    }
};

连通网络的操作次数

题目[1319]:??Link.

解题思路

考虑使用并查集。

考虑到特殊情况,要使 N 个点连通,至少需要 N-1 条边,否则返回 -1 即可。

通过并查集,可以计算出多余的边的数目(多余的边是指使得图成环的边),只要 findroot(x) == findroot(y) 说明边 (x,y) 使得图成环。

遍历所有边,在并查集中执行合并 merge 操作(多余的边忽略不合并,只进行计数)。设 components 为合并后后 root 数组中 -1 的个数(也就是连通分量的个数),要想所有的连通分支都连起来,需要 components - 1 个边,所以要求「多余的边」的数目必须大于等于 components - 1

一个简单的例子如下:

0--1         0--1                0--1
| /    =>    |          =>       |  | 
2  3         2  3                2  3
             components = 2
             duplicateEdge = 1

代码实现

class Solution
{
public:
    vector<int> root;
    int result = 0;
    int makeConnected(int n, vector<vector<int>> &connections)
    {
        int E = connections.size();
        // corner cases
        if (n == 0 || n == 1)
            return 0;
        if (E < n - 1)
            return -1;
        root.resize(n), root.assign(n, -1);
        // merge
        for (auto &v : connections)
        {
            int a = v[0], b = v[1];
            merge(a, b);
        }
        int components = count(root.begin(), root.end(), -1);
        if (counter >= (components - 1))
            return components - 1;
        // should not be here
        return -1;
    }
    int find(int x)
    {
        return root[x] == -1 ? x : (root[x] = find(root[x]));
    }
    // the number of duplicate edges
    int counter = 0;
    void merge(int x, int y)
    {
        x = find(x), y = find(y);
        if (x != y)
            root[y] = x;
        else
        {
            // there is a duplicate edge
            counter++;
        }
    }
};

等式方程的可满足性

题目[990]:??Link.

解题思路

考虑并查集。遍历所有的包含 == 的等式,显然,相等的 2 个变量就合并。对于不等式 x!=y ,必须满足 findroot(x) != findroot(y) 才不会出现逻辑上的错误。也就是说,不相等的 2 个变量必然在不同的连通分支当中。

#define getidx(x) ((x) - ‘a‘)
class Solution
{
public:
    vector<int> root;
    bool equationsPossible(vector<string> &equations)
    {
        root.resize(‘z‘ - ‘a‘ + 1, -1);
        vector<int> notequal;
        int len = equations.size();
        for (int i = 0; i < len; i++)
        {
            auto &s = equations[i];
            if (s[1] == ‘!‘)
            {
                notequal.emplace_back(i);
                continue;
            }
            int a = getidx(s[0]), b = getidx(s[3]);
            merge(a, b);
        }
        for (int i : notequal)
        {
            auto &s = equations[i];
            int a = getidx(s[0]), b = getidx(s[3]);
            if (find(a) == find(b))
                return false;
        }
        return true;
    }
    int find(int x)
    {
        return (root[x] == -1) ? x : (root[x] = find(root[x]));
    }
    void merge(int x, int y)
    {
        x = find(x), y = find(y);
        if (x != y)
            root[y] = x;
    }
};

尽量减少恶意软件的传播 II

题目[928]:??这题有点难。

解题思路

参考 题解1题解2

首先,对原来的并查集结构添加一点改进,利用 vector<int> size[N] 记录某个连通分量中节点的数目,注意当且仅当 x 是该连通分量的根节点时,size[x] 才表示该连通分量的节点数目。这是因为在 merge 中,只对根节点的 size 进行了处理。

vector<int> root;
vector<int> size;
int find(int x)
{
    return root[x] == -1 ? (x) : (root[x] = find(root[x]));
}
void merge(int x, int y)
{
    x = find(x), y = find(y);
    if (x != y)
        root[y] = x, size[x] += size[y];	// pay attention here
}
// get the size of the connected component where node x is in
int getComponentSize(int x)
{
    return size[find(x)];
}

然后,建立一个基本图,该图是原图 graph 去除所有感染节点 initial 的结果,并把这个基本图转换为上述改进后的并查集。把这个基本图中的节点暂且称为 clean nodes 或者 non-infected nodes .

从直觉上来说,我们应该在 initial 中找到那个标号最小,感染最多 non-infected nodes 的节点,但是这样是否符合预期?

显然是不符合的,来看个例子,设 initial nodes = [a,b,c] ,并设 2 个没有被感染的连通分量为 N1, N2 ,且这 2 个连通分量的点数满足 size(N1) > size(N2),原图 graph 结构如下:

a--N1--c

b--N2

根据题目的意思,需要找到的是使得最终感染数目 M(initial) 最小的节点。

如果我们按照上述所谓的「直觉」:“在 initial 中找到那个感染最多 non-infected nodes 的节点”,应该去除的是节点 a ,但是由于 c 的存在,N1 依旧会被感染,这样 M(initial) = size(N1) + size(N2)。(也就是说,某个连通分量相邻的感染节点多于 1 个,该连通分量最终是必然被感染的,因为我们只能去除一个感染节点。)

实际上,这种情况下正确答案是去除 b ,因为除 b 后:M(initial) = size(N1) ,该结果才是最小的。

所以,我们要找的是:在 initial 中找到那个感染最多 non-infected nodes 的节点 ans,但这些 non-infected nodes 节点只能被 ans 感染,不能被其他的 initial 节点感染(即只能被感染一次)。

代码实现

class Solution
{
public:
    vector<int> root;
    vector<int> size;
    int minMalwareSpread(vector<vector<int>> &graph, vector<int> &initial)
    {
        int N = graph.size();
        root.resize(N, -1);
        size.resize(N, 1);

        // use hash table to mark infected nodes
        vector<bool> init(N, false);
        for (int x : initial)
            init[x] = true;
        // change the non-infected graph into disjoint union set
        for (int i = 0; i < N; i++)
        {
            if (init[i])
                continue;
            for (int j = 0; j < i; j++)
            {
                if (init[j])
                    continue;
                if (graph[i][j] == 1)
                    merge(i, j);
            }
        }
        // table[x] = {...}
        // the set {...} means the non-infected components which initial node x will infect
        // counter[x] = k
        // k means that the non-infected component x will be infected by initial nodes for k times
        vector<int> counter(N, 0);
        unordered_map<int, unordered_set<int>> table;
        for (int u : initial)
        {
            unordered_set<int> infected;
            for (int v = 0; v < graph[u].size(); v++)
            {
                if (!init[v] && graph[u][v] == 1)
                    infected.insert(find(v));
            }
            table[u] = infected;
            for (int x : infected)
                counter[x]++;
        }

        // find the node we want
        int ans = N + 1, maxInfected = -1;
        for (int u : initial)
        {
            int sum = 0;
            for (int x : table[u])
                if (counter[x] == 1)	// must be infected only once
                    sum += getComponentSize(x);
            if (sum > maxInfected || (sum == maxInfected && u < ans))
            {
                ans = u;
                maxInfected = sum;
            }
        }
        return ans;
    }

    int find(int x)
    {
        return root[x] == -1 ? (x) : (root[x] = find(root[x]));
    }

    void merge(int x, int y)
    {
        x = find(x), y = find(y);
        if (x != y)
            root[y] = x, size[x] += size[y];
    }

    int getComponentSize(int x)
    {
        return size[find(x)];
    }
};

尽量减少恶意软件的传播

题目[924]:??做了上面那题之后简单一点。

解题思路

依然是使用上题中 尽量减少恶意软件的传播 II 改进后的并查集结构。

对整个原图处理,转换为并查集。然后,模拟处理。即 (forall x in initial) ,使用集合 (newSet = initial - {x}) 去模拟感染原图,得到最终的感染节点数 t,选取感染节点数 t 最小且标号值最小的 (x) 作为返回结果。

代码实现

class Solution
{
public:
    vector<int> root, size;
    int minMalwareSpread(vector<vector<int>> &graph, vector<int> &initial)
    {
        int N = graph.size();
        root.resize(N, -1);
        size.resize(N, 1);

        for (int i = 0; i < N; i++)
            for (int j = 0; j < i; j++)
                if (graph[i][j] == 1)
                    merge(i, j);

        int ans = N + 1, minval = N + 1;
        // assume that discard the node x in the initial set
        // get the injected value
        for (int x : initial)
        {
            int t = getInjected(x, initial);
            if (t < minval || (t == minval && ans > x))
            {
                minval = t;
                ans = x;
            }
        }
        return ans;
    }
    // use set initial - {x} to inject the graph
    int getInjected(int x, vector<int> &initial)
    {
        unordered_set<int> s;
        for (int k : initial)
        {
            if (k == x)
                continue;
            s.insert(find(k));
        }
        int sum = 0;
        for (int t : s)
            sum += size[find(t)];
        return sum;
    }
    int find(int x)
    {
        return root[x] == -1 ? (x) : (root[x] = find(root[x]));
    }
    void merge(int x, int y)
    {
        x = find(x), y = find(y);
        if (x != y)
            root[y] = x, size[x] += size[y];
    }
};

被围绕的区域

题目[130]:??本题难度一般。

解题思路

本题最特殊的节点是边界上的 O 以及内部与边界 O 相邻的节点。

首先,通过边界的 O 入手,从它开始进行 DFS 搜索,把所有这些的特殊节点标记为 Y 。然后,在 board 中剩下的 O 就是普通的节点(必然是不与边界 O 相邻且被 X 所围绕的),可以把它们全部换成 X 。最后,把所有的 Y 还原为 O

对于搜索方法,既可以是 DFS 也可以是 BFS

代码实现

class Solution
{
public:
    const vector<vector<int>> direction = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
    int row, col;
    void solve(vector<vector<char>> &board)
    {
        row = board.size();
        if (row == 0)
            return;
        col = board[0].size();
        #define func bfs
        for (int j = 0; j < col; j++)
        {
            if (board[0][j] == ‘O‘)
                func(0, j, board);
            if (board[row - 1][j] == ‘O‘)
                func(row - 1, j, board);
        }

        for (int i = 0; i < row; i++)
        {
            if (board[i][0] == ‘O‘)
                func(i, 0, board);
            if (board[i][col - 1] == ‘O‘)
                func(i, col - 1, board);
        }

        for (int i = 0; i < row; i++)
        {
            for (int j = 0; j < col; j++)
            {
                if (board[i][j] == ‘O‘)
                    board[i][j] = ‘X‘;
                if (board[i][j] == ‘Y‘)
                    board[i][j] = ‘O‘;
            }
        }
    }

    void dfs(int i, int j, vector<vector<char>> &board)
    {
        board[i][j] = ‘Y‘;
        for (auto &v : direction)
        {
            int a = i + v[0], b = j + v[1];
            if (a < 0 || b < 0 || a >= row || b >= col)
                continue;
            if (board[a][b] == ‘O‘)
                dfs(a, b, board);
        }
    }

    void bfs(int i, int j, vector<vector<char>> &board)
    {
        typedef pair<int, int> node;
        queue<node> q;
        q.push(node(i, j));
        board[i][j] = ‘Y‘;
        while (!q.empty())
        {
            node n = q.front();
            q.pop();
            for (auto &v : direction)
            {
                int a = n.first + v[0], b = n.second + v[1];
                if (!(a < 0 || b < 0 || a >= row || b >= col) && board[a][b] == ‘O‘)
                    board[a][b] = ‘Y‘, q.push(node(a, b));
            }
        }
    }
};

以上是关于[leetcode] 并查集(Ⅱ)的主要内容,如果未能解决你的问题,请参考以下文章

并查集/dfs解决——leetcode每日一题——1020飞地的数量

LeetCode﹝并查集ி﹞连通分量个数(套用模板一直爽)

LeetCode﹝并查集ி﹞连通分量个数(套用模板一直爽)

并查集-leetcode题目总结

并查集-leetcode题目总结

并查集-leetcode题目总结