圖演算法DFS與BFS
BFS和DFS代表對圖進行遍歷,即搜尋的演算法,搜尋演算法中常用的只要有兩種演算法:深度優先遍歷(Depth-First-Search : DFS)和廣度優先遍歷(Breadth-First-Search : BFS)。一個圖結構可以用來表示大量現實生活中的問題,比如,道路網路,計算機網路,社交網路,使用者身份解析圖
①DFS深度優先搜尋樹
200. 島嶼數量
給你一個由 '1'
(陸地)和 '0'
(水)組成的的二維網格,請你計算網格中島嶼的數量。
島嶼總是被水包圍,並且每座島嶼只能由水平方向和/或豎直方向上相鄰的陸地連線形成。
此外,你可以假設該網格的四條邊均被水包圍。
示例 1:
輸入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
輸出:1
示例 2:
輸入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
輸出:3
如何在二維矩陣中使用 DFS 搜尋呢?
// 二維矩陣遍歷框架
void dfs(int[][] grid, int i, int j, boolean[][] visited) {
int m = grid.length, n = grid[0].length;
// 超出索引邊界
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
// 已遍歷過 (i, j)
if (visited[i][j]) {
return;
}
// 進入節點 (i, j)
visited[i][j] = true;
dfs(grid, i - 1, j, visited); // 上
dfs(grid, i + 1, j, visited); // 下
dfs(grid, i, j - 1, visited); // 左
dfs(grid, i, j + 1, visited); // 右
visited[i][j] = false;
}
因為二維矩陣本質上是一幅「圖」,所以遍歷的過程中需要一個 visited
布林陣列防止走回頭路
- 目標是找到矩陣中 “島嶼的數量” ,即上下左右相連的
1
都被認為是連續島嶼。
演算法步驟:每次遇到陸地計數器加1,並把該陸地和該陸地上下左右的陸地變為海水,遍歷完之後計數器就是島嶼數量
class Solution {
// 主函式,計算島嶼數量
int numIslands(char[][] grid) {
int res = 0;
int m = grid.length, n = grid[0].length;
// 遍歷 grid
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
// 每發現一個島嶼,島嶼數量加一
res++;
// 然後使用 DFS 將島嶼淹了
dfs(grid, i, j);
}
}
}
return res;
}
// 從 (i, j) 開始,將與之相鄰的陸地都變成海水
void dfs(char[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
// 超出索引邊界
return;
}
if (grid[i][j] == '0') {
// 已經是海水了
return;
}
// 將 (i, j) 變成海水
grid[i][j] = '0';
// 淹沒上下左右的陸地
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
}
為什麼每次遇到島嶼,都要用 DFS 演算法把島嶼「淹了」呢?主要是為了省事,避免維護 visited
陣列。
因為 dfs
函式遍歷到值為 0
的位置會直接返回,所以只要把經過的位置都設定為 0
,就可以起到不走回頭路的作用。
733. 影像渲染
有一幅以 m x n
的二維整數陣列表示的圖畫 image
,其中 image[i][j]
表示該圖畫的畫素值大小。
你也被給予三個整數 sr
, sc
和 newColor
。你應該從畫素 image[sr][sc]
開始對影像進行 上色填充 。
為了完成 上色工作 ,從初始畫素開始,記錄初始座標的 上下左右四個方向上 畫素值與初始座標相同的相連畫素點,接著再記錄這四個方向上符合條件的畫素點與他們對應 四個方向上 畫素值與初始座標相同的相連畫素點,……,重複該過程。將所有有記錄的畫素點的顏色值改為 newColor
。
最後返回 經過上色渲染後的影像 。
示例 1:
輸入: image = [[1,1,1],[1,1,0],[1,0,1]],sr = 1, sc = 1, newColor = 2
輸出: [[2,2,2],[2,2,0],[2,0,1]]
解析: 在影像的正中間,(座標(sr,sc)=(1,1)),在路徑上所有符合條件的畫素點的顏色都被更改成2。
注意,右下角的畫素沒有更改為2,因為它不是在上下左右四個方向上與初始點相連的畫素點。
示例 2:
輸入: image = [[0,0,0],[0,0,0]], sr = 0, sc = 0, newColor = 2
輸出: [[2,2,2],[2,2,2]]
class Solution {
public int[][] floodFill(int[][] image, int sr, int sc, int color) {
dfs(image, sr, sc, color, image[sr][sc]);
return image;
}
public void dfs(int[][] image, int i, int j, int newColor, int oldColor){
int m = image.length, n = image[0].length;
// 超出邊界
if(i < 0 || j < 0 || i >= m || j >= n) {
return;
}
// 連續的初始值才染色
if(image[i][j] != oldColor || newColor == oldColor){
return;
}
// 染色
image[i][j] = newColor;
// 遍歷
dfs(image, i - 1, j, newColor, oldColor);
dfs(image, i + 1, j, newColor, oldColor);
dfs(image, i, j - 1, newColor, oldColor);
dfs(image, i, j + 1, newColor, oldColor);
}
}
1254. 統計封閉島嶼的數目
二維矩陣 grid
由 0
(土地)和 1
(水)組成。島是由最大的4個方向連通的 0
組成的群,封閉島是一個 完全
由1包圍(左、上、右、下)的島。
請返回 封閉島嶼 的數目。
示例 1:
輸入:grid = [[1,1,1,1,1,1,1,0],[1,0,0,0,0,1,1,0],[1,0,1,0,1,1,1,0],[1,0,0,0,0,1,0,1],[1,1,1,1,1,1,1,0]]
輸出:2
解釋:
灰色區域的島嶼是封閉島嶼,因為這座島嶼完全被水域包圍(即被 1 區域包圍)。
示例 2:
輸入:grid = [[0,0,1,0,0],[0,1,0,1,0],[0,1,1,1,0]]
輸出:1
示例 3:
輸入:grid = [[1,1,1,1,1,1,1],
[1,0,0,0,0,0,1],
[1,0,1,1,1,0,1],
[1,0,1,0,1,0,1],
[1,0,1,1,1,0,1],
[1,0,0,0,0,0,1],
[1,1,1,1,1,1,1]]
輸出:2
力扣第 200 題「 島嶼數量」有兩點不同:
1、用 0
表示陸地,用 1
表示海水。
2、讓你計算「封閉島嶼」的數目。所謂「封閉島嶼」就是上下左右全部被 1
包圍的 0
,也就是說靠邊的陸地不算作「封閉島嶼」。
那麼如何判斷「封閉島嶼」呢?其實很簡單,把第200題中那些靠邊的島嶼排除掉,剩下的不就是「封閉島嶼」了嗎?
class Solution {
public int closedIsland(int[][] grid) {
int m = grid.length, n = grid[0].length;
for(int j = 0; j < n; j++){
dfs(grid, 0, j); // 把靠上邊的島嶼淹掉
dfs(grid, m - 1, j); // 把靠下邊的島嶼淹掉
}
for(int i = 0; i < m; i++){
dfs(grid, i, 0); // 把靠上邊的島嶼淹掉
dfs(grid, i, n - 1); // 把靠下邊的島嶼淹掉
}
int res = 0;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid[i][j] == 0){
res++;
dfs(grid, i, j);
}
}
}
return res;
}
// 從 (i, j) 開始,將與之相鄰的陸地都變成海水
public void dfs(int[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
// 超出邊界
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
// 已經是海水了
if (grid[i][j] == 1) {
return;
}
// 將 (i, j) 變成海水
grid[i][j] = 1;
// 淹沒上下左右的陸地
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
}
695. 島嶼的最大面積
給你一個大小為 m x n
的二進位制矩陣 grid
。
島嶼 是由一些相鄰的 1
(代表土地) 構成的組合,這裡的「相鄰」要求兩個 1
必須在 水平或者豎直的四個方向上 相鄰。你可以假設 grid
的四個邊緣都被 0
(代表水)包圍著。
島嶼的面積是島上值為 1
的單元格的數目。
計算並返回 grid
中最大的島嶼面積。如果沒有島嶼,則返回面積為 0
。
示例 1:
img
輸入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
輸出:6
解釋:答案不應該是 11 ,因為島嶼只能包含水平或垂直這四個方向上的 1 。
示例 2:
輸入:grid = [[0,0,0,0,0,0,0,0]]
輸出:0
給 dfs
函式設定返回值,記錄每次淹沒的陸地的個數
class Solution {
public int maxAreaOfIsland(int[][] grid) {
int m = grid.length, n = grid[0].length;
int max = 0; // 記錄島嶼的最大面積
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid[i][j] == 1){
// 淹沒島嶼,並更新最大島嶼面積
max = Math.max(max, dfs(grid, i, j));
}
}
}
return max;
}
// 淹沒與(i,j) 相鄰的陸地,並返回淹沒的陸地面積
public int dfs(int[][] grid, int i, int j){
int m = grid.length, n = grid[0].length;
// 超出索引邊界
if(i < 0 || j < 0 || i >= m || j >= n){
return 0;
}
// 已經是海水
if(grid[i][j] == 0){
return 0;
}
// 將(i,j)變為海水
grid[i][j] = 0;
// 淹沒上下左右的陸地,並統計淹沒陸地數量
int sum = 1; // 預設sum為1,如果不是島嶼,則直接返回0,就可以避免預防錯誤的情況。
sum += dfs(grid, i - 1, j);
sum += dfs(grid, i + 1, j);
sum += dfs(grid, i, j - 1);
sum += dfs(grid, i, j + 1);
return sum;
}
}
1020. 飛地的數量
給你一個大小為 m x n
的二進位制矩陣 grid
,其中 0
表示一個海洋單元格、1
表示一個陸地單元格。
一次 移動 是指從一個陸地單元格走到另一個相鄰(上、下、左、右)的陸地單元格或跨過 grid
的邊界。
返回網格中 無法 在任意次數的移動中離開網格邊界的陸地單元格的數量。
示例 1:
輸入:grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]]
輸出:3
解釋:有三個 1 被 0 包圍。一個 1 沒有被包圍,因為它在邊界上。
示例 2:
img
輸入:grid = [[0,1,1,0],[0,0,1,0],[0,0,1,0],[0,0,0,0]]
輸出:0
解釋:所有 1 都在邊界上或可以到達邊界。
class Solution {
public int numEnclaves(int[][] grid) {
int m = grid.length, n = grid[0].length;
// 去除上下邊界陸地
for(int j = 0; j < n; j++){
dfs(grid, 0, j);
dfs(grid, m - 1, j);
}
// 去除左右邊界陸地
for(int i = 0; i < m; i++){
dfs(grid, i, 0);
dfs(grid, i, n - 1);
}
int res = 0;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid[i][j]== 1){
res++;
}
}
}
return res;
}
// 從 (i, j) 開始,將與之相鄰的陸地都變成海水
public void dfs(int[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
// 超出邊界
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
// 已經是海水了
if (grid[i][j] == 0) {
return;
}
// 將 (i, j) 變成海水
grid[i][j] = 0;
// 淹沒上下左右的陸地
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
}
class Solution {
public int numEnclaves(int[][] grid) {
int m = grid.length, n = grid[0].length;
// 去除上下邊界陸地
for(int j = 0; j < n; j++){
dfs(grid, 0, j);
dfs(grid, m - 1, j);
}
// 去除左右邊界陸地
for(int i = 0; i < m; i++){
dfs(grid, i, 0);
dfs(grid, i, n - 1);
}
int res = 0;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid[i][j]== 1){
res += dfs(grid, i, j);
}
}
}
return res;
}
// 淹沒與(i,j) 相鄰的陸地,並返回淹沒的陸地面積
public int dfs(int[][] grid, int i, int j){
int m = grid.length, n = grid[0].length;
// 超出邊界
if(i < 0 || j < 0 || i >= m || j >= n){
return 0;
}
// 已是海水
if(grid[i][j] == 0){
return 0;
}
// 將(i,j)變為海水
grid[i][j] = 0;
// 淹沒上下左右的陸地,並統計淹沒陸地數量
int sum = 1;
sum += dfs(grid, i + 1, j);
sum += dfs(grid, i, j + 1);
sum += dfs(grid, i - 1, j);
sum += dfs(grid, i, j - 1);
return sum;
}
}
噪音傳播-華為數存面試
終端產品在進行一項噪音監測實驗。若將空實驗室平面圖視作一個N*M的二維矩陣(左上角為[0,0])。工作人員在實驗室內設定了若干噪音源,並以[噪音源所在行,噪音源所在列,噪音值]的形式記錄於二維陣列noise中。
噪音沿相鄰8個方向傳播,在傳播過程中,噪音值(單位為分貝)逐級遞減1分貝,直至分貝削弱至1(即噪音源覆蓋區域邊緣噪音分貝為1);
若同一格被多個噪音源的噪音覆蓋,檢測結果不疊加,僅保留較大的噪音值(噪音源所在格也可能被其他噪音源的噪聲傳播所覆蓋)。
在所有噪音源開啟且持續傳播情況穩定後,請監測每格噪音分貝數並返回他們的總和。
注意:
除噪音源以外的所有格初始值為0分貝;不考慮牆面反射。
示例1:
輸入:n=5,m=6,noise=[[3,4,3],[1,1,4]]
輸出:63
class Main {
public static void main(String[] args) {
int[][] noise = {{3,4,3}, {1,1,4}};
System.out.println(spreadNoise(5, 6, noise));
}
public static int spreadNoise(int n, int m, int[][] noise) {
int[][] grid = new int[n][m];
for (int[] num: noise) {
dfs(grid, num[0], num[1], num[2]);
}
// 統計結果
int sum = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
sum += grid[i][j];
}
}
return sum;
}
// 噪聲填充
// 將(i,j)的 8 個方向填充資料
public static void dfs(int[][] grid, int row, int col, int val){
int m = grid.length, n = grid[0].length;
// 超出邊界
if (row < 0 || col < 0 || row >= m || col >= n) {
return;
}
// 迴圈結束(val為0 或 保留較大的噪音值)
if (val == 0 || grid[row][col] >= val) {
return;
}
// 記錄值
grid[row][col] = val;
// 沿8個方向擴散
dfs(grid, row + 1, col, val - 1);
dfs(grid, row - 1, col, val - 1);
dfs(grid, row, col + 1, val - 1);
dfs(grid, row, col - 1, val - 1);
dfs(grid, row + 1, col + 1, val - 1);
dfs(grid, row + 1, col - 1, val - 1);
dfs(grid, row - 1, col + 1, val - 1);
dfs(grid, row - 1, col - 1, val - 1);
}
}
79. 單詞搜尋
給定一個 m x n
二維字元網格 board
和一個字串單詞 word
。如果 word
存在於網格中,返回 true
;否則,返回 false
。
單詞必須按照字母順序,透過相鄰的單元格內的字母構成,其中“相鄰”單元格是那些水平相鄰或垂直相鄰的單元格。同一個單元格內的字母不允許被重複使用。
進階:你可以使用搜尋剪枝的技術來最佳化解決方案,使其在 board
更大的情況下可以更快解決問題?
示例 1:
輸入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
輸出:true
示例 2:
輸入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
輸出:true
示例 3:
輸入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
輸出:false
class Solution {
public boolean exist(char[][] board, String word) {
int m = board.length, n = board[0].length;
boolean[][] visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(dfs(board, i, j, word, 0, visited)){
return true;
}
}
}
return false;
}
// 從 (i, j) 開始向四周搜尋,試圖匹配 word[p..]
boolean dfs(char[][] board, int i, int j, String word, int p, boolean[][] visited) {
int m = board.length, n = board[0].length;
if(p == word.length()){
return true;
}
if (i < 0 || j < 0 || i >= m || j >= n) {
// 超出索引邊界
return false;
}
if(board[i][j] != word.charAt(p)){
return false;
}
if (visited[i][j]) {
// 已遍歷過 (i, j)
return false;
}
// 進入節點 (i, j)
visited[i][j] = true;
boolean t = dfs(board, i - 1, j, word, p + 1, visited); // 上
boolean b = dfs(board, i + 1, j, word, p + 1, visited); // 下
boolean l = dfs(board, i, j - 1, word, p + 1, visited); // 左
boolean r = dfs(board, i, j + 1, word, p + 1, visited); // 右
visited[i][j] = false;
return t || b || l || r;
}
}
994. 腐爛的橘子
在給定的 m x n
網格 grid
中,每個單元格可以有以下三個值之一:
- 值
0
代表空單元格; - 值
1
代表新鮮橘子; - 值
2
代表腐爛的橘子。
每分鐘,腐爛的橘子 周圍 4 個方向上相鄰 的新鮮橘子都會腐爛。
返回 直到單元格中沒有新鮮橘子為止所必須經過的最小分鐘數。如果不可能,返回 -1
。
示例 1:
輸入:grid = [[2,1,1],[1,1,0],[0,1,1]]
輸出:4
示例 2:
輸入:grid = [[2,1,1],[0,1,1],[1,0,1]]
輸出:-1
解釋:左下角的橘子(第 2 行, 第 0 列)永遠不會腐爛,因為腐爛只會發生在 4 個正向上。
示例 3:
輸入:grid = [[0,2]]
輸出:0
解釋:因為 0 分鐘時已經沒有新鮮橘子了,所以答案就是 0 。
對於二維網格 grid 來說,當遍歷到腐爛的?時,grid[i][j] == 2
定義 time 用來記錄傳染到該位置所需要的時間,初始化 time=2,最後要減 2,即每個格子在經過 DFS 遍歷後,放的不再是?的情況,而是從第一個腐爛?的位置感染到當前位置的?所需要經過的分鐘數,此時需要以該位置為起點找與當前遍歷的腐爛的?相鄰的 4 個位置中是否有新鮮的?,若有新鮮的?,則直接將其腐爛並且所需要的時間 time+1,即得到從起點腐爛到該位置所需要的分鐘數。
class Solution {
public int orangesRotting(int[][] grid) {
int m = grid.length, n = grid[0].length;
// 網格為null或者長度為0的時候返回0;
if (grid == null || m == 0) {
return 0;
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 2) {
// 每次更新橘子腐爛時間是直接覆蓋 grid 而 grid 中已經有2了,所以從2開始
dfs(i, j, grid, 2); // 開始傳染
}
}
}
// 經過dfs後,grid陣列中記錄了每個橘子被傳染時的時間,找出最大的時間即為腐爛全部橘子所用的時間。
int maxTime = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
return -1; // 若有新鮮橘子未被傳染到,直接返回-1
} else {
maxTime = Math.max(maxTime, grid[i][j]);
}
}
}
return maxTime == 0 ? 0 : maxTime - 2;
}
// time用來記錄傳染時間(當然最後要減2)
private void dfs(int i, int j, int[][] grid, int time) {
int m = grid.length, n = grid[0].length;
// 超出範圍
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
// 只有新鮮橘子或者其他橘子時間time>現在time的橘子,才繼續進行傳播。
if (grid[i][j] != 1 && grid[i][j] < time) {
return;
}
// 將傳染時間存到grid[i][j]中
grid[i][j] = time;
time++;
dfs(i - 1, j, grid, time);
dfs(i + 1, j, grid, time);
dfs(i, j - 1, grid, time);
dfs(i, j + 1, grid, time);
}
}
1905. 統計子島嶼
給你兩個 m x n
的二進位制矩陣 grid1
和 grid2
,它們只包含 0
(表示水域)和 1
(表示陸地)。一個 島嶼 是由 四個方向 (水平或者豎直)上相鄰的 1
組成的區域。任何矩陣以外的區域都視為水域。
如果 grid2
的一個島嶼,被 grid1
的一個島嶼 完全 包含,也就是說 grid2
中該島嶼的每一個格子都被 grid1
中同一個島嶼完全包含,那麼我們稱 grid2
中的這個島嶼為 子島嶼 。
請你返回 grid2
中 子島嶼 的 數目 。
示例 1:
輸入:grid1 = [[1,1,1,0,0],[0,1,1,1,1],[0,0,0,0,0],[1,0,0,0,0],[1,1,0,1,1]], grid2 = [[1,1,1,0,0],[0,0,1,1,1],[0,1,0,0,0],[1,0,1,1,0],[0,1,0,1,0]]
輸出:3
解釋:如上圖所示,左邊為 grid1 ,右邊為 grid2 。
grid2 中標紅的 1 區域是子島嶼,總共有 3 個子島嶼。
示例 2:
輸入:grid1 = [[1,0,1,0,1],[1,1,1,1,1],[0,0,0,0,0],[1,1,1,1,1],[1,0,1,0,1]], grid2 = [[0,0,0,0,0],[1,1,1,1,1],[0,1,0,1,0],[0,1,0,1,0],[1,0,0,0,1]]
輸出:2
解釋:如上圖所示,左邊為 grid1 ,右邊為 grid2 。
grid2 中標紅的 1 區域是子島嶼,總共有 2 個子島嶼。
這道題的關鍵在於,如何快速判斷子島嶼?
什麼情況下 grid2
中的一個島嶼 B
是 grid1
中的一個島嶼 A
的子島?
當島嶼 B
中所有陸地在島嶼 A
中也是陸地的時候,島嶼 B
是島嶼 A
的子島。
反過來說,如果島嶼 B
中存在一片陸地,在島嶼 A
的對應位置是海水,那麼島嶼 B
就不是島嶼 A
的子島。
那麼,我們只要遍歷 grid2
中的所有島嶼,把那些不可能是子島的島嶼排除掉,剩下的就是子島。
這道題的思路和「統計封閉島嶼的數目」的思路有些類似,只不過後者排除那些靠邊的島嶼,前者排除那些不可能是子島的島嶼。
class Solution {
public int countSubIslands(int[][] grid1, int[][] grid2) {
int m = grid1.length, n = grid1[0].length;
// 將 grid2 中肯定不是子島的,淹掉
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid1[i][j] == 0 && grid2[i][j] == 1){
dfs(grid2, i, j);
}
}
}
// 現在 grid2 中剩下的島嶼都是子島,計算島嶼數量
int res = 0;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid2[i][j] == 1){
res++;
dfs(grid2, i, j);
}
}
}
return res;
}
// 從 (i, j) 開始,將與之相鄰的陸地都變成海水
public void dfs(int[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
// 超出邊界
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
// 已經是海水了
if (grid[i][j] == 0) {
return;
}
// 將 (i, j) 變成海水
grid[i][j] = 0;
// 淹沒上下左右的陸地
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
}
劍指 Offer 13. 機器人的運動範圍
地上有一個m行n列的方格,從座標 [0,0]
到座標 [m-1,n-1]
。一個機器人從座標 [0, 0]
的格子開始移動,它每次可以向左、右、上、下移動一格(不能移動到方格外),也不能進入行座標和列座標的數位之和大於k的格子。例如,當k為18時,機器人能夠進入方格 [35, 37] ,因為3+5+3+7=18。但它不能進入方格 [35, 38],因為3+5+3+8=19。請問該機器人能夠到達多少個格子?
示例 1:
輸入:m = 2, n = 3, k = 1
輸出:3
示例 2:
輸入:m = 3, n = 1, k = 0
輸出:1
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];
dfs(m, n, k, 0, 0, visited);
return res;
}
int res = 0; // 記錄合法座標數
public void dfs(int m, int n, int k, int i, int j, boolean[][] visited){
// 超出索引邊界
if(i < 0 || j < 0 || i >= m || j >= n){
return;
}
// 座標和超出 k 的限制
if(i / 10 + i % 10 + j / 10 + j % 10 > k){
return;
}
// 之前已經訪問過當前座標
if(visited[i][j]){
return;
}
res++; // 走到一個合法座標
visited[i][j] = true;
dfs(m, n, k, i + 1, j, visited);
dfs(m, n, k, i, j + 1, visited);
dfs(m, n, k, i - 1, j, visited);
dfs(m, n, k, i, j - 1, visited);
}
}
小於n的最大整數
給一個陣列,以及數字n,求arr中的數字能組成小於n的最大整數
例如A={1, 2, 4, 9},n=2533,返回2499
首先將arr陣列排序,之後使用深搜+貪心的思想,從第一位開始,儘量使用與對應位置相等的數字。如果有任意一位沒有使用相等的數字,那在後面的所有位中都直接使用最大的數字即可。
public class ZJtest {
static int ans; // 用於儲存找到的符合條件的數字
public static void main(String[] args) {
int[] arr = {1,2,9,4};
System.out.println(find(2533, arr));
}
public static boolean dfs(int index, boolean pass, int temp, int[] arr, int n) {
String s = String.valueOf(n);
int len = s.length();
if (index == len) { // 如果已經遍歷完字串,則找到了符合條件的數字
ans = temp;
return true; // 返回true表示已經找到符合條件的數字
}
if (pass) { // 如果已經找到了比當前數字小的數字,則直接使用陣列中的最大數字
return dfs(index + 1, true, temp * 10 + arr[arr.length - 1], arr, n);
} else {
int val = s.charAt(index) - '0';
for (int i = arr.length - 1; i >= 0; i--) {
if (val == arr[i]) { // 如果當前數字在陣列中存在,則使用該數字繼續遞迴查詢
if (dfs(index + 1, false, temp * 10 + arr[i], arr, n)) {
return true;
}
} else if (val > arr[i]) { // 如果當前數字大於陣列中的數字,則使用該數字繼續遞迴查詢
if (dfs(index + 1, true, temp * 10 + arr[i], arr, n)) {
return true;
}
}
}
if (index != 0) { // 如果當前位置不是第一位,則返回false表示沒有找到符合條件的數字
return false;
}
// 如果當前位置是第一位,則使用陣列中的最小數字繼續遞迴查詢
return dfs(index + 1, true, temp, arr, n);
}
}
public static int find(int N, int[] arr) {
Arrays.sort(arr);
dfs(0, false, 0, arr, N);
return ans;
}
}
dfs方法中有四個形參:index,pass,temp和arr,表示當前遍歷到的字元索引位置,上一位是否能匹配當前位(即小於等於或大於當前位),已匹配數值和陣列。函式返回值為布林型。
在函式體內部,透過if else語句判斷當前位數是否與陣列中的數字相等或者大於目標數字,根據判斷結果分別進行遞迴操作,直至搜尋完成。如果搜尋成功,就將當前匹配的數值更新到ans變數中。
- 當pass為true時,表示上一位匹配成功,當前位可以是陣列中任意數字。故直接在陣列的尾部數字上進行遞迴搜尋。
- 當pass為false時,表示上一位並沒有匹配成功,此時需要從大到小列舉陣列中可能的數字。
- 如果當前位與陣列中某一位數字相等,則可以直接與之匹配,並繼續向後遞迴。
- 如果當前位大於陣列中某一位數字,則說明需要跳過這個數字,將pass置為true,並再次遞迴搜尋。
- 如果整個字串都遍歷完畢但還沒有匹配出結果,則返回false。
最後,在dfs方法的末尾判斷是否為第一個字元。如果是,說明第一個字元可以取0,即有前導零的情況,此時將pass置為true,並再次遞迴搜尋。
整體來說,這段程式碼的主要功能就是對固定數字按照規定的陣列進行匹配查詢,返回能夠匹配的最大值。
841. 鑰匙和房間
有 n
個房間,房間按從 0
到 n - 1
編號。最初,除 0
號房間外的其餘所有房間都被鎖住。你的目標是進入所有的房間。然而,你不能在沒有獲得鑰匙的時候進入鎖住的房間。
當你進入一個房間,你可能會在裡面找到一套不同的鑰匙,每把鑰匙上都有對應的房間號,即表示鑰匙可以開啟的房間。你可以拿上所有鑰匙去解鎖其他房間。
給你一個陣列 rooms
其中 rooms[i]
是你進入 i
號房間可以獲得的鑰匙集合。如果能進入 所有 房間返回 true
,否則返回 false
。
示例 1:
輸入:rooms = [[1],[2],[3],[]]
輸出:true
解釋:
我們從 0 號房間開始,拿到鑰匙 1。
之後我們去 1 號房間,拿到鑰匙 2。
然後我們去 2 號房間,拿到鑰匙 3。
最後我們去了 3 號房間。
由於我們能夠進入每個房間,我們返回 true。
示例 2:
輸入:rooms = [[1,3],[3,0,1],[2],[0]]
輸出:false
解釋:我們不能進入 2 號房間。
當 x 號房間中有 y 號房間的鑰匙時,我們就可以從 x 號房間去往 y 號房間。如果我們將這 n 個房間看成有向圖中的 n 個節點,那麼上述關係就可以看作是圖中的 x 號點到 y 號點的一條有向邊。問題就變成了給定一張有向圖,詢問從 0 號節點出發是否能夠到達所有的節點。
DFS
我們可以使用深度優先搜尋的方式遍歷整張圖,統計可以到達的節點個數,並利用陣列 visited 標記當前節點是否訪問過,以防止重複訪問。
class Solution {
boolean[] visited; // 房間是否訪問過
int num; // 已訪問房間數
public boolean canVisitAllRooms(List<List<Integer>> rooms) {
int n = rooms.size();
num = 0;
visited = new boolean[n];
dfs(rooms, 0); // 從 0 號房開始訪問
return num == n;
}
public void dfs(List<List<Integer>> rooms, int x) {
visited[x] = true;
num++;
for (int i : rooms.get(x)) {
if (!visited[i]) { // 若該房間未被訪問過
dfs(rooms, i);
}
}
}
}
BFS
我們也可以使用廣度優先搜尋的方式遍歷整張圖,統計可以到達的節點個數,並利用陣列 visited 標記當前節點是否訪問過,以防止重複訪問。
class Solution {
public boolean canVisitAllRooms(List<List<Integer>> rooms) {
int n = rooms.size();
int num = 0;
boolean[] visited = new boolean[n];
Queue<Integer> queue = new LinkedList<Integer>();
queue.offer(0);
visited[0] = true;
while (!queue.isEmpty()) {
int x = queue.poll();
num++;
for (int i : rooms.get(x)) {
if (!visited[i]) {
visited[i] = true;
queue.offer(i);
}
}
}
return num == n;
}
}
547. 省份數量
有 n
個城市,其中一些彼此相連,另一些沒有相連。如果城市 a
與城市 b
直接相連,且城市 b
與城市 c
直接相連,那麼城市 a
與城市 c
間接相連。
省份 是一組直接或間接相連的城市,組內不含其他沒有相連的城市。
給你一個 n x n
的矩陣 isConnected
,其中 isConnected[i][j] = 1
表示第 i
個城市和第 j
個城市直接相連,而 isConnected[i][j] = 0
表示二者不直接相連。
返回矩陣中 省份 的數量。
示例 1:
img
輸入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
輸出:2
示例 2:
img
輸入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
輸出:3
可以把 n 個城市和它們之間的相連關係看成圖,城市是圖中的節點,相連關係是圖中的邊,給定的矩陣 isConnected即為圖的鄰接矩陣,省份即為圖中的連通分量。
計算省份總數,等價於計算圖中的連通分量數,可以透過深度優先搜尋或廣度優先搜尋實現,也可以透過並查集實現。
DFS
遍歷所有城市,對於每個城市,如果該城市尚未被訪問過,則從該城市開始深度優先搜尋,透過矩陣 isConnected 得到與該城市直接相連的城市有哪些,這些城市和該城市屬於同一個連通分量,然後對這些城市繼續深度優先搜尋,直到同一個連通分量的所有城市都被訪問到,即可得到一個省份。遍歷完全部城市以後,即可得到連通分量的總數,即省份的總數。
class Solution {
public int findCircleNum(int[][] isConnected) {
int cities = isConnected.length;
boolean[] visited = new boolean[cities];
int provinces = 0;
for (int i = 0; i < cities; i++) {
if (!visited[i]) {
dfs(isConnected, visited, cities, i);
provinces++;
}
}
return provinces;
}
public void dfs(int[][] isConnected, boolean[] visited, int cities, int i) {
for (int j = 0; j < cities; j++) {
if (isConnected[i][j] == 1 && !visited[j]) {
visited[j] = true;
dfs(isConnected, visited, cities, j);
}
}
}
}
BFS
對於每個城市,如果該城市尚未被訪問過,則從該城市開始廣度優先搜尋,直到同一個連通分量中的所有城市都被訪問到,即可得到一個省份。
class Solution {
public int findCircleNum(int[][] isConnected) {
int cities = isConnected.length;
boolean[] visited = new boolean[cities];
int provinces = 0;
Queue<Integer> queue = new LinkedList<Integer>();
for (int i = 0; i < cities; i++) {
if (!visited[i]) {
queue.offer(i);
while (!queue.isEmpty()) {
int k = queue.poll();
visited[k] = true;
for (int j = 0; j < cities; j++) {
if (isConnected[k][j] == 1 && !visited[j]) {
queue.offer(j);
}
}
}
provinces++;
}
}
return provinces;
}
}
1466. 重新規劃路線
n
座城市,從 0
到 n-1
編號,其間共有 n-1
條路線。因此,要想在兩座不同城市之間旅行只有唯一一條路線可供選擇(路線網形成一顆樹)。去年,交通運輸部決定重新規劃路線,以改變交通擁堵的狀況。
路線用 connections
表示,其中 connections[i] = [a, b]
表示從城市 a
到 b
的一條有向路線。
今年,城市 0 將會舉辦一場大型比賽,很多遊客都想前往城市 0 。
請你幫助重新規劃路線方向,使每個城市都可以訪問城市 0 。返回需要變更方向的最小路線數。
題目資料 保證 每個城市在重新規劃路線方向後都能到達城市 0 。
示例 1:
輸入:n = 6, connections = [[0,1],[1,3],[2,3],[4,0],[4,5]]
輸出:3
解釋:更改以紅色顯示的路線的方向,使每個城市都可以到達城市 0 。
示例 2:
輸入:n = 5, connections = [[1,0],[1,2],[3,2],[3,4]]
輸出:2
解釋:更改以紅色顯示的路線的方向,使每個城市都可以到達城市 0 。
示例 3:
輸入:n = 3, connections = [[1,0],[2,0]]
輸出:0
我們可以將這個圖看做是無向圖,從0城市開始往外走,走到哪個城市後看下與原有道路的方向是否相反,如果相反代表不用改變路線的方向,如果不相反則需要改變路線的方向(因為我們是從0開始走,走的方向是對外的,而其他城市想來0城市,肯定是往裡走,路線方向應該與我們往外走的方式相反),然後統計需要改變方向的路線數即可。
// 建立無向圖,使用鄰接表存圖,並使用一個額外標記記錄邊的方向
class Solution {
Map<Integer, List<int[]>> map = new HashMap<>();
boolean[] visited;
// direction記錄該邊和原始邊的方向, 1 表示同向,0 表示反向
void add(int i, int j, int direction) {
if (!map.containsKey(i)) {
map.put(i, new ArrayList<>());
}
map.get(i).add(new int[]{j, direction});
}
public int minReorder(int n, int[][] connections) {
// 建立無向圖
for (int[] connection : connections) {
int i = connection[0], j = connection[1];
add(i, j, 1);
add(j, i, 0);
}
visited = new boolean[n];
// 若某個子節點是透過反向邊到達的,則該邊不用變更方向,
// 若某個子節點是透過正向邊到達的,則該邊需要變更方向
// 所以樹中同向邊的數量就是需要變更方向的路線數,則對做根節點 0 進行dfs遍歷可得結果
return dfs(0);
}
// 返回當前節點中對應子樹中同向邊的數量
int dfs(int node) {
visited[node] = true;
List<int[]> edges = map.get(node);
int count = 0;
for (int[] edge : edges) {
int child = edge[0], direction = edge[1];
if (!visited[child]) {
count += direction + dfs(child);
}
}
return count;
}
}
399. 除法求值
給你一個變數對陣列 equations
和一個實數值陣列 values
作為已知條件,其中 equations[i] = [Ai, Bi]
和 values[i]
共同表示等式 Ai / Bi = values[i]
。每個 Ai
或 Bi
是一個表示單個變數的字串。
另有一些以陣列 queries
表示的問題,其中 queries[j] = [Cj, Dj]
表示第 j
個問題,請你根據已知條件找出 Cj / Dj = ?
的結果作為答案。
返回 所有問題的答案 。如果存在某個無法確定的答案,則用 -1.0
替代這個答案。如果問題中出現了給定的已知條件中沒有出現的字串,也需要用 -1.0
替代這個答案。
示例 1:
輸入:equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]
輸出:[6.00000,0.50000,-1.00000,1.00000,-1.00000]
解釋:
條件:a / b = 2.0, b / c = 3.0
問題:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?
結果:[6.0, 0.5, -1.0, 1.0, -1.0 ]
示例 2:
輸入:equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]]
輸出:[3.75000,0.40000,5.00000,0.20000]
示例 3:
輸入:equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"],["a","c"],["x","y"]]
輸出:[0.50000,2.00000,-1.00000,-1.00000]
我們可以將整個問題建模成一張圖:給定圖中的一些點(變數),以及某些邊的權值(兩個變數的比值),試對任意兩點(兩個變數)求出其路徑長(兩個變數的比值)。
因此,我們首先需要遍歷 equations\textit{equations}equations 陣列,找出其中所有不同的字串,並透過雜湊表將每個不同的字串對映成整數。
在構建完圖之後,對於任何一個查詢,就可以從起點出發,透過廣度優先搜尋的方式,不斷更新起點與當前點之間的路徑長度,直到搜尋到終點為止。
影片指路連結:https://www.bilibili.com/video/BV1XU4y1s7Lk
class Solution {
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
//初始化Graph(以HashMap形式)
Map<String, List<Cell>> graph = new HashMap<>();
//對於每個Equation和其結果答案,將其加入Graph中
// 對於每個點,儲存其直接連線到的所有點及對應的權值
for(int i = 0; i < values.length; i++) {
String s1 = equations.get(i).get(0), s2 = equations.get(i).get(1);
graph.computeIfAbsent(s1, k -> new ArrayList<>()).add(new Cell(s2, values[i]));
graph.computeIfAbsent(s2, k -> new ArrayList<>()).add(new Cell(s1, 1.0 / values[i]));
}
//建立答案result陣列以及訪問過的HashSet: visited
double[] res = new double[queries.size()];
//首先將答案中所有答案值置為-1.0,出現(x / x)情況可以直接不用修改
Arrays.fill(res, -1.0);
//對於每個query中的值呼叫dfs函式
for(int i = 0; i < queries.size(); i++) {
dfs(queries.get(i).get(0), queries.get(i).get(1), 1.0, graph, res, i, new HashSet<>());
}
return res;
}
//src: 當前位置; dst: 答案節點; cur: 當前計算值; graph: 之前建的圖; res: 答案array; index: 當前遍歷到第幾個query; visited: 查重Set
private void dfs(String src, String dst, double cur, Map<String, List<Cell>> graph, double[] res, int index, Set<String> visited) {
//base case: 在visited中加入當前位置資訊;如果加不了代表已經訪問過,直接返回
if(!visited.add(src)) {
return;
}
//如果當前位置src = 答案節點dst,並且此節點在graph中(避免x/x的情況),用當前計算值cur來填充答案res[index]
if(src.equals(dst) && graph.containsKey(src)) {
res[index] = cur;
return;
}
//對於鄰居節點,呼叫dfs函式
for(Cell nei : graph.getOrDefault(src, new ArrayList<>())) {
dfs(nei.str, dst, cur * nei.div, graph, res, index, visited);
}
}
}
class Cell {
String str;
double div;
Cell(String str, double div) {
this.str = str;
this.div = div;
}
}
②BFS
演算法框架
我們先舉例一下 BFS 出現的常見場景好吧,問題的本質就是讓你在一幅「圖」中找到從起點 start
到終點 target
的最近距離,這個例子聽起來很枯燥,但是 BFS 演算法問題其實都是在幹這個事兒。
這個廣義的描述可以有各種變體,比如走迷宮,有的格子是圍牆不能走,從起點到終點的最短距離是多少?如果這個迷宮帶「傳送門」可以瞬間傳送呢?
再比如說兩個單詞,要求你透過某些替換,把其中一個變成另一個,每次只能替換一個字元,最少要替換幾次?
再比如說連連看遊戲,兩個方塊消除的條件不僅僅是圖案相同,還得保證兩個方塊之間的最短連線不能多於兩個拐點。你玩連連看,點選兩個座標,遊戲是如何判斷它倆的最短連線有幾個拐點的?
// 計算從起點 start 到終點 target 的最近距離
int BFS(Node start, Node target) {
Queue<Node> q; // 核心資料結構
Set<Node> visited; // 避免走回頭路
q.offer(start); // 將起點加入佇列
visited.add(start);
while (!q.isEmpty()) {
int sz = q.size();
/* 將當前佇列中的所有節點向四周擴散 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
/* 劃重點:這裡判斷是否到達終點 */
if (cur == target)
return step;
/* 將 cur 的相鄰節點加入佇列 */
for (Node x : cur.adj()) {
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
}
}
// 如果走到這裡,說明在圖中沒有找到目標節點
}
cur.adj()
泛指 cur
相鄰的節點,比如說二維陣列中,cur
上下左右四面的位置就是相鄰節點;visited
的主要作用是防止走回頭路,大部分時候都是必須的,但是像一般的二叉樹結構,沒有子節點到父節點的指標,不會走回頭路就不需要 visited
。
1926. 迷宮中離入口最近的出口
給你一個 m x n
的迷宮矩陣 maze
(下標從 0 開始),矩陣中有空格子(用 '.'
表示)和牆(用 '+'
表示)。同時給你迷宮的入口 entrance
,用 entrance = [entrancerow, entrancecol]
表示你一開始所在格子的行和列。
每一步操作,你可以往 上,下,左 或者 右 移動一個格子。你不能進入牆所在的格子,你也不能離開迷宮。你的目標是找到離 entrance
最近 的出口。出口 的含義是 maze
邊界 上的 空格子。entrance
格子 不算 出口。
請你返回從 entrance
到最近出口的最短路徑的 步數 ,如果不存在這樣的路徑,請你返回 -1
。
示例 1:
輸入:maze = [["+","+",".","+"],[".",".",".","+"],["+","+","+","."]], entrance = [1,2]
輸出:1
解釋:總共有 3 個出口,分別位於 (1,0),(0,2) 和 (2,3) 。
一開始,你在入口格子 (1,2) 處。
- 你可以往左移動 2 步到達 (1,0) 。
- 你可以往上移動 1 步到達 (0,2) 。
從入口處沒法到達 (2,3) 。
所以,最近的出口是 (0,2) ,距離為 1 步。
示例 2:
輸入:maze = [["+","+","+"],[".",".","."],["+","+","+"]], entrance = [1,0]
輸出:2
解釋:迷宮中只有 1 個出口,在 (1,2) 處。
(1,0) 不算出口,因為它是入口格子。
初始時,你在入口與格子 (1,0) 處。
- 你可以往右移動 2 步到達 (1,2) 處。
所以,最近的出口為 (1,2) ,距離為 2 步。
示例 3:
img
輸入:maze = [[".","+"]], entrance = [0,0]
輸出:-1
解釋:這個迷宮中沒有出口。
標準的 BFS 演算法,只要套用 BFS 演算法模板框架就可以了
class Solution {
public int nearestExit(char[][] maze, int[] entrance) {
int m = maze.length;
int n = maze[0].length;
int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};// 右左下上
// BFS 演算法的佇列和 visited 陣列
Queue<int[]> queue = new LinkedList<>();
boolean[][] visited = new boolean[m][n];
queue.offer(entrance);
visited[entrance[0]][entrance[1]] = true;
// 啟動 BFS 演算法從 entrance 開始像四周擴散
int step = 0;
while (!queue.isEmpty()) {
int sz = queue.size();
step++;
// 擴散當前佇列中的所有節點
for (int i = 0; i < sz; i++) {
int[] cur = queue.poll();
// 每個節點都會嘗試向上下左右四個方向擴充套件一步
for (int[] dir : dirs) {
int x = cur[0] + dir[0];
int y = cur[1] + dir[1];
if (x < 0 || x >= m || y < 0 || y >= n
|| visited[x][y] || maze[x][y] == '+') {
continue;
}
if (x == 0 || x == m - 1 || y == 0 || y == n - 1) {
// 走到邊界(出口)
return step;
}
visited[x][y] = true;
queue.offer(new int[]{x, y});
}
}
}
return -1;
}
}
111. 二叉樹的最小深度
給定一個二叉樹,找出其最小深度。
最小深度是從根節點到最近葉子節點的最短路徑上的節點數量。
說明:葉子節點是指沒有子節點的節點。
示例 1:
輸入:root = [3,9,20,null,null,15,7]
輸出:2
示例 2:
輸入:root = [2,null,3,null,4,null,5,null,6]
輸出:5
// 時間複雜度:O(N)
// 空間複雜度:O(N)
class Solution {
public int minDepth(TreeNode root) {
if(root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
int depth = 1;
queue.offer(root);
while(!queue.isEmpty()){
int n = queue.size();
depth++;
while(n > 0){
TreeNode node = queue.poll();
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
if(node.left == null && node.right == null){
return depth;
}
n--;
}
}
return depth;
}
}