算法学习-深度优先遍历(持续更新中)


深度优先遍历可以说是力扣以及大厂面试中最常见的题型了,因为其能够和很多类型的题目联合起来解题。笔者为了能够掌握这种搜索技巧,因此对自己的刷题路径和理解进行了汇总整理。

本文参考:

深度优先搜索

深度优先遍历理解

深度优先遍历中,大量的使用了“递归”这种思想,我之前的一篇文章中也谈到了一些自己的理解,希望能带给你一些帮助。引路->算法学习-递归思想

「一条路走到底,不撞南墙不回头」是对「深度优先遍历」的最直观描述。对应着「递归」来说,走到底的意思就是递归栈不断开拓下去,南墙即递归的终止条件或者称为base情况回头就是递归栈返回,return回来。一般都是走到底递归栈return回来,但也会碰到需要「回溯」的情况。

但在刷题的过程中也会出现这样一种情况,没有很明显的return的base情况,反倒希望dfs把节点可行的方向都执行一遍,在外部定义一个全局变量,dfs内部通过访问数组进行标记从而避免重复访问,最后递归栈还是会自动return,不过是全部执行了一遍以后。参考题目6139.受限条件下可到达节点的数目

一些心得:

  1. 访问数组vis不一定是必须的,可以通过「污染」原来地图上的数据,达到不重复访问的目的。
  2. base case不一定是必须的,有些时候我们就需要递归栈开辟下去,全部搜索完再自动返回,图中尤其如此。
  3. 递归函数定义需要明确,在明白了定义以后,我们可以灵活地将其放在判断语句、return语句上,并且明白在怎么样的逻辑判断下应该继续递归。
  4. 不同于广度优先遍历在进入队列的时候就检查元素是否合法,深度优先遍历将异常返回的情况都当作base,在下一层直接返回。

相关题目

以下题目类型只是根据该题目最浅显的特征进行区分,有些题目可能是多种类型的结合,需要灵活运用。同时搜索问题本质上是一样的,有些题目也可以通过广度优先遍历进行求解。

1. 隐式穷举遍历到底

隐式是指一下子分辨不出来它究竟是属于图、树、还是二维平面上的搜索问题,这种情况本质上还是用DFS这种搜索方法去遍历所有的情况,触底了就将符合条件的结果收集起来。

6137.检查数组是否存在有效划分

直观的,多种划分情况暴力递归,将接下来的情况交给子递归去做,base情况为划分点能够到达数组末尾。

下面做法是不了的,TLE但方法没错,原因是暴力递归想象成树,单节点分支为3,层数O(N),复杂度O(3^N),N为20就接近1*10^9。可以尝试在这个上面加记忆化。

class Solution {
    public boolean validPartition(int[] nums) {
        return dfs(nums,0);
    }
    //表示从index(包括index),能够划分nums
    public boolean dfs(int[]nums,int index){
        if(index==nums.length) return true;
        int len=nums.length;
        boolean ans1=false;
        boolean ans2=false;
        boolean ans3=false;
        if(index<=len-2&&nums[index]==nums[index+1]){
            if(dfs(nums,index+2)) ans1=true;
        }
        if(index<=len-3&&nums[index]==nums[index+1]&&nums[index+1]==nums[index+2]){
            if(dfs(nums,index+3)) ans2=true;
        } 
        if(index<=len-3&&nums[index]+1==nums[index+1]&&nums[index+1]+1==nums[index+2]){
            if(dfs(nums,index+3)) ans3=true;
        }
        return ans1||ans2||ans3;
    }
}

由于搜索状态就是「当前划分序号是否满足要求」,会出现很多子状态重复计算,可以进行记忆化DFS,通过数组m进行状态标记。

class Solution {
    int[]m;
    public boolean validPartition(int[] nums) {
        int len=nums.length;
        m=new int[len+1];
        return dfs(nums,0);
    }
    //表示从index(包括index),能够划分nums
    public boolean dfs(int[]nums,int index){
    	//如果状态已经被保存,直接返回
        if(m[index]==1) return true;
        if(m[index]==-1) return false;
        if(index==nums.length) return true;
        int len=nums.length;
        boolean ans1=false;
        boolean ans2=false;
        boolean ans3=false;
        if(index<=len-2&&nums[index]==nums[index+1]){
            if(dfs(nums,index+2)) ans1=true;
        }
        if(index<=len-3&&nums[index]==nums[index+1]&&nums[index+1]==nums[index+2]){
            if(dfs(nums,index+3)) ans2=true;
        } 
        if(index<=len-3&&nums[index]+1==nums[index+1]&&nums[index+1]+1==nums[index+2]){
            if(dfs(nums,index+3)) ans3=true;
        }
        //记录状态
        m[index]=ans1||ans2||ans3?1:-1;
        return ans1||ans2||ans3;
    }
}

由于状态结果是唯一的,所以记忆化搜索其实可以改为「动态规划」解法。状态定义dp[index]表示是否能到达数组边界index,由后面的状态往前推导。

