「程式碼隨想錄演算法訓練營」第四十五天 | 圖論 part3

云雀AC了一整天發表於2024-08-23

目錄
  • 101. 孤島的總面積
    • DFS思路
    • BFS思路
  • 102. 沉沒孤島
  • 103. 水流問題
  • 104. 建造最大島嶼

101. 孤島的總面積

題目連結:https://kamacoder.com/problempage.php?pid=1173
文章講解:https://programmercarl.com/kamacoder/0101.孤島的總面積.html
題目狀態:看題解

DFS思路

思路:

程式碼結構

  1. 變數和方向陣列:

    • dir[4][2]:定義了四個方向的移動(上、左、下、右)。
    • count:用於統計符合條件的陸地塊數量。
  2. 深度優先搜尋函式 dfs:

    • 輸入引數為二維陣列 grid 和當前座標 (x, y)
    • 將當前座標的值設為 0(表示已訪問)。
    • 增加 count 計數。
    • 遍歷四個方向,遞迴呼叫 dfs,前提是新座標在邊界內且值為 1(表示陸地)。
  3. 主函式 main:

    • 讀取網格的大小 nm
    • 初始化一個 n x m 的二維陣列 grid
    • 從輸入讀取網格資料。
    • 從網格的四個邊界開始,使用 dfs 清除邊界連線的陸地。
    • 重置 count
    • 遍歷內部陸地,使用 dfs 統計不與邊界相連的陸地塊。
    • 輸出 count

邏輯分析

  • 邊界處理:

    • 先從網格的邊界開始,使用 dfs 清除與邊界相連的陸地。這樣可以排除掉與邊界相連的陸地塊。
  • 統計孤立陸地:

    • 遍歷整個網格,使用 dfs 統計不與邊界相連的陸地塊。

程式碼:

#include <iostream>
#include <vector>
using namespace std;

int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 儲存四個方向
int count; // 統計符合題目要求的陸地空格數量

void dfs(vector<vector<int>> &grid, int x, int y) {
    grid[x][y] = 0;
    count++;
    for(int i = 0; i < 4; ++i) { // 向四個方向遍歷
        int nextx = x + dir[i][0];
        int nexty = y + dir[i][1];
        // 超過邊界
        if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue;
        // 不符合條件,不繼續遍歷
        if(grid[nextx][nexty] == 0) continue;

        dfs(grid, nextx,nexty);
    }
    return;
}

int main() {
    int n, m;
    cin >> n >> m;
    vector<vector<int>> grid(n, vector<int>(m, 0));
    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < m; ++j) {
            cin >> grid[i][j];
        }
    }

    // 從左側邊和右側邊,向中間遍歷
    for(int i = 0; i < n; ++i) {
        if(grid[i][0] == 1) dfs(grid, i, 0);
        if(grid[i][m - 1] == 1) dfs(grid, i, m - 1);
    }

    // 從上邊和下邊,向中間遍歷
    for(int j = 0; j < m; ++j) {
        if(grid[0][j] == 1) dfs(grid, 0, j);
        if(grid[n - 1][j] == 1) dfs(grid, n - 1, j);
    }

    count = 0;
    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < m; ++j) {
            if(grid[i][j] == 1) dfs(grid, i, j);
        }
    }
    cout << count << endl;
}

BFS思路

思路:

程式碼結構

  1. 方向陣列 dir

    • 表示四個方向(右、下、左、上)的移動。
    • dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1} 表示:
      • 右:(0, 1)
      • 下:(1, 0)
      • 左:(-1, 0)
      • 上:(0, -1)
  2. 全域性變數 count

    • 用於統計符合條件的陸地格子數量。
  3. 廣度優先搜尋函式 bfs

    • 引數:網格 grid,起始座標 (x, y)
    • 使用佇列 que 來實現BFS。
    • 將起始點標記為訪問過(設定為0)並加入佇列。
    • 遍歷佇列中的每個點,向四個方向擴充套件。
    • 檢查邊界條件,確保不越界。
    • 對於相鄰的陸地格子,標記為訪問過並加入佇列。
  4. 主函式 main

    • 輸入:網格大小 nm,以及網格資料。
    • 初始化網格 grid
    • 從網格的邊界開始,移除與邊界相連的陸地。
    • 重置 count
    • 遍歷整個網格,統計被包圍的陸地數量。
    • 輸出結果。

