[leetcode] 並查集(Ⅰ)

sinkinben發表於2020-04-27

預備知識

並查集 (Union Set) 一種常見的應用是計算一個圖中連通分量的個數。比如:

    a     e
   / \    |
  b   c   f
  |       |
  d       g

上圖的連通分量的個數為 2 。

並查集的主要思想是在每個連通分量的集合中,選取一個代表,作為這個連通分量的根。根的選取是任意的,因為連通分量集合中每個元素都是等價的。我們只需關心根的個數(也是連通分量的個數)。例如:

   a       e
 / | \    / \
b  c  d  f   g

也就是說:root[b] = root[c] = root[d] = aroot[a] = -1(根節點的特徵也可以定義為 root[x] = x)。

最後計算 root[x] == -1 的個數即可,這也就是連通分量的個數。虛擬碼如下:

// n nodes, all nodes is independent at the beginning
vector<int> root(n, -1);
int find(int x)
{
    return root[x] == -1 ? x : (root[x] = find(root[x]));
}
// if x and y are connected, then call union(x, y)
void unionSet(int x, int y)
{
    x = find(x), y = find(y);
    if (x != y)  root[x] = y; // it also can be root[y] = x
}
int main()
{
    // (x,y) are connected
    while (cin >> x >> y)
        unionSet(x, y);
    // print the number of connectivity components
    print(count(root.begin(), root.end(), -1));
}

find 函式也可以通過迭代實現:

int find(int x)
{
    int t = -1, p = x;
    while (root[p] != -1)  p = root[p];
    while (x != p)  {t = root[x]; root[x] = p; x = t;}
    return p;
}

朋友圈

題目[547]:點選 ?連結 檢視題目。

示例

輸入: 
[[1,1,0],
 [1,1,0],
 [0,0,1]]
輸出: 2 
說明:已知學生0和學生1互為朋友,他們在一個朋友圈。第2個學生自己在一個朋友圈。所以返回2。

解題思路

典型的計算連通分量的模板題。

class Solution
{
public:
    int findCircleNum(vector<vector<int>> &m)
    {
        int len = m.size();
        vector<int> v(len, -1);
        int a, b;
        for (int i = 0; i < len; i++)
        {
            for (int j = 0; j < i; j++)
            {
                if (m[i][j] == 1)
                {
                    a = findRoot(v, i), b = findRoot(v, j);
                    if (a != b)
                        v[a] = b;
                }
            }
        }
        return count(v.begin(), v.end(), -1);
    }

    int findRoot(vector<int> &root, int x)
    {
        // return (v[x] == -1) ? x : (v[x] = findRoot(v, v[x]));
        int p = x;
        while (root[p] != -1)  p = root[p];
        int t;
        while (x != p)  {t = root[x]; root[x] = p; x = t;}
        return p;        
    }
};

冗餘連線

題目[684]:點選 ?連結 檢視題目。

解題思路

關鍵在於找到使得現有的圖中成環的第一條邊。也就是對於新邊 (x,y) 使得 findroot(x) == findroot(y),該邊就是問題所求。

#include "leetcode.h"
class Solution
{
public:
    vector<int> findRedundantConnection(vector<vector<int>> &edges)
    {
        int n = edges.size();
        vector<int> r(n + 1, -1);
        vector<int> ans(2);
        int x, y;
        for (auto &v : edges)
        {
            x = findroot(r, v[0]), y = findroot(r, v[1]);
            if (x != y)
                r[x] = y;
            else
            {
                ans[0] = v[0], ans[1] = v[1];
                break;
            }
        }
        return ans;
    }
    int findroot(vector<int> &r, int x)
    {
        return (r[x] == -1) ? x : (r[x] = findroot(r, r[x]));
    }
};

情侶牽手

題目[765]:?題目詳情

解題思路

本題用並查集似乎會使問題變得複雜(實際上我自己也沒想到用並查集怎麼做?)。這裡採用了簡單的模擬法(本質上是貪心演算法),但怎麼證明是「最小次數」確實是個問題。

設第 i 個人的編號為 row[i]

  • 如果 row[i] 為偶數,那麼其伴侶編號為 row[i] + 1
  • 如果 row[i] 為奇數,那麼其伴侶編號為 row[i] - 1

也即是說:對於任意一個 row[i] ,其伴侶編號為 row[i] ^ 1

每次從 row 讀取 2 個數: x = row[i], y = row[i+1],如果 (x ^ 1) == y(x,y) 配對成功,否則找到 x 的伴侶,讓其與 y 交換。

  • 樸素暴力模擬法

直接遍歷後面的元素,找到 row[i] 的伴侶,與 row[i+1] 交換位置。