class Solution {
    public boolean validPartition(int[] nums) {
        int len=nums.length;
        //dp[index]代表数组边界到index开始,是否可以正确划分
        boolean[]dp=new boolean[len+1];
        dp[len]=true;
        for(int index=len-1;index>=0;index--){
            if(index<=len-2&&nums[index]==nums[index+1]) dp[index]|=dp[index+2];
            if(index<=len-3&&nums[index]==nums[index+1]&&nums[index+1]==nums[index+2]){
                dp[index]|=dp[index+3];
            }
            if(index<=len-3&&nums[index]+1==nums[index+1]&&nums[index+1]+1==nums[index+2]){
                dp[index]|=dp[index+3];
            }
        }
        return dp[0];
    }
}
761.特殊的二进制序列

参考宫水三叶的题解s可以被可恰好划分为多个特殊的子串item,当且仅当某个子串满足1和0个数相等就可以,因为原始序列s可以保证第二个性质。第二个性质也表明,特殊子串item必然为1...0的模式。

因此可将本题构造分两步进行:1.对每个item进行重排,使其本身调整为字典序最大;2.对不同item之间进行重排,使最终s整体字典序最大。对于1,我们可以对 item 中的 1...0 中的非边缘部分进行调整(递归处理子串部分),同时用list将该子串进行收集。对于2,我们利用对list进行重排,重排参考179.最大数,最终组合出结果。

这里在一个DFS递归中,调用了多次递归,并将每次的结果收集起来,

class Solution {
    public String makeLargestSpecial(String s) {
        int len=s.length();
        return dfs(s,0,len-1);
    }
    public String dfs(String s,int start,int end){
        if(start>end) return "";
        ArrayList<String> list=new ArrayList<>();
        int count=0;
        int split=start;
        for(int i=start;i<=end;i++){
            if(s.charAt(i)=='1') count++;
            else count--;
            if(count==0){
                //步骤一,item本身重排
                list.add("1"+dfs(s,split+1,i-1)+"0");
                split=i+1;
            }
        }
        //步骤二,item整体重排
        //对ArrayList调用Collections.sort()
        Collections.sort(list,(a,b)->(b+a).compareTo(a+b));
        StringBuilder sb=new StringBuilder();
        for(String ss:list){
            sb.append(ss);
        }
        return sb.toString();
    }
}

2. 二维平面上的搜索问题

其中递归应该把握的点,从上往下分析问题:

  • 判断当前选择的点,本身是不是一个错的点,如果是直接return
  • 如果当前的点正确,需要做何种标记或修改
  • 剩下的情况,交给递归子调用去做。

其中判断错误的点应该把握:

  • 当前的点越出边界(通常是上一轮强行出来的)
  • 当前的点已经访问过(要么是已经污染修改,要么是标记已访问)
  • 当前的点不是目标点
79.单词搜索

参考笨猪爆破组的题解,其中将设置边界条件以及递推关系讲的很清楚。先将当前不能继续递归的条件进行return false。其中需要考虑单词复原的回溯处理。

class Solution {
    int n,m;
    public boolean exist(char[][] board, String word) {
        n=board.length;
        m=board[0].length;
        boolean[][] vis=new boolean[n][m];
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                if(dfs(board,vis,word,i,j,0)) return true;
            }
        }
        return false;
    }
    public boolean dfs(char[][] board,boolean[][] vis,String word,int i,int j,int idx){
        //越界或者已经访问过
        if(i<0||i>=n||j<0||j>=m||vis[i][j]==true) return false;
        //如果不匹配
        if(word.charAt(idx)!=board[i][j]) return false;
        //匹配到最后一个了,上面没有return false,就说明当前是正确的
        if(idx==word.length()-1) return true;
        
        //继续递归
        vis[i][j]=true;
        boolean res=dfs(board,vis,word,i+1,j,idx+1)||dfs(board,vis,word,i-1,j,idx+1)||dfs(board,vis,word,i,j-1,idx+1)||dfs(board,vis,word,i,j+1,idx+1);
        if(res) return true;
        //回溯
        vis[i][j]=false;
        return false;
    }
}
417.太平洋大西洋水流问题

从海洋的四周出发逆向DFS标记可以访问到的格子,递归下去的条件是heights大于等于四周格子,将两种海洋访问格子的交集返回。可以设置不同的全局数组分别进行可访问标记,递归函数对两种数组进行标记,同时只需要返回void。由于不断递归下去将数组进行标记,不需要额外的回溯操作。参考题解水往高处流

class Solution {
    int m,n;
    public List<List<Integer>> pacificAtlantic(int[][] heights) {
        m=heights.length;
        n=heights[0].length;
        boolean[][] reachPac=new boolean [m][n];
        boolean[][] reachAtl=new boolean [m][n];
        for(int i=0;i<m;i++){
            dfs(heights,reachPac,i,0);
            dfs(heights,reachAtl,i,n-1);
        }
        for(int i=0;i<n;i++){
            dfs(heights,reachPac,0,i);
            dfs(heights,reachAtl,m-1,i);
        }
        List<List<Integer>> ans=new ArrayList<>();
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(reachPac[i][j]&&reachAtl[i][j]){
                    ans.add(Arrays.asList(i,j));
                }
            }
        }
        return ans;
    }

    public void dfs(int[][]heights,boolean[][]canReach,int i,int j){
        //已经标记直接return
        if(canReach[i][j]) return;

        //进行标记并判断能否继续搜索
        canReach[i][j]=true;
        if(i-1>=0&&heights[i-1][j]>=heights[i][j]) dfs(heights,canReach,i-1,j);
        if(i+1<=m-1&&heights[i+1][j]>=heights[i][j]) dfs(heights,canReach,i+1,j);
        if(j-1>=0&&heights[i][j-1]>=heights[i][j]) dfs(heights,canReach,i,j-1);
        if(j+1<=n-1&&heights[i][j+1]>=heights[i][j]) dfs(heights,canReach,i,j+1);
        return;
    }
}

