Number of Islands
題目描述
Given a 2d grid map of '1' s (land) and '0' s (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.
Example 1:
11110
11010
11000
00000
Answer: 1
Example 2:
11000
11000
00100
00011
Answer: 3
解題思路
拿到題目,如果對數據結構——“圖”的內容比較熟悉,很容易就能想到用DFS
或BFS
來解決。通過這兩種途徑解決該問題相對不太難,并且網上有大量講解,所以我不再贅述。今天要說的是種新的算法(對我來說)——union-find 算法
(見《算法》1.5節)。
union-find 算法簡介
顧名思義,union
即合并,find
為查找,核心就在這兩部分。完整的算法的行為就是通過不斷的find
出某兩個元素分別所屬的集合,判斷他們是否是同一個,然后將不屬于同一集合的兩個元素union
到同一個集合中。
那它到底是用來干嘛的呢?
嗯...書上說...它主要是用來解決動態連通性
問題的。如果你不知道什么是動態連通性
,還是請你查閱書籍,我不覺得自己有能力解釋的比書上更好,起碼在現階段...
好在要解決今天這個問題,你還不需要明白那些復雜的東西,讓我們進入正題。
UF 數據結構的實現(C++)
在此之前我們先來分析下題目:
將所給二維數組抽象成一個圖(如上),標記 1 的為陸地,標記為 0 的是水域,相鄰的陸地連接在一起成為島嶼,即圖中的連通分量。所以本題就轉換成為:求所給圖的連通分量總數。
class UF {
public:
int count = 0; //用來記錄總的連通分量數目
int *id; //數組的元素對應各頂點,存儲的內容為它自身所屬所屬連通分量的名稱
//這里不太容易理解,我打個比方:
//一群互不相識的小孩兒參加夏令營,老師把他們分成多組,每組選出一人為隊長,并要求按組排隊集合。
//集合站隊時,隊長站在最前方,其他人通過辨認自己的隊長來選擇自己的隊伍。
//理論上只要每人都記住隊長的樣子就能站好隊伍,但也并非必須如此。
//也許在第一次排隊的時候小孩兒B沒記住自己的隊長,但他記住了自己前面的小朋友A,那只要A站對了位置,他就能跟著A站到正確的位置了。
//這里id數組的元素就像是一個小孩兒,它所存儲的就是自己記住的那個跟自己在同一隊的小伙伴的樣子,當然這個人可能是隊長,也可能是其他任何一個同隊的人 。
//構造函數
UF(int m, int n, vector<vector<char>>& grid) {
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') count++;
//初始化count值的時候,我們假設每塊陸地起初都是孤立的,所以有多少塊陸地,就有多少個連通分量
}
}
int a[m*n];
id = a;
for (int i = 0; i < m * n; i++) {
id[i] = i;
//如上面所假設的,每塊陸地都是孤立的(換成排隊即指每個孩子除了自己誰都不認識,所以他們各自為營),所以自己的id里存儲的就是自己的下標
}
}
//尋找p所屬的連通分量的名稱(換成排隊即尋找p小孩兒的隊長)
int find(int p) {
while (p != id[p]) {
id[p] = id[id[p]];
p = id[p];
}
return p;
}
//當然隊長只需要認識自己,站在原地不動就好,所以如果id[p]==p,那他就是隊長,否則就說明他(id[p])只是P小孩記住的那個同隊的小伙伴。
//為了找到隊長,需要再問id[p]小朋友他記住的那個同隊的小伙伴(id[id[p]])是不是隊長了。
//一直這么問下去,總會找到隊長本人的(畢竟不聽話的搗蛋鬼只是少數呀)
//判斷p,q是否屬于同一連通分量(即兩塊陸地是否相連...或者兩個小孩是不是同一隊的)
bool isConnected(int p, int q) {
int pRoot = find(p); //p的隊長
int qRoot = find(q); //q的隊長
if (pRoot != qRoot)
return false; //若兩個隊長不是同一個人,則他們不同隊
else
return true; //否則同隊
}
//合并p,q所屬的兩個連通分量(或者說把兩隊小孩組成一隊)
void myUnion(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) return;
id[pRoot] = qRoot; //讓p的隊長(pRoot)認q的隊長(qRoot)為自己的隊長,就是說pRoot的職務被罷免了...
count--; //結果當然是總隊伍數少一個~
}
};
利用 UF 設計算法求島嶼數
實現數據結構的時候,我們假設一開始每塊陸地是孤立的,就是把每塊陸地都當成一個島嶼。這顯然不符合題意,現在我們要做的就是找出所有相連的陸地,并把他們union
到一起,直到所有相連的陸地都被包含在同一個島嶼中為止。
//求總的島嶼數(即其中的連通分量總數)
int numIslands(vector<vector<char>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0)
return 0; //如果沒有陸地,當然就沒有島嶼...
int m = (int)grid.size(), n = (int)grid[0].size();
UF uf = UF(m, n, grid);
//從上向下,從左向右地遍歷整個圖,遇到陸地,就把它和自己周圍的(上下左右四個方向)陸地or島嶼合并
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '0') continue;
int p = i * n + j;
int q;
if (i < m - 1 && grid[i + 1][j] == '1') { //右邊相鄰
q = p + n;
uf.myUnion(p, q);
}
if (j < n - 1 && grid[i][j + 1] == '1') { //下邊相鄰
q = p + 1;
uf.myUnion(p, q);
}
}
}
//由于我們是按照從上向下,從左向右的順序進行遍歷,所以當我們遍歷到某頂點時,它上面和左邊的頂點一定已經遍歷過了
//所以事實上我們只需要對每個頂點作右和下方的判斷即可
return uf.count;
//每次union都會count--,所以當完整遍歷整個圖之后,count就是我們需要的島嶼數量了
}
??int main(int argc, const char * argv[]) {
vector<vector<char>> grid;
string s[4] = {"11000", "11000", "00100", "00011"};
int i = 0; while (i < 4) {
vector<char> line(s[i].begin(), s[i].begin() + s[i].length());
grid.push_back(line);
++i;
}
cout << numIslands(grid) << endl;
return 0;
}
總結
一開始我只是看到說union-find算法
可以解決島嶼問題,剛好手邊的《算法》書里有詳細講解,就想學習后自己試著實現一下。可當我花了近兩個小時終于似乎學會了這個算法,打算小試牛刀的時候,卻完全找不到用它解決島嶼問題的思路。最終我還是放棄了,到 LeetCode 上查看了大佬的 solution,并醍醐灌頂,自嘆不如。大佬是用 Java 實現的,看過之后為加深印象,也為溫習 C++,我決定重新實現一下,便有了這篇文章。寫過之后我發現自己對這個算法的理解更深了一層,希望能幫助更多的朋友。