int minSwapsCouples(vector<int> &row)
{
    int n = row.size();
    int ans = 0;
    for (int i = 0; i < n; i += 2)
    {
        if ((row[i] ^ 1) == row[i + 1])
            continue;
        int target = row[i] ^ 1;
        for (int j = i + 2; j < n; j++)
        {
            if (row[j] == target)
            {
                swap(row[i + 1], row[j]), ans++;
                break;
            }
        }
    }
    return ans;
}
  • 優化查詢伴侶

上面我們採取的是遍歷找伴侶,實際上可以通過雜湊表記錄每個人的座位號。對於陣列 index[N] 和給定的編號 row[i],令 index[row[i]] = i。時間和空間複雜度均為 \(O(N)\)

int minSwapsCouples2(vector<int> &row)
{
    int ans = 0;
    int len = row.size();
    vector<int> v(len, 0);
    for (int i = 0; i < len; i++)
        v[row[i]] = i;
    int t;
    for (int i = 0; i < len; i += 2)
    {
        t = row[i] ^ 1;
        if (t != row[i + 1])
        {
            ans++;
            int idx = v[t];
            // swap position
            swap(row[idx], row[i + 1]);
            // update hash table
            v[row[idx]] = idx;
            v[row[i + 1]] = i + 1;
        }
    }
    return ans;
}

除法求值

題目[399]:?連結

解題思路

這是一道圖論的題目(廢話)。首先對於 x1 / x2 = value 這樣的等式,使用二維結構 graph[x1][x2] = value 去記錄(圖的二維矩陣形式)。

給定 (u, v) ,如果存在路徑 u -> x0 -> ... -> xn -> v,那麼 u/v 的值為:

\[getval(u,v) = graph(u,x_0) \cdot \prod_{i=0}^{n-1}{graph(x_i,x_{i+1})} \cdot graph(x_n,v) \]

對於題目給出的輸入x1x2都是字串,需要優化空間,所以採取預處理把每個 xi 都對映為一個 int

  • BFS

給定 (u,v) ,採取 BFS 去搜尋 uv 的路徑,同時在 graph 中記錄 u/xi 的值(這樣可減少一定量的重複搜尋)。

class Solution
{
public:
    unordered_map<string, int> m;
    unordered_map<int, unordered_map<int, double>> graph;
    vector<double> calcEquation(vector<vector<string>> &equations, vector<double> &values, vector<vector<string>> &queries)
    {
        int V = hashstring(equations);
        // init gragh
        for (size_t i = 0; i < equations.size(); i++)
        {
            int a = m[equations[i][0]], b = m[equations[i][1]];
            double v = values[i];
            graph[a][b] = v, graph[b][a] = 1 / v;
            graph[a][a] = graph[b][b] = 1;
        }
        // exec
        vector<bool> visited(V, false);
        for (auto &v : queries)
        {
            string &a = v[0], &b = v[1];
            // one of the arguments is not given in equations
            if (m.find(a) == m.end() || m.find(b) == m.end())
            {
                result.emplace_back(-1);
                continue;
            }
            // bfs(a) to find whether if it can reach b 
            result.emplace_back(getval(m[a], m[b]));
        }
        return result;
    }

    // bfs
    double getval(int x, int y)
    {
        if (graph[x].find(y) != graph[x].end())
            return graph[x][y];
        typedef pair<int, double> node;
        queue<node> q;
        vector<int> vis(graph.size(), 0);
        q.push(node(x, 1));
        vis[x] = 1;
        while (!q.empty())
        {
            node n = q.front();
            q.pop();
            graph[x][n.first] = n.second;
            graph[n.first][x] = 1 / n.second;
            if (n.first == y)
                return n.second;
            for (auto &p : graph[n.first])
            {
                if (vis[p.first] == 0)
                {
                    vis[p.first] = 1;
                    q.push(node(p.first, n.second * p.second));
                }
            }
        }
        return -1;
    }

    // pre hashing string into int
    int hashstring(vector<vector<string>> &e)
    {
        int idx = 0;
        for (auto &v : e)
        {
            if (m.find(v[0]) == m.end())
                m[v[0]] = idx++;
            if (m.find(v[1]) == m.end())
                m[v[1]] = idx++;
        }
        return idx;
    }
};
  • 並查集

待完善。

島嶼數量

題目[200]:?連結

解題思路

由題意可得,顯然是找連通分量的數目,需要套上並查集的模板。

將每一個 grid[i][j] 看作是一個節點,那麼二維陣列中相鄰1 需要合併,所有0 可以合併在一起(可以把任意的 0 作為根)。

並查集需要用一個 root 陣列,其下標含義是每個節點的標號。設 rows, cols 分別為 grid 的行數和列數,使用 i * cols + j 作為節點 grid[i][j] 的標號,並設一個 waterFiled = rows * cols 作為所有 0 的根節點(根據題意,地圖的所有水域都是連在一起的)。那麼 root 陣列的長度為 rows + cols + 1