以下是岛屿问题,参考了以下优秀题解:

岛屿类问题的通用解法、DFS 遍历框架
一套模板,解决五个岛屿问题

核心是:

  1. 了解DFS的基本搜索结构,类比二叉树,1.判断base case 2. 访问相邻节点
  2. 不同于「树」不会访问重复节点,网格结构本质上是一个「图」,可能会重复访问,因此要标记访问避免重复访问的死循环(可以在原地图中修改,从而省下定义vis数组的空间)
  3. 主函数循环的理解,特别是开始dfs搜索的时机,有时候多个联通分量就要开始多次
岛屿问题-200.岛屿数量
class Solution {
    int m,n;
    public int numIslands(char[][] grid) {
        int ans=0;
        m=grid.length;
        n=grid[0].length;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                //这句很妙,碰到陆地就从它开始搜索,将这片区域变成已访问
                if(grid[i][j]=='1'){
                    dfs(grid,i,j);
                    ans++;
                }
            }
        }
        return ans;
    }
    public void dfs(char[][]grid,int i,int j){
        //把越界定义在前面,防止下面数组下标访问出错
        if(i<0||i>=m||j<0||j>=n) return;
        if(grid[i][j]!='1') return;

        grid[i][j]='2';
        dfs(grid,i+1,j);
        dfs(grid,i-1,j);
        dfs(grid,i,j+1);
        dfs(grid,i,j-1);
        // int[]dx={1,0,-1,0};
        // int[]dy={0,1,0,-1};
        // for(int k=0;k<4;k++){
        //     dfs(grid,i+dx[k],j+dy[k]);
        // }
    } 
}
岛屿问题-695.岛屿的最大面积

注意dfs函数定义的返回值为int,以及结束条件的返回值

class Solution {
    int m,n;
    public int maxAreaOfIsland(int[][] grid) {
        m=grid.length;
        n=grid[0].length;
        int ans=0;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(grid[i][j]==1){
                    int res=dfs(grid,i,j);
                    ans=Math.max(ans,res);
                }
            }
        }
        return ans;
    }
    public int dfs(int[][]grid,int i,int j){
        if(i<0||i>=m||j<0||j>=n) return 0;
        if(grid[i][j]!=1) return 0;
        grid[i][j]=2;
        return 1+dfs(grid,i-1,j)+dfs(grid,i+1,j)+dfs(grid,i,j-1)+dfs(grid,i,j+1);
    }
}
岛屿问题-463.岛屿的周长

充分利用了终止条件,碰到海洋、陆地、已访问过的节点返回值都不一样。

class Solution {
    int m,n;
    public int islandPerimeter(int[][] grid) {
        m=grid.length;
        n=grid[0].length;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(grid[i][j]==1) return dfs(grid,i,j);
            }
        }
        return 0;
    }
    public int dfs(int[][]grid,int i,int j){
        if(i<0||i>=m||j<0||j>=n) return 1;
        if(grid[i][j]==0) return 1;
        if(grid[i][j]==2) return 0;

        grid[i][j]=2;
        //grid[i][j]=1 本次不记录,继续往下搜索
        return dfs(grid,i+1,j)+dfs(grid,i-1,j)+dfs(grid,i,j+1)+dfs(grid,i,j-1);
    }
}
岛屿问题-463.岛屿的周长

参考岛屿问题DFS没有那么难,独立写出来的一次hard,这道题实际上是对网格做了两遍遍历:第一遍 DFS 遍历陆地格子,计算每个岛屿的面积并标记岛屿序号;第二遍直接循环遍历,找到海洋格子,观察每个海洋格子相邻的陆地格子,将对应岛屿序号的面积加起来。

class Solution {
    int n;
    public int largestIsland(int[][] grid) {
        n=grid.length;
        int index=2;
        HashMap<Integer,Integer> indexMap=new HashMap<>();
        //第一次遍历
        int maxArea=0;
        for(int i=0;i<n;i++){
            for(int j=0;j<n;j++){
                if(grid[i][j]==1){
                    int area=dfs(grid,i,j,index);
                    indexMap.put(index++,area);
                    maxArea=Math.max(maxArea,area);
                }
            }
        }

        //将ans初始化为上面最大的陆地面积,可以避免无海洋不能填海造陆的情况
        int ans=maxArea;
        //第二次遍历
        for(int i=0;i<n;i++){
            for(int j=0;j<n;j++){
                if(grid[i][j]==0){
                    int size=1;
                    HashSet<Integer> set=new HashSet<>();
                    if(inArea(i-1,j)&&grid[i-1][j]!=0)set.add(grid[i-1][j]);
                    if(inArea(i+1,j)&&grid[i+1][j]!=0)set.add(grid[i+1][j]);
                    if(inArea(i,j-1)&&grid[i][j-1]!=0)set.add(grid[i][j-1]);
                    if(inArea(i,j+1)&&grid[i][j+1]!=0)set.add(grid[i][j+1]);
                    if(!set.isEmpty()){
                        for(int k:set){
                            size+=indexMap.get(k);
                        }
                    }
                    ans=Math.max(size,ans);
                }
            }
        }
        return ans;
    }
    public int dfs(int[][]grid,int i,int j,int index){
        if(!(inArea(i,j))) return 0;
        if(grid[i][j]!=1) return 0;
        grid[i][j]=index;
        return 1+dfs(grid,i+1,j,index)+dfs(grid,i-1,j,index)+dfs(grid,i,j+1,index)+dfs(grid,i,j-1,index);
    }
    public boolean inArea(int i,int j){
        if(i<0||i>=n||j<0||j>=n) return false;
        return true;
    }
}
岛屿问题-1020.飞地的数量