邏輯流程

  1. 輸入處理

    • 讀取網格的大小 nm
    • 讀取網格資料,填充 grid
  2. 邊界BFS

    • 從網格的四個邊界開始,使用 bfs 移除與邊界相連的陸地。
    • 這部分陸地不計入最終統計,因為題目可能要求只統計被完全包圍的陸地。
  3. 統計被包圍的陸地

    • 重置 count
    • 遍歷網格內部,使用 bfs 統計所有剩餘陸地格子的數量。
  4. 輸出結果

    • 列印出符合條件的陸地格子數量。

程式碼:

#include <iostream>
#include <vector>
#include <queue>
using namespace std;

int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 儲存四個方向
int count = 0; // 統計符合題目要求的陸地空格數量

void bfs(vector<vector<int>> &grid, int x, int y) {
    queue<pair<int, int>> que;
    que.push({x, y});
    grid[x][y] = 0; // 只要加入佇列,立即標記
    count++;
    while(!que.empty()) {
        pair<int, int> cur = que.front();
        que.pop();
        int curx = cur.first;
        int cury = cur.second;
        for(int i = 0; i < 4; ++i) {
            int nextx = curx + dir[i][0];
            int nexty = cury + dir[i][1];
            if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid.size()) continue; // 越界了,直接跳過
            if(grid[nextx][nexty] == 1) {
                que.push({nextx, nexty});
                count++;
                grid[nextx][nexty] = 0; // 只要加入佇列立刻標記
            }
        }
    }    
}

int main() {
    int n, m;
    cin >> n >> m;
    vector<vector<int>> grid(n, vector<int>(m, 0));
    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < m; ++j) {
            cin >> grid[i][j];
        }
    }

    // 從左側邊和右側邊,向中間遍歷
    for(int i = 0; i < n; ++i) {
        if(grid[i][0] == 1) bfs(grid, i, 0);
        if(grid[i][m - 1] == 1) bfs(grid, i, m - 1);
    }

    // 從上邊和下邊,向中間遍歷
    for(int j = 0; j < m; ++j) {
        if(grid[0][j] == 1) bfs(grid, 0, j);
        if(grid[n - 1][j] == 1) bfs(grid, n - 1, j);
    }

    count = 0;
    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < m; ++j) {
            if(grid[i][j] == 1) bfs(grid, i, j);
        }
    }
    cout << count << endl;
}

102. 沉沒孤島

題目連結:https://kamacoder.com/problempage.php?pid=1174
文章講解:https://programmercarl.com/kamacoder/0102.沉沒孤島.html
題目狀態:看題解

思路:

程式碼結構

  1. 方向陣列 dir:

    • 定義了四個方向(上、左、下、右)的移動座標:{-1, 0}{0, -1}{1, 0}{0, 1}
  2. DFS 函式:

    • 引數: 接受一個二維陣列 grid 和當前座標 (x, y)
    • 功能: 將當前陸地標記為 2,並遞迴遍歷四個方向的相鄰陸地。
    • 邊界條件: 檢查是否越界或遇到水(0)或已訪問(2)的格子。
  3. 主函式 main:

    • 輸入: 從標準輸入讀取網格大小 nm,以及網格資料。
    • 步驟一: 從邊界開始,將與邊界相連的陸地(1)透過 DFS 標記為 2。
    • 步驟二: 將剩餘的陸地(孤島)變成水(0)。
    • 步驟三: 將標記為 2 的邊界陸地恢復為陸地(1)。
    • 輸出: 列印修改後的網格。

邏輯流程

  • 邊界處理:

    • 從網格的四條邊界開始,使用 DFS 將所有與邊界相連的陸地標記為 2。
  • 孤島處理:

    • 遍歷整個網格,將所有未標記為 2 的陸地(即孤島)變為水(0)。
  • 恢復邊界陸地:

    • 將標記為 2 的格子恢復為陸地(1),因為它們與邊界相連。