關鍵點是如何處理合並?

  • 對於 grid[i][j] == 0 的節點,只需要把 getid(i, j) 和水域的根節點 waterField 連線合並。
  • 對於 grid[i][j] == 1 的節點,需要合併相鄰的 1 。「相鄰」一共有 4 個位置,但是由於掃描陣列 grid 的方向是從左到右,從上到下,因此只需要看節點右邊和下邊是否為 1 即可。如果 grid[i][j+1] == 1 那麼需要 merge(getid(i, j), getid(i, j+1))grid[i+1][j] 與之同理。

根據上面的操作,需要對 grid 進行預處理,在地圖的最右邊和最下邊使用 0 包圍起來。

#define getid(i, j) ((i)*cols + (j))
class Solution
{
public:
    int numIslands(vector<vector<char>> &grid)
    {
        const int rows = grid.size();
        if (rows == 0)
            return 0;
        const int cols = grid[0].size();
        const int waterField = rows * cols;
        vector<int> root(rows * cols + 1, -1);

        // preparation
        for (auto &v : grid)
            v.push_back('0');
        grid.push_back(vector<char>(cols + 1, '0'));

        for (int i = 0; i < rows; i++)
        {
            for (int j = 0; j < cols; j++)
            {
                if (grid[i][j] == '1')
                {
                    if (grid[i][j + 1] == '1')
                        merge(root, getid(i, j), getid(i, j + 1));
                    if (grid[i + 1][j] == '1')
                        merge(root, getid(i, j), getid(i + 1, j));
                }
                else
                {
                    merge(root, waterField, getid(i, j));
                }
            }
        }
        return count(root.begin(), root.end(), -1) - 1;
    }

    int find(vector<int> &r, int x)
    {
        return (r[x] == -1) ? (x) : (r[x] = find(r, r[x]));
    }

    void merge(vector<int> &r, int x, int y)
    {
        int a = find(r, x);
        int b = find(r, y);
        if (a != b)
            r[b] = a;
    }
};

移除最多的同行或同列石頭

題目[947]:?連結

示例解析

輸入:stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]
輸出:5

使用 stones 的下標作為每個點的記號,對於上述輸入,可以用下圖來表示:

     3----5
          |
1---------4
|
0----2

可以按照 3, 5, 4, 1, 0 的順序去除,所以輸出為 5 。

解題思路

使用下標作為各個點的記號,並且兩個點相連的條件為:橫座標或縱座標相等。下面考慮使用並查集解決。

顯然,對於任意多的點,圖中就會有若干的連通分量,但其形式總是下面 2 種形式的組合:

Type-1
a----b

Type-2
a
|
b

通過歸納法容易證明:對於有 n 個點的連通分量,最多可執行 move 操作的次數為 n - 1(證明思路:對於 \(n=2\) 或者 \(n=3\) 的情況是顯然成立的,而 \(n=k\) 的連通分量總是可以通過 \(n=2\)\(n=3\) 的情況組合而成。)

假設有 n 個連通分量,在第 i 個分支包含的節點數為 p[i],這個圖最多可以執行的 move 操作的次數為:

\[\sum_{i=1}^{n}(p_i-1) = -n + \sum_{i=1}^{n}{p_i} \]

也就是說,本題所求即是:圖的點數減去連通分量的個數

程式碼實現

stones 的下標作為節點的記號。通過 rowmap<int, vector<int>> 記錄位於同一行的點,colmap 記錄同一列的點,那麼同一個 vector 裡面的點都是可以合併到同一個連通分量的。選取 vector[0] 作為這個連通分量的根。

class Solution
{
public:
    int removeStones(vector<vector<int>> &stones)
    {
        int len = stones.size();
        vector<int> root(len, -1);

        unordered_map<int, vector<int>> rowmap, colmap;
        for (int i = 0; i < stones.size(); i++)
        {
            const auto &v = stones[i];
            rowmap[v[0]].emplace_back(i);
            colmap[v[1]].emplace_back(i);
        }

        for (auto &p : rowmap)
        {
            auto &v = p.second;
            for (int x : v)
                merge(root, v[0], x);
        }

        for (auto &p : colmap)
        {
            auto &v = p.second;
            for (int x : v)
                merge(root, v[0], x);
        }
        return len - count(root.begin(), root.end(), -1);
    }
    int find(vector<int> &r, int x)
    {
        return (r[x] == -1) ? (x) : (r[x] = find(r, r[x]));
    }
    void merge(vector<int> &r, int x, int y)
    {
        if (x == y)
            return;
        x = find(r, x), y = find(r, y);
        if (x != y)
            r[y] = x;
    }
};

相關文章