从边界开始,将与边界相连的陆地1都标记出来,最后剩下的就是无法离开的陆地。

class Solution {
    int m,n;
    public int numEnclaves(int[][] grid) {
        m=grid.length;
        n=grid[0].length;
        for(int i=0;i<m;i++){
            dfs(grid,i,0);
            dfs(grid,i,n-1);
        }
        for(int i=0;i<n;i++){
            dfs(grid,0,i);
            dfs(grid,m-1,i);
        }
        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;
    }

    public void dfs(int[][]grid,int i,int j){
        if(i<0||i>=m||j<0||j>=n||grid[i][j]==0||grid[i][j]==2) return;
        
        grid[i][j]=2;
        dfs(grid,i+1,j);
        dfs(grid,i-1,j);
        dfs(grid,i,j-1);
        dfs(grid,i,j+1);
    }
}
岛屿问题-1254.统计封闭岛屿的数目

岛屿问题-200.岛屿数量的区别是,封闭岛屿的充要条件是四周都是水域,而本题没有假设网格四条边均被水包围,因此需要考虑未封闭的岛屿。函数返回结果为四个方向都满足封闭。两种方法解决:

  1. 是否为封闭岛屿,两个终止条件:
    1. 如果能到边界外,说明不是封闭岛屿 return false
    2. 如果g[i][j]为水域,说明被拦截了 return true
class Solution {
    int m,n;
    public int closedIsland(int[][] grid) {
        m=grid.length;
        n=grid[0].length;
        int ans=0;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(grid[i][j]==0&&dfs(grid,i,j))ans++;
            }
        }
        return ans;
    }
    public boolean dfs(int[][]grid,int i,int j){
        if(i<0||i>=m||j<0||j>=n) return false;
        //本来就是水域或者已经被访问标记
        if(grid[i][j]==1) return true;
        //grid[i][j]==0
        grid[i][j]=1;
        //&和&&结果一致,但是&判断了四个方向的封闭性,不存在短路规则
        return dfs(grid,i-1,j)&dfs(grid,i+1,j)&dfs(grid,i,j-1)&dfs(grid,i,j+1);
    }   
}
  1. 两遍DFS,先将与外界连通的岛屿进行处理,然后解法同岛屿问题-200.岛屿数量
class Solution {
    int m,n;
    public int closedIsland(int[][] grid) {
        m=grid.length;
        n=grid[0].length;
        for(int i=0;i<m;i++){
            preDfs(grid,i,0);
            preDfs(grid,i,n-1);
        }
        for(int i=0;i<n;i++){
            preDfs(grid,0,i);
            preDfs(grid,m-1,i);
        }
        int ans=0;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(grid[i][j]==0){
                    dfs(grid,i,j);
                    ans++;
                } 
            }
        }
        return ans;
    }
    public void dfs(int[][]grid,int i,int j){
        if(i<0||i>=m||j<0||j>=n||grid[i][j]==1) return;
        grid[i][j]=1;
        preDfs(grid,i+1,j);
        preDfs(grid,i-1,j);
        preDfs(grid,i,j-1);
        preDfs(grid,i,j+1);
    }

    public void preDfs(int[][]grid,int i,int j){
        //本来就是水域,或者已经被访问标记了
        if(i<0||i>=m||j<0||j>=n||grid[i][j]==1) return;
        grid[i][j]=1;
        preDfs(grid,i+1,j);
        preDfs(grid,i-1,j);
        preDfs(grid,i,j-1);
        preDfs(grid,i,j+1);
    }
}
岛屿问题-130.被围绕的区域

类似于 岛屿问题-1254.统计封闭岛屿的数目中的两种岛屿,但是这里不是统计数量,而需要改变原数组。
先将边界延伸的O替换成N,然后循环深搜替换掉所有被围绕的O,最终将N还原。

