預備知識
並查集 (Union Set) 一種常見的應用是計算一個圖中連通分量的個數。比如:
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
函式也可以通過迭代實現:
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
的值為:
對於題目給出的輸入x1
和x2
都是字串,需要優化空間,所以採取預處理把每個 xi
都對映為一個 int
。
- BFS
給定 (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;
}
};