並查集 (Union Set) 一種常見的應用是計算一個圖中連通份量的個數。好比:node
a e / \ | b c f | | d g
上圖的連通份量的個數爲 2 。算法
並查集的主要思想是在每一個連通份量的集合中,選取一個表明,做爲這個連通份量的根。根的選取是任意的,由於連通份量集合中每一個元素都是等價的。咱們只需關心根的個數(也是連通份量的個數)。例如:數組
a e / | \ / \ b c d f g
也就是說:root[b] = root[c] = root[d] = a
而 root[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
函數也能夠經過迭代實現:lua
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]:點擊 🔗連接 查看題目。spa
示例code
輸入: [[1,1,0], [1,1,0], [0,0,1]] 輸出: 2 說明:已知學生0和學生1互爲朋友,他們在一個朋友圈。第2個學生本身在一個朋友圈。因此返回2。
解題思路component
典型的計算連通份量的模板題。ci
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
的值爲:
對於題目給出的輸入x1
和x2
都是字符串,須要優化空間,因此採起預處理把每一個 xi
都映射爲一個 int
。
給定 (u,v)
,採起 BFS 去搜索 u
到 v
的路徑,同時在 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 操做的次數爲:
也就是說,本題所求便是:圖的點數減去連通份量的個數。
代碼實現
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; } };