class Solution {
    int m,n;
    public void solve(char[][] board) {
        m=board.length;
        n=board[0].length;
        for(int i=0;i<m;i++){
            Ndfs(board,i,0);
            Ndfs(board,i,n-1);
        }
        for(int i=0;i<n;i++){
            Ndfs(board,0,i);
            Ndfs(board,m-1,i);
        }
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(board[i][j]=='O') dfs(board,i,j);
            }
        }
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(board[i][j]=='N') board[i][j]='O';
            }
        }
    }

    public void Ndfs(char[][]board,int i,int j){
        if(i<0||i>=m||j<0||j>=n||board[i][j]=='X'||board[i][j]=='N') return;
        board[i][j]='N';
        Ndfs(board,i+1,j);
        Ndfs(board,i-1,j);
        Ndfs(board,i,j-1);
        Ndfs(board,i,j+1);
    }

    public void dfs(char[][]board,int i,int j){
        if(i<0||i>=m||j<0||j>=n||board[i][j]=='X'||board[i][j]=='N') return;
        board[i][j]='X';
        dfs(board,i+1,j);
        dfs(board,i-1,j);
        dfs(board,i,j-1);
        dfs(board,i,j+1);
    }
}
1034.边界着色

这题主要是边界的判断条件:

  1. 颜色与grid[row][col]相同,与连通分量连接,这两个是必须满足的条件
  2. 当满足上述条件的点出现在最外一圈时,必为边界。
  3. 当满足上述条件的点出现在内圈时,如果它四周有一个点与grid[row][col]颜色不同,为边界。

此外,由于题目中color标记较多,在原数组上修改会造成混淆,因此采用vis数组的方式标记访问。

class Solution {
    int m,n;
    public int[][] colorBorder(int[][] grid, int row, int col, int color) {
        m=grid.length;
        n=grid[0].length;
        boolean[][] vis=new boolean[m][n];
        dfs(grid,vis,row,col,color,grid[row][col]);
        return grid;
    }
    public void dfs(int[][]grid,boolean[][]vis,int i,int j,int color,int curColor){
        //越界或者不是当前着色或者已访问,不作为当前考虑的点
        if(i<0||i>=m||j<0||j>=n||grid[i][j]!=curColor||vis[i][j]) return;

        //在矩阵边界上为边界
        if(i==0||i==m-1||j==0||j==n-1) grid[i][j]=color;
        //相邻存在不同的连通分量为边界
        if(diff(grid,vis,i-1,j,curColor)||diff(grid,vis,i+1,j,curColor)||diff(grid,vis,i,j-1,curColor)||diff(grid,vis,i,j+1,curColor)) grid[i][j]=color;
        vis[i][j]=true;
        dfs(grid,vis,i-1,j,color,curColor);
        dfs(grid,vis,i+1,j,color,curColor);
        dfs(grid,vis,i,j-1,color,curColor);
        dfs(grid,vis,i,j+1,color,curColor);
        
    }
    //需要标记访问,不然已经修改了颜色条件判断会出错,出现在此修改
    public boolean diff(int[][]grid,boolean[][]vis,int i,int j,int curColor){
        if(i<0||i>=m||j<0||j>=n||vis[i][j]) return false;
        if(grid[i][j]!=curColor) return true;
        return false;
    }
}
529.扫雷游戏

上来先判断是否点到雷,点到可以直接返回。

否则八个方向进行搜索,终止条件为越界或者非E(B或者有雷),遍历E的过程中计算八个方向雷的数量,有雷则显示数量停止搜索,没有雷则置B继续搜索。

class Solution {
    int n,m;
    public char[][] updateBoard(char[][] board, int[] click) {
        m=board.length;
        n=board[0].length;
        if(board[click[0]][click[1]]=='M'){
            board[click[0]][click[1]]='X';
            return board;
        }else{
            dfs(board,click[0],click[1]);
        }
        return board;
    }
    public void dfs(char[][] board,int i,int j){
        if(i<0||i>=m||j<0||j>=n) return;
        if(board[i][j]!='E') return;
        int count=calMine(board,i,j);
        if(count!=0){
            board[i][j]=(char)('0'+count);
            return;
        }else{
            board[i][j]='B';
            int[] dx={-1,-1,0,1,1,1,0,-1};
            int[] dy={0,1,1,1,0,-1,-1,-1};
            //注意是8个方向
            for(int k=0;k<8;k++){
                dfs(board,i+dx[k],j+dy[k]);
            }
        }
    }

    public int calMine(char[][] board,int i,int j){
        int[] dx={-1,-1,0,1,1,1,0,-1};
        int[] dy={0,1,1,1,0,-1,-1,-1};
        int count=0;
        for(int k=0;k<8;k++){
            if(i+dx[k]>=0&&i+dx[k]<m&&j+dy[k]>=0&&j+dy[k]<n&&board[i+dx[k]][j+dy[k]]=='M')count++;
        }
        return count;
    }
}

剑指Offer13.机器人的运动范围
简简单单DFS,注意vis数组,数位之和,函数定义如何返回格子数量

class Solution {
    public int movingCount(int m, int n, int k) {
        boolean[][] vis=new boolean[m][n];
        return dfs(vis,0,0,m,n,k);
    }
    public int dfs(boolean[][]vis,int i,int j,int m,int n,int k){
        if(i<0||i>=m||j<0||j>=n||vis[i][j]||cal(i)+cal(j)>k) return 0;
        vis[i][j]=true;
        return 1+dfs(vis,i-1,j,m,n,k)+dfs(vis,i+1,j,m,n,k)+dfs(vis,i,j-1,m,n,k)+dfs(vis,i,j+1,m,n,k);
    }
    public int cal(int k){
        int res=0;
        while(k>0){
            res+=k%10;
            k/=10;
        }
        return res;
    }
}
133.克隆图

