0. 前言
在这篇文章中,我们来探讨一下 dfs(deep first search,dfs)。与之相对应的,通常还有广度优先遍历(bread first search,bfs)。通常,它们都被用于树或者图的遍历。对于树的遍历,在之前的文章中,我有提到。在这篇文章中,我们重点关注图的dfs遍历。
图的表示方法一般有以下几种:
- 通过
0, 1
来表示一个二维空间的网格的两种属性 - 通过 节点 + 邻接矩阵 的方式来表示
- 通过 节点 + 有序数对构成的数组 的方式来表示
1. 点状图
这种方式一般会给一个如图所示的 m x n 的二维数组,数组的值为 0 或 1。
对于这一类问题的遍历,需要准备一个方向向量dx = [0, 1, 0, -1]; dy = [1, 0, -1, 0]
,表示要走的四个方位。
如图所示,按照某个特定的节点的角度来看,一直往一个方向走,走到边界;然后,再朝着另一个方向走,走到边界;遍历完四个方向,即完成了当前节点的相关的遍历。
深度优先遍历的代码如下:
const dx = [0, 1, 0, -1];
const dy = [1, 0, -1, 0];
const dfs = (grid, r, c) => {
// 边界条件的判断,如果越过 grid 的边界,则直接返回
if(!inArea(grid, r, c)) {
return;
}
// 遍历过的格子,直接返回,避免重复遍历
if(grid[r][c] === 2) {
return;
}
// 表示当前格子已经被遍历过
grid[r][c] = 2;
for(let i = 0; i < 4; i++) {
dfs(grid, r + dx[i], c + dy[i]);
}
}
leetcode中的几道岛与相关的问题都是用这样的方法来解答的:
- 200. 岛屿数量:
题目的要求是计算网格中的岛屿数量,也就是说寻找 1 的连通域。思路是这样的:对每个值为 1 格点进行dfs,直到到达边界。如果遍历完,就说明找到了一个岛屿,岛屿数量加一。代码如下:
/**
* @param {character[][]} grid
* @return {number}
*/
var numIslands = function(grid) {
const m = grid.length;
const n = grid[0].length;
let cnt = 0;
const dfs = (r, c) => {
if(r < 0 || r >= m || c < 0 || c >= n || grid[r][c] === '0') {
return;
}
grid[r][c] = '0';
dfs(r - 1, c);
dfs(r + 1, c);
dfs(r, c - 1);
dfs(r, c + 1);
}
for(let i = 0; i < m; i++) {
for(let j = 0; j < n; j++) {
if(grid[i][j] === '1') {
dfs(i, j);
cnt++;
}
}
}
return cnt;
};
-
- 岛屿的最大面积
这一个题目要求我们在遍历的过程中,统计经过的 1 的数量。只需要在 dfs 的过程中,对满足条件的进行统计,并且将结果作为返回值即可。代码如下:
/**
* @param {number[][]} grid
* @return {number}
*/
var maxAreaOfIsland = function(grid) {
const m = grid.length;
const n = grid[0].length;
let minArea = 0;
const dx = [0, 0, 1, -1];
const dy = [1, -1, 0, 0];
const dfs = (x, y) => {
if(x < 0 || x === m || y < 0 || y === n || grid[x][y] !== 1) {
return 0;
}
grid[x][y] = 0;
let res = 1;
for(let i = 0; i < 4; i++) {
const tx = x + dx[i];
const ty = y + dy[i];
res += dfs(tx, ty);
}
return res;
}
for(let i = 0; i < m; i++) {
for(let j = 0; j < n; j++) {
if(grid[i][j] === 1) {
const area = dfs(i, j);
minArea = Math.max(minArea, area);
}
}
}
return minArea;
};
2. 邻接矩阵表示的图
节点+邻接矩阵 是图最基本的表示方法。假设某个图里有 n 个节点,那么,表示该图的邻接矩阵就是 n x n 的。若该图是有向图,则邻接矩阵的 i 行 j 列的值与 j 行 i 列的值相等,并且 i == j 时,值为 1.
解决这类问题,需要准备一个visited
数组,表示第 i 个节点是否被便利过。然后,在以某个节点为中心的 dfs 函数中去遍历所有的节点,如果邻接矩阵表示这两个节点是相连的,则用 dfs 迭代这个新的节点。具体的代码如下:
const dfs = (n, connected, visited, i) => {
for(let j = 0; j < n; j++) {
if(connected[i][j] === 1 && visited[j] === 0) {
visited[j] = 1;
dfs(n, connected, visited, j);
}
}
}
leetcode 中的题目:
- 547.省份数量
这道题的本质就是寻找图中连通域的数量,可以用dfs来解决。我们直接套用dfs的模板寻找连通域,找到之后,cnt++即可。代码如下:
/**
* @param {number[][]} isConnected
* @return {number}
*/
var findCircleNum = function(isConnected) {
const n = isConnected.length;
const visited = new Set();
let ans = 0;
const dfs = (cities, isConnected, visited,i) => {
for(let j = 0; j < cities; j++) {
if(isConnected[i][j] === 1 && !visited.has(j)) {
visited.add(j);
dfs(cities, isConnected, visited, j);
}
}
}
for(let i = 0; i < n; i++) {
if(!visited.has(i)) {
dfs(n, isConnected, visited, i);
ans++;
}
}
return ans;
};
3. 数组表示邻接关系的有向图
这种类型的图只需要在用邻接数组表示的情况解决方法之前加一个map
来表示节点之间的关系就好。具体的解法,我们看例题:
- 207.课程表
废话不多说,直接贴代码:
/**
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {boolean}
*/
var canFinish = function(numCourses, prerequisites) {
const edge = new Map();
const visited = new Array(numCourses).fill(0);
let valid = true;
const dfs = (u) => {
visited[u] = 1;
const pre = edge.has(u) ? edge.get(u) : null;
if(pre !== null) {
for(let v of pre) {
if(visited[v] === 0) {
dfs(v);
if(!valid) {
return;
}
} else if(visited[v] === 1){
valid = false;
return;
}
}
}
visited[u] = 2;
}
for(let pre of prerequisites) {
let arr = edge.has(pre[0]) ? edge.get(pre[0]) : [];
arr.push(pre[1]);
edge.set(pre[0], arr);
}
for(let i = 0; i < numCourses && valid; i++) {
if(visited[i] === 0) {
dfs(i);
}
}
return valid;
};