leetcode刷题之DFS遍历

997 阅读2分钟

0. 前言

在这篇文章中,我们来探讨一下 dfs(deep first search,dfs)。与之相对应的,通常还有广度优先遍历(bread first search,bfs)。通常,它们都被用于树或者图的遍历。对于树的遍历,在之前的文章中,我有提到。在这篇文章中,我们重点关注图的dfs遍历。

图的表示方法一般有以下几种:

  • 通过 0, 1 来表示一个二维空间的网格的两种属性
  • 通过 节点 + 邻接矩阵 的方式来表示
  • 通过 节点 + 有序数对构成的数组 的方式来表示

1. 点状图

这种方式一般会给一个如图所示的 m x n 的二维数组,数组的值为 0 或 1。

image.png

对于这一类问题的遍历,需要准备一个方向向量dx = [0, 1, 0, -1]; dy = [1, 0, -1, 0],表示要走的四个方位。

如图所示,按照某个特定的节点的角度来看,一直往一个方向走,走到边界;然后,再朝着另一个方向走,走到边界;遍历完四个方向,即完成了当前节点的相关的遍历。

image.png

深度优先遍历的代码如下:

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. 岛屿数量:

image.png

题目的要求是计算网格中的岛屿数量,也就是说寻找 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. 岛屿的最大面积

image.png

这一个题目要求我们在遍历的过程中,统计经过的 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.省份数量

image.png

这道题的本质就是寻找图中连通域的数量,可以用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.课程表

image.png

废话不多说,直接贴代码:

/**
 * @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;
};