参考题解DFS克隆图,深刻理解java对于引用类型的值拷贝,需要建立原图的克隆图,理论上要遍历团图的同时创建和其所有节点的克隆,值一样,节点已经不是原图的引用了,要重新创建。DFS遍历原图所有节点,其中子问题是DFS遍历其所有儿子节点,在这个过程中克隆。通过<Node,Clone>的Map记录原先节点是否已经被克隆,如果被克隆直接返回克隆指针。

为什么要用Map标记访问?
可以试着考虑只有两个点的情况。第一个点克隆之后如果不被添加到visited就直接开始克隆第二个点,则第二个点的克隆方法中会再次调用方法(创建克隆点,等待第一个点的引用加入)克隆第一个点,始终没有点被加入到visited中return,出现死循环。

/*
// Definition for a Node.
class Node {
    public int val;
    public List<Node> neighbors;
    public Node() {
        val = 0;
        neighbors = new ArrayList<Node>();
    }
    public Node(int _val) {
        val = _val;
        neighbors = new ArrayList<Node>();
    }
    public Node(int _val, ArrayList<Node> _neighbors) {
        val = _val;
        neighbors = _neighbors;
    }
}
*/

class Solution {
    //整道题就是用DFS复制已经存在的图
    //邻接表输出根据Node指针自动创建
    public Node cloneGraph(Node node) {
        HashMap<Node,Node> lookup=new HashMap<>();
        return dfs(node,lookup);
    }
    public Node dfs(Node node, HashMap<Node,Node>lookup){
        //如果当前节点为空,则创建空节点,
        if(node==null) return null;
        //如果已经被访问过克隆过,直接返回它的克隆
        if(lookup.containsKey(node)) return lookup.get(node);
        //没有被访问过,创建克隆节点
        Node clone =new Node(node.val,new ArrayList<Node>());
        //clone目前为空,后面通过引用指针往其中放入节点
        lookup.put(node,clone);
        for(Node n:node.neighbors) clone.neighbors.add(dfs(n,lookup));
        return clone;
    }
}
547.省份数量

DFS统计连通域数量,从每个未访问过的节点进行搜索,通过“污染”的方式对和它相连且未被访问过的点进行标记,最后执行DFS的次数就是连通域个数。相比于二维平面上的搜索题目往上下左右进行搜索标记,如果已被访问则不搜索,这里是对相邻且未被访问的节点进行搜索标记,需要理解DFS的搜索路线。

class Solution:
    def findCircleNum(self, isConnected: List[List[int]]) -> int:
        vis=set()
        res=0
        # 从某个节点开始将与其连接的所有点都进行访问
        def dfs(i):
            for j in range(len(isConnected[i])):
                if j not in vis and isConnected[i][j]==1:
                    vis.add(j)
                    dfs(j)

        # 从每个未被访问过的节点开始进行搜索
        for i in range(len(isConnected)):
            if i not in vis:
                vis.add(i)
                dfs(i)
                res +=1
        return res
839.相似字符串组

DFS或者BFS找连通分量,一组单词就是相似的所有单词组成的一个连通分量。所以本质上就是上面的省份数量,只不过这里的isConnected是相似字符串的判断,相似则connect.

DFS查找连通分量个数,时间复杂度 O ( N 2 ) O(N^2) O(N2),循环dfs N次,每次都尝试标记所有的N个节点。

class Solution {
    HashSet<Integer> vis=new HashSet<>();
    public int numSimilarGroups(String[] strs) {
        int ans=0;
        for(int i=0;i<strs.length;i++){
            if(!vis.contains(i)){
                vis.add(i);
                dfs(i,strs);
                ans++;
            }
        }
        return ans;
    }
    
    public void dfs(int k,String[] strs){
        for(int i=0;i<strs.length;i++){
        	// 从前往后检查后面未被访问过的单词i
            if(!vis.contains(i)&&isConnect(strs[k],strs[i])){
                vis.add(i);
                dfs(i,strs);
            }
        }
    }

    public boolean isConnect(String a, String b){
        int count=0;
        for(int i=0;i<a.length();i++){
            if(a.charAt(i)!=b.charAt(i)) count++;
        }
        return count<=2; // 必定成立,必然有0,2,4...个位置不同
    }
}

BFS也可以解决连通数量问题,时间复杂度 O ( N 2 ) O(N^2) O(N2),循环bfs N次,每次在一次一次出队入队的过程中都尝试标记所有的N个节点。

class Solution {
    HashSet<Integer> vis=new HashSet<>();
    public int numSimilarGroups(String[] strs) {
        int ans=0;
        for(int i=0;i<strs.length;i++){
            if(!vis.contains(i)){
                bfs(i,strs);
                ans++;
            }
        }
        return ans;
    }


    public void bfs(int k,String[] strs){
        Deque<Integer> que=new ArrayDeque<>();
        vis.add(k);
        que.offer(k);
        while(!que.isEmpty()){
            int top=que.poll();
            // 从前往后检查后面未被访问过的单词i
            for(int i=0;i<strs.length;i++){
                if(!vis.contains(i)&&isConnect(strs[top],strs[i])){
                    vis.add(i);
                    que.offer(i);
                }
            }
        }
    }