程式碼:

#include <iostream>
#include <vector>
using namespace std;

int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 儲存四個方向
void dfs(vector<vector<int>> &grid, int x, int y) {
    grid[x][y] = 2;
    for(int i = 0; i < 4; ++i) { // 向四個方向遍歷
        int nextx = x + dir[i][0];
        int nexty = y + dir[i][1];
        // 超過邊界
        if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue;
        // 不符合條件,不繼續遍歷
        if(grid[nextx][nexty] == 0 || grid[nextx][nexty] == 2) continue;
        dfs(grid, nextx, nexty);
    }
    return;
}

int main() {
    int n, m;
    cin >> n >> m;
    vector<vector<int>> grid(n, vector<int>(m, 0));
    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < m; ++j) {
            cin >> grid[i][j];
        }
    }

    // 步驟一:(將邊界陸地1變為2)
    // 從左邊和右邊向中間遍歷
    for(int i = 0; i < n; ++i) {
        if(grid[i][0] == 1) dfs(grid, i, 0);
        if(grid[i][m - 1] == 1) dfs(grid, i, m - 1);
    }

    // 從上邊和下邊向中間遍歷
    for(int j = 0; j < m; ++j) {
        if(grid[0][j] == 1) dfs(grid, 0, j);
        if(grid[n - 1][j] == 1) dfs(grid, n - 1, j);
    }

    // 步驟二:將地圖中的1(孤島)變為0
    // 步驟三:將地圖中的2(邊界陸地)變為1
    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < m; ++j) {
            if(grid[i][j] == 1) grid[i][j] = 0;
            if(grid[i][j] == 2) grid[i][j] = 1;
        }
    }

    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < m; ++j) {
            cout << grid[i][j] << " ";
        }
        cout << endl;
    }
}

103. 水流問題

題目連結:https://kamacoder.com/problempage.php?pid=1175
文章講解:https://programmercarl.com/kamacoder/0103.水流問題.html
題目狀態:看題解

思路:

透過便利在給定的網格中找到能夠同時到達兩組邊界的點。透過深度優先搜尋,計算每個點可以訪問的區域,並檢查這些區域是否滿足到達邊界的條件。

  1. 變數宣告

    • n, m: 網格的行數和列數。
    • dir: 四個方向的移動向量,分別表示上、左、下、右。
  2. 函式 dfs

    • 引數:grid(網格)、visited(記錄訪問狀態的二維布林陣列)、xy(當前座標)。
    • 功能:從 (x, y) 開始,遞迴地訪問所有可以到達的點。
    • 檢查條件:
      • 如果當前點已經訪問過,直接返回。
      • 確保下一個點在網格範圍內。
      • 確保移動方向的高度不增加(即只能走向相等或更低的高度)。
  3. 函式 isResult

    • 引數:grid(網格)、xy(起始座標)。
    • 功能:檢查從 (x, y) 出發是否可以同時到達第一組和第二組邊界。
    • 使用 dfs 標記從 (x, y) 可以到達的所有點。
    • 檢查是否可以到達:
      • 第一組邊界(上邊界和左邊界)。
      • 第二組邊界(右邊界和下邊界)。
    • 返回 true 如果可以同時到達兩組邊界,否則返回 false
  4. main 函式

    • 讀取網格的大小 nm
    • 讀取網格的高度資訊。
    • 遍歷每個點 (i, j),呼叫 isResult 檢查是否滿足條件。
    • 如果滿足條件,輸出該點的座標。

程式碼:

#include <iostream>
#include <vector>
using namespace std;

int n, m;
int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1};

// 從x,y出發把可以走的地方都標記上
void dfs(vector<vector<int>> &grid, vector<vector<bool>> &visited, int x, int y) {
    if(visited[x][y]) return;
    visited[x][y] = true;
    for(int i = 0; i < 4; ++i) {
        int nextx = x + dir[i][0];
        int nexty = y + dir[i][1];
        if(nextx < 0 || nextx >= n || nexty < 0 || nexty >= m) continue;
        if(grid[x][y] < grid[nextx][nexty]) continue; // 高度不合適
        dfs(grid, visited, nextx, nexty);
    }
    return;
}