    public boolean isConnect(String a, String b){
        int count=0;
        for(int i=0;i<a.length();i++){
            if(a.charAt(i)!=b.charAt(i)) count++;
        }
        return count<=2; // 必定成立,必然有0,2,4...个位置不同
    }
}

3. 回溯问题

784.字母大小写全排列

用DFS遍历+回溯,将所有从根节点到叶子节点的路径收集起来,从根节点到叶子节点的序列长度就是树的高度,分支就是看当前元素是否有大小写两种可能,如果不能则继续往下搜索,如果能则有另一条搜索选择。

class Solution:
    def letterCasePermutation(self, s: str) -> List[str]:
        l=list(s)
        ans=[]
        def dfs(i):
            if i== len(s):
                ans.append(''.join(l))
                return 
            # 保持原样
            dfs(i+1)
            # 可以转换大小写
            if l[i].isalpha():
                l[i]=chr(ord(l[i])^32)
                dfs(i+1)
                # 转换回来
                l[i]=chr(ord(l[i])^32)
        dfs(0)
        return ans

4. 动态规划结合问题

5. 图相关

797.所有可能的路径

深度优先搜索(回溯),采用全局path记录路径,因此需要回溯来撤销影响,所有节点的相邻节点都已经给出,所以不需要建图。题目保证有向无环图(DAG),因此也不需要考虑节点重复访问出现死循环的问题,不用标记访问。

class Solution {
    LinkedList<Integer>path;
    List<List<Integer>> res;
    int n;
    int[][] graph;
    public List<List<Integer>> allPathsSourceTarget(int[][] g) {
        n=g.length-1;
        path=new LinkedList<>();
        res=new ArrayList<>();
        graph=g;
        path.add(0);
        backTracing(0);
        return res;
    }
    public void backTracing(int cur){
        if(cur==n){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i:graph[cur]){
            path.add(i);
            backTracing(i);
            path.removeLast();
        }
    }
}
2492.两个城市间路径的最小分数

深度优先遍历,“一条路径可以多次包含同一条道路”相当于遍历一个连通分量的所有边,找到其中的最小值,不同于要找到从起点到终点的最短路径,比如test case2。图中需要标记访问防止重复访问死循环。

class Solution {
    boolean[] vis;
    int ans=0x3f3f3f3f;
    ArrayList<int[]>[] map;
    public int minScore(int n, int[][] roads) {
        vis=new boolean[n];
        // 存图
        map=new ArrayList[n];
        for(int i=0;i<n;i++){
            map[i]=new ArrayList<int[]>();
        }
        for(int[]r:roads){
            map[r[0]-1].add(new int[]{r[1]-1,r[2]});
            map[r[1]-1].add(new int[]{r[0]-1,r[2]});
        }
        dfs(0);
        return ans;
    }

    // 直接在这层不让vis[n]=true的点进入下层
    public void dfs(int n){
        vis[n]=true;
        for(int[] i:map[n]){
            // 更新最小边要在dfs之前
            ans=Math.min(ans,i[1]);
            // 不能重复遍历
            if(!vis[i[0]]) dfs(i[0]);
        }
    }

    // // 将return情况放到下层去处理
    // public void dfs(int n){
    //     if(vis[n]) return;
    //     vis[n]=true;
    //     for(int[] i:map[n]){
    //         // 更新最小边要在
    //         ans=Math.min(ans,i[1]);
    //         dfs(i[0]);
    //     }
    // }
}
LCP07.传递信息

根据relations数组的定义,这是一个边权相等的图,对于边权相等的图,统计有限步数的到达某个节点的方案数,最常见的方式是使用 BFSDFS。对于只需要存储邻点信息,存图方式可以采用HashMap实现,参考算法学习-最短路算法与链式前向星,配图加深理解

在 DFS 过程中限制深度最多为 k,然后检查所达节点为 n-1 的次数即可。递归函数void dfs(int id, int sum)定义为:搜索到点id,走的步数为sum。

class Solution {
    HashMap<Integer,HashSet<Integer>> adj=new HashMap<>();
    int n,k,ans;
    public int numWays(int _n, int[][] relation, int _k) {
        n=_n;
        k=_k;
        ans=0;
        for(int[]r:relation){
            HashSet<Integer> set=adj.getOrDefault(r[0],new HashSet<>());
            set.add(r[1]);
            adj.put(r[0],set);
        }
        dfs(0,0);
        return ans;
    }
    public void dfs(int id,int sum){
        if(sum==k){
            if(id==n-1) ans++;
            return;
        }
        HashSet<Integer> set=adj.get(id);
        if(set==null) return;
        for(int i:set){
            dfs(i,sum+1);
        }
    }
}
6139.受限条件下可到达节点的数目

采用容器存图,DFS递归遍历图并标记,这里都是碰到未访问过的节点继续递归,所以开头直接无脑ans++,就是从0开始的连通分量中的节点数。相关存图方式可以看我的算法学习-最短路算法与各种存图方式,链式前向星,配图加深理解,讲的很详细。

class Solution {
    int ans=0;
    public int reachableNodes(int n, int[][] edges, int[] restricted) {
        HashSet<Integer>[] adj=new HashSet[n];
        for(int i=0;i<n;i++){
            adj[i]=new HashSet<>();
        }
        for(int[]e:edges){
            adj[e[0]].add(e[1]);
            adj[e[1]].add(e[0]);
        }
        boolean[]vis=new boolean[n];
        //受限点可以简单处理成已访问的点
        for(int r:restricted){
            vis[r]=true;
        }
        dfs(0,vis,adj);
        return ans;
    }
    public void dfs(int node,boolean[]vis, HashSet<Integer>[] adj){
        vis[node]=true;
        ans++;
        for(int i:adj[node]){
            if(!vis[i]){
                dfs(i,vis,adj);
            }
        }
    }
}
886.可能的二分法

邻接矩阵存图+染色法+DFS遍历(没有回溯,一直往下标记完)。通过一个数组染色来决定每个点的分组情况,我们从前往后一个个染色就可以了,因为前面的标记可以一并对后面的决策产生影响力。将不喜欢的关系用邻接矩阵存图,每个点在用邻接矩阵访问自己的相邻节点时,需要确保有dislike关系(graph[i][j]==1)的人不是自己同一组的,这其中又有两种情况,已经被标记在自己组内没办法了,返回false,如果还未被标记,则递归看反向标记是否可以。

class Solution:
    def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool:
        graph=[[0]*n for _ in range(n)]
        # 标记数组,0表示未分组,1为第一组,-1为第二组,不断往下标记
        colors=[0]*n
        for a,b in dislikes:
            graph[a-1][b-1]=1
            graph[b-1][a-1]=1
        for i in range(n):
            # 对数组中未标记的点进行标记检查,看能否标记成1,一种情况不满足就return
            # 只需要检查当前点是否能标记成1,因为在前面dfs过程中,已经给其他可以标记的点分好组了
            if colors[i]==0 and not self.dfs(graph,colors,1,i,n):
                return False
        return True

    # 判断某个点i是否能被标记成color
    def dfs(self,graph,colors,color,i,n):
        colors[i]=color
        for j in range(n):
            # 对于存在dislike的组
            if graph[i][j]==1:
                # 已经分成同组的了
                if colors[j]==color:
                    return False
                # 不能被分到别的组去
                if colors[j]==0 and not self.dfs(graph,colors,-1*color,j,n):
                    return False
        return True
785.判断二分图

DFS+邻接表的数组形式存图,每个节点可以相邻的点都已经列出来了,其他思路同上。

class Solution:
    def isBipartite(self, graph: List[List[int]]) -> bool:
        n = len(graph)
        colors= [0]*n
        for i in range(n):
        	# 一种情况不满足就return
            if colors[i]==0 and not self.dfs(graph,colors,1,i):
                return False
        return True

    def dfs(self,graph,colors,color,i):
        colors[i]=color
        # 对于相邻的点
        for j in graph[i]:
            if colors[j]==color:
                return False
            if colors[j]==0 and not self.dfs(graph,colors,-1*color,j):
                return False
        return True    
934.最短的桥

DFS标记图+层数相关的多源BFS,不同于1162.地图分析要找到所有最短路的最大值,这里只需要存在一条最近的桥就可以了,可以在找到和当前值不同的值直接return层数。刚开始多源BFS将一座岛上的层数都看为0,在向外扩张的时候相当于是一层一层往外移动,找到最近的另一座岛上的节点就返回。

class Solution:
    def shortestBridge(self, grid: List[List[int]]) -> int:
        que=deque()
        def dfs(grid,i,j,mark):
            if i<0 or i>=len(grid) or j<0 or j>=len(grid[0]):
                return
            if grid[i][j] != 1:
                return
            grid[i][j]=mark
            que.append((i,j))
            dfs(grid,i+1,j,mark)
            dfs(grid,i-1,j,mark)
            dfs(grid,i,j-1,mark)
            dfs(grid,i,j+1,mark)

        find=0
        for i in range (len(grid)):
            for j in range(len(grid[0])):
                if grid[i][j]==1 and find==0:
                    dfs(grid,i,j,2)
                    find +=1
                    
        # BFS 找最短路径
        distance =-1
        while len(que)!=0:
            distance +=1
            size = len(que)
            for _ in range(size):
                top=que.popleft()
                dx=[0,1,0,-1]
                dy=[1,0,-1,0]
                for k in range(4):
                    i,j=top[0]+dx[k],top[1]+dy[k]
                    if i<0 or i>=len(grid) or j<0 or j>=len(grid[0]):
                        continue
                    if grid[i][j] == 2:
                        continue
                    # 直接return 结果,已走路径的两个端点都不算
                    if grid[i][j] == 1:
                        return distance
                    grid[i][j]=2
                    que.append((i,j))
        return distance

6. 树相关

129.求根节点到叶节点数字之和

dfs返回值表示以root节点为根的子树的路径总和(累积了前面的数位),叶子节点直接返回

num表示不包含root.val值的根节点开始到root节点结尾的子路径值

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int sumNumbers(TreeNode root) {
        return dfs(root,0);
    }
    public int dfs(TreeNode root,int num){
        //如果二叉树单侧无节点,直接返回0
        if(root==null) return 0;
        num=num*10+root.val;
        //如果为叶子节点,直接返回结果
        if(root.left==null&&root.right==null) return num;
        return dfs(root.left,num)+dfs(root.right,num);
    }
}
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

互联网民工蒋大钊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值