bool isResult(vector<vector<int>> &grid, int x, int y) {
    vector<vector<bool>> visited(n, vector<bool>(m, false));
    // 深搜,將x,y出發能到的節點都標記上
    dfs(grid, visited, x, y);
    bool isFirst = false;
    bool isSecond = false;
    // 以下就是判斷x,y出發,是否到達第一組邊界和第二組邊界
    // 第一邊界的上邊
    for(int j = 0; j < m; ++j) {
        if(visited[0][j]) {
            isFirst = true;
            break;
        }
    }
    // 第一邊界的左邊
    for(int i = 0; i < n; ++i) {
        if(visited[i][0]) {
            isFirst = true;
            break;
        }
    }
    // 第二邊界的右邊
    for(int j = 0; j < m; ++j) {
        if(visited[n - 1][j]) {
            isSecond = true;
            break;
        }
    }
    // 第二邊界下邊
    for(int i = 0; i < n; ++i) {
        if(visited[i][m - 1]) {
            isSecond = true;
            break;
        }
    }
    if(isFirst && isSecond) return true;
    return false;
}

int main() {
    cin >> n >> m;
    vector<vector<int>> grid(n, vector<int>(m, 0));
    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < m; ++j) {
            cin >> grid[i][j];
        }
    }
    // 遍歷每一個點,看是否能同時到達第一組邊界和第二組邊界
    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < m; ++j) {
            if(isResult(grid, i, j)) cout << i << " " << j << endl;
        }
    }
}

上述程式碼和思路在卡碼網中執行會出現超時情況,下面是最佳化的思路和程式碼。

思路:

不進行單個元素的遍歷,而是從邊界向中間遍歷。

image

image

程式碼:

#include <iostream>
#include <vector>
using namespace std;
int n, m;
int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1};
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y) {
    if (visited[x][y]) return;

    visited[x][y] = true;

    for (int i = 0; i < 4; i++) {
        int nextx = x + dir[i][0];
        int nexty = y + dir[i][1];
        if (nextx < 0 || nextx >= n || nexty < 0 || nexty >= m) continue;
        if (grid[x][y] > grid[nextx][nexty]) continue; // 注意:這裡是從低向高遍歷

        dfs (grid, visited, nextx, nexty);
    }
    return;
}

int main() {

    cin >> n >> m;
    vector<vector<int>> grid(n, vector<int>(m, 0));

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> grid[i][j];
        }
    }
    // 標記從第一組邊界上的節點出發,可以遍歷的節點
    vector<vector<bool>> firstBorder(n, vector<bool>(m, false));

    // 標記從第一組邊界上的節點出發,可以遍歷的節點
    vector<vector<bool>> secondBorder(n, vector<bool>(m, false));

    // 從最上和最下行的節點出發,向高處遍歷
    for (int i = 0; i < n; i++) {
        dfs (grid, firstBorder, i, 0); // 遍歷最左列,接觸第一組邊界
        dfs (grid, secondBorder, i, m - 1); // 遍歷最右列,接觸第二組邊界
    }

    // 從最左和最右列的節點出發,向高處遍歷
    for (int j = 0; j < m; j++) {
        dfs (grid, firstBorder, 0, j); // 遍歷最上行,接觸第一組邊界
        dfs (grid, secondBorder, n - 1, j); // 遍歷最下行,接觸第二組邊界
    }
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            // 如果這個節點,從第一組邊界和第二組邊界出發都遍歷過,就是結果
            if (firstBorder[i][j] && secondBorder[i][j]) cout << i << " " << j << endl;;
        }
    }
}

104. 建造最大島嶼

題目連結:https://kamacoder.com/problempage.php?pid=1176
文章講解:https://programmercarl.com/kamacoder/0104.建造最大島嶼.html
題目狀態:看題解,好難

思路:

找出透過將一個海水格子變為陸地後,可能形成的最大島嶼面積。透過標記和記錄每個島嶼的面積,高效地計算出將某個海水格子變為陸地後的最大可能面積。

  1. 變數宣告

    • n, m: 網格的行數和列數。
    • count: 當前島嶼的面積。
    • dir: 四個方向的移動向量,分別表示右、下、左、上。
  2. 函式 dfs

    • 引數:grid(網格)、visited(記錄訪問狀態的二維布林陣列)、xy(當前座標)、mark(島嶼標記)。
    • 功能:透過深度優先搜尋,將所有連線的陸地標記為同一個島嶼。
    • 終止條件:當前點已經訪問過或是海水。
    • 標記當前陸地並遞迴訪問相鄰的陸地。
  3. main 函式

    • 讀取網格的大小 nm
    • 讀取網格的高度資訊。
    • 初始化 visited 陣列和 gridNum 對映,用於儲存每個島嶼的面積。
    • 遍歷網格,使用 dfs 查詢並標記所有島嶼,記錄每個島嶼的面積。
    • 檢查是否整個網格都是陸地,如果是,直接輸出總面積。
  4. 計算最大可能的島嶼面積

    • 遍歷每個海水格子 (i, j),嘗試將其變為陸地。
    • 對於每個海水格子,檢查其四個相鄰格子。
    • 計算相鄰島嶼的總面積(避免重複計算同一島嶼)。
    • 更新最大島嶼面積。

程式碼:

#include <iostream>
#include <vector>
#include <unordered_set>
#include <unordered_map>
using namespace std;
int n, m;
int count;

int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四個方向
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y, int mark) {
    if (visited[x][y] || grid[x][y] == 0) return; // 終止條件:訪問過的節點 或者 遇到海水
    visited[x][y] = true; // 標記訪問過
    grid[x][y] = mark; // 給陸地標記新標籤
    count++;
    for (int i = 0; i < 4; i++) {
        int nextx = x + dir[i][0];
        int nexty = y + dir[i][1];
        if (nextx < 0 || nextx >= n || nexty < 0 || nexty >= m) continue;  // 越界了,直接跳過
        dfs(grid, visited, nextx, nexty, mark);
    }
}

int main() {
    cin >> n >> m;
    vector<vector<int>> grid(n, vector<int>(m, 0));

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> grid[i][j];
        }
    }
    vector<vector<bool>> visited(n, vector<bool>(m, false)); // 標記訪問過的點
    unordered_map<int ,int> gridNum;
    int mark = 2; // 記錄每個島嶼的編號
    bool isAllGrid = true; // 標記是否整個地圖都是陸地
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (grid[i][j] == 0) isAllGrid = false;
            if (!visited[i][j] && grid[i][j] == 1) {
                count = 0;
                dfs(grid, visited, i, j, mark); // 將與其連結的陸地都標記上 true
                gridNum[mark] = count; // 記錄每一個島嶼的面積
                mark++; // 記錄下一個島嶼編號
            }
        }
    }
    if (isAllGrid) {
        cout << n * m << endl; // 如果都是陸地,返回全面積
        return 0; // 結束程式
    }

    // 以下邏輯是根據新增陸地的位置,計算周邊島嶼面積之和
    int result = 0; // 記錄最後結果
    unordered_set<int> visitedGrid; // 標記訪問過的島嶼
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            count = 1; // 記錄連線之後的島嶼數量
            visitedGrid.clear(); // 每次使用時,清空
            if (grid[i][j] == 0) {
                for (int k = 0; k < 4; k++) {
                    int neari = i + dir[k][1]; // 計算相鄰座標
                    int nearj = j + dir[k][0];
                    if (neari < 0 || neari >= n || nearj < 0 || nearj >= m) continue;
                    if (visitedGrid.count(grid[neari][nearj])) continue; // 新增過的島嶼不要重複新增
                    // 把相鄰四面的島嶼數量加起來
                    count += gridNum[grid[neari][nearj]];
                    visitedGrid.insert(grid[neari][nearj]); // 標記該島嶼已經新增過
                }
            }
            result = max(result, count);
        }
    }
    cout << result << endl;

}

相關文章