Leetcode一起攻克搜索(BFS,DFS,回溯,并查集)

本文详细介绍了LeetCode中的BFS(广度优先搜索)、DFS(深度优先搜索)、回溯算法以及并查集的概念,并通过多个题目实例解析了这些算法的应用,包括员工重要性、朋友圈、岛屿数量等问题,帮助读者深入理解和掌握这些搜索算法。
摘要由CSDN通过智能技术生成

BFS简介

参考链接
在这里插入图片描述
广度优先搜索一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。
第一层:
0->{1,2,6,5}
第二层:
6->{4}
5->{3,4}
第三层:
3->{}
4->{}
每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。利用这个结论,可以求解最短路径等 最优解 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径,无权图是指从一个节点到另一个节点的代价都记为 1。
在程序实现 BFS 时需要考虑以下问题:

  1. 队列:用来存储每一轮遍历得到的节点;
  2. 标记:对于遍历过的节点,应该将它标记,防止重复遍历。

DFS简介

深度优先遍历顾名思义就是可着一条路去遍历。以下图举例,程序会优先去走一条路,比如会先a->b->e;之后以程序语言来说就是e是叶子节点了,所以结束递归,返回b的位置,此时b会取遍历f,路径就是a->b->f,之后就是返回a,a->c,a->d。其实上述描述语言为前序遍历,当前->左->右。
在这里插入图片描述
在程序实现 DFS 时需要考虑以下问题:
栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。

回溯简介

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就 “回溯” 返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为 “回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。

并查集简介

DFS题目

690. 员工的重要性

题目链接。本题需要考虑的就是不是直系下属的重要程度也需要考虑入内。

1.dfs解法:

dfs的思路就是一条路一条路的去走。
以该例子举例
[[1,5,[2,3]],[2,3,[4]],[3,4,[]],[4,1,[]]]
1
程序的流程是:首先找到员工1,之后找到直系下属员工2,之后找到非直系下属4,最后在非直系下属4的时候结束寻找。也就是传递的始终是当前要找的员工id。
路径为:1->2->4,1->3。

/*
// Employee info
class Employee {
    // It's the unique id of each node;
    // unique id of this employee
    public int id;
    // the importance value of this employee
    public int importance;
    // the id of direct subordinates
    public List<Integer> subordinates;
};
*/
class Solution {
   
    //不是直系下属的也需要添加进去。
    int impo;
    public int getImportance(List<Employee> employees, int id) {
   
        //还是dfs最好用我觉得
        //id与对应的员工信息
        Map<Integer,Employee>map = new HashMap<>();
        for(Employee e:employees)
            map.put(e.id,e);
        dfs(map,id);
        return impo;**加粗样式**
    }
    public void dfs(Map<Integer,Employee>map,int id)
    {
   
        Employee e = map.get(id);
        impo += e.importance;
        for(Integer i:e.subordinates)
        {
   
            dfs(map,i);
        }
        return ;
    }
}

2.bfs算法

bfs算法是一层一层的遍历。
以该例子举例
[[1,5,[2,3]],[2,3,[4]],[3,4,[]],[4,1,[]]]
1
程序的流程是,一层一层的去遍历,首先遍历的是我们要找的父员工,然后去遍历父员工的所有直系下属员工,之后遍历所有直系下员工的所有直系下属员工。也就是说每一回遍历传递的是一个列表,列表中存储当前层的所有id。
路径为:
第一层:1;
第二层:2,3;
第三层:4。

/*
// Employee info
class Employee {
    // It's the unique id of each node;
    // unique id of this employee
    public int id;
    // the importance value of this employee
    public int importance;
    // the id of direct subordinates
    public List<Integer> subordinates;
};
*/
class Solution {
   
    //不是直系下属的也需要添加进去。
    int impo;
    public int getImportance(List<Employee> employees, int id) {
   
        //还是dfs最好用我觉得
        //id与对应的员工信息
        Map<Integer,Employee>map = new HashMap<>();
        for(Employee e:employees)
            map.put(e.id,e);
        List<Integer>list = new ArrayList<>();
        list.add(id);
        bfs(map,list);
        return impo;
    }
    public void bfs(Map<Integer,Employee>map,List<Integer>l)
    {
   
        List<Integer>list=new ArrayList<>();
        for(Integer i:l)
        {
   
             Employee e = map.get(i);
            impo+=e.importance;
            list.addAll(e.subordinates);
        }
        if(list.size()!=0)
             bfs(map,list);
        return ;
    }
}

547.朋友圈

题目链接

dfs解法

这道题我的思路就是深度优先遍历。其实是两层遍历,第一层是遍历每个学生,第二层是遍历每个学生和他有好友关系的学生。第一层的含义其实是循环一次就代表一个朋友圈,因为我们要做的是有记忆功能的dfs。我们使用一个boolean型数组来存储,flag【i】代表i是否已经在一个朋友圈里了。当该学生不在朋友圈中,此时进入dfs递归。在dfs中我们去把该学生的好友关系挨个寻找一遍,以此类推,直到学生的好友关系的那些好友都已经在某个朋友圈当中了。
首先要理解第一个循环走一次就代表一个朋友圈,因为我会在这一次里把所有与该A学生有关的学生全部找到(不是直接有关系,有间接关系的也会找到),这些都在一个朋友圈内。下一次去找的学生是与他们一点关系都没有的学生,然后和这个学生有关系的学生们会组成另一个朋友圈。所以第一个循环走几次就代表有几个朋友圈。
其次,dfs中会遇到两个误导。
1.是否可以从(M【i】数组)i位置往后走,因为i位置以前的学生我都看过了。
答案是否定的。因为比如有四个学生。有可能第一个学生和第二个学生没关系,和第三个学生有关系,那么我们去找第三个学生的时候,就直接去看第三个和第四个学生是否有关系,那万一第三个和第二个学生还有关系呢?
更改做法:所以我们每次都必须从0开始往后找。
2.是否可以在dfs后再将flag【j】赋值为true呢?
答案是否定的。设想第二个学生和第三个学生有关系,那么我们会在M【2】数组的第三个位置进入dfs循环,之后又会在M【3】数组的第二个位置进入上一个dfs循环,这样就会一直循环。
**更改做法:**我们在进入dfs的时候将flag【i】设为true,因为我们是从i学生走过来的,所以我们要将i学生放入我们的朋友圈。

class Solution {
   
    int circles = 0;
     //有存储记忆
    boolean[]flag;
    public int findCircleNum(int[][] M) {
   
        //代表i是否已经在一个朋友圈了
        flag = new boolean[M.length];
       for(int i=0;i<M.length;i++){
   
            if(flag[i])
                continue;
           //循环几次就是有几个朋友圈(因为能循环,就代表这个学生没有被加入到朋友圈之中)
            circles++;
           dfs(M,flag,i);
        }
        return circles;
        }
    public void dfs(int[][]M,boolean[]flag,int i) {
   
        //列i之前的不用看了,肯定都看过了。
        //因为i行之前的行都看过了。
        //要看i学生,是否能和后面的学生继续互为朋友关系。
        //上面的是错的,因为比如有四个学生
        //有可能第一个学生和第二个学生没关系,和第三个学生有关系,
        //那么我们去找第三个学生的时候,就直接去看第三个和第四个学生是否有关系,那万一第三个和第二个学生还有关系呢?
        
        //我们把第i个同学加进我们的朋友圈。
         flag[i]=true;
        for(int j=0;j<M[0].length;j++)
        {
   
            if(flag[j]||i==j)
                continue;
            if(M[i][j]==1)
            {
   
            dfs(M,flag,j);
            //这会导致我们一直在dfs循环中,比如第二个和第三个有关系,那么第三个也和第二个有关系。
            //就会不断的循环dfs(M,flag,1),和dfs(M,flag,2),去dfs一次就应该把当前循环的那个
            //学生加入到朋友圈之中,但不能再dfs之后,应该在dfs初始就加入,防止陷入死循环。
            //flag[j]=false;
            }
        }
        
    }
}

200.岛屿数量

题目链接
本题实在是与朋友圈太像了,与陆地相连的陆地都算是一个岛屿中的陆地,找到数组中所有的岛屿。

dfs解法

使用具有记忆性的dfs。同样是首先循环判断数组中的每个值,当我们发现一个值为1,且他没有被寻找过,那我们就进入dfs递归。在dfs递归中,我们去继续判断该位置的上下左右位置。递归的结束条件是当位置越界或者该值为0(水)或该值被访问过了。
最后判断第一次循环中,走了多少else分支,就代表有多少个岛屿。具体见代码。

class Solution {
   
    //尝试dfs
    //其实思路就是当找到一个1后,就将所有与其垂直与水平连通(直接与间接)的1放一起组成岛屿,
    //然后看有多少个这样的岛屿
    char[][]grid;
    //必须带有记忆性
    boolean [][]flag;
    int rows;
    int columns;
    public int numIslands(char[][] grid) {
   
        this.grid = grid;
        //防止有空
        if(grid==null||grid.length==0)
            return 0;
        rows = grid.length;
        columns = grid[0].length;
        flag = new boolean [rows][columns];
        int island=0;
        for(int i=0;i<rows;i++)
            {
   
            for(int j=0;j<columns;j++)
            {
   
                if(flag[i][j]||grid[i][j]=='0')
                    continue;
                //能执行几次该语句就说明有多少个独立的岛屿
                
                else{
   
                   island++;
                    dfs(i,j);
                }
            }
        }
        return island;
        
    }
    public void dfs(int i,int j)
    {
   
        //判断越界以及是否为水以及是否已经判断过。
        if(i<0||i>=rows||j<0||j>=columns||grid[i][j]=='0'||flag[i][j])
            return ;
        //该值找过了
        flag[i][j]=true;
        //水平垂直方向
        dfs(i-1,j);
        dfs(i+1,j);
        dfs(i,j-1);
        dfs(i,j+1);
    }
}

417.太平洋大西洋水流问题

题目链接
要找到一个位置,它既可以将水流流到太平洋也可以流到大西洋。从位置(i,j)流动到(k,t)的条件为matrix[i][j]>=matrix[k][t]。
在这里插入图片描述

dfs解法

  1. 最初的想法是对每个位置去判断是否可以到达太平洋或者大西洋,后来想到这样递归的时间成本太大,所以最后思路是从太平洋(第一行和第一列)和大西洋的海岸(最后一行,最后一列)开始往陆地去寻找。
  2. 使用两个boolean数组来存放该位置是否可以到达大西洋/太平洋
  3. 需要注意的就是dfs要传入四个参数,前两个为位置i和j,第三个为对应海洋的数组,第四个为一个高度值第四个代表的是什么呢,可以理解为当前位置对应的高度值,只有大于等于该高度值才可以满足该位置能够到达对应海洋。
  4. 当所有海洋的边界(以及可到达海洋边界的点)都递归结束后,可以循环所有点了,将同时满足两个数组的值添加进list。
  5. 本算法的好处是节省时间。以上述例子的第二行来举例:32344,如果以正常每个位置都判断的算法,会判断3,判断2,判断3,判断4,判断4,而2,3,4,4还会继续向左判断。如果以我们的算法判断,在判断到2的时候就会停止判断,因为2<3,后面的水都无法从这里走到3。
class Solution {
   
    //原本思路是需要从大陆判断到海洋边界(每一块陆地去判断能否到达太平洋和大西洋)
    //后来发现可能会超时,那我们从海洋边界逆向判断吧。
    int[][]matrix;
    int rows;
    int columns;
    public List<List<Integer>> pacificAtlantic(int[][] matrix) {
   
     List<List<Integer>>results = new ArrayList<>();
        this.matrix=matrix;
        if(matrix==null||matrix.length==0)
            return results;
        rows = matrix.length;
        columns = matrix[0].length;
        //是否能到达太平洋
        boolean[][]tai = new boolean[rows][columns];
        //是否能到达大西洋
        boolean[][]da = new boolean[rows][columns];
        //第一行最后一行
        for(int j=0;j<columns;j++)
        {
   
            dfs(0,j,tai,-1);
            dfs(rows-1,j,da,-1);
        }
        //第一列,最后一列
        for(int i=0;i<rows;i++)
        {
   
            dfs(i,0,tai,-1);
            dfs(i,columns-1,da,-1);
        }
        for(int i=0;i<rows;i++)
        {
   
            for(int j=0;j<columns;j++)
            {
   
                if(tai[i][j]&&da[i][j])
                {
   
                   List<Integer>list = new ArrayList<>();
                    list.add(i);
                    list.add(j);
                    results.add(list);
                }
            }
        }
        return results;
    }
    //传入4个参数,前两个为当前位置,flag为存储能否到达当前洋的数组,last是上一个值(也是能到达flag海岸的最小值)
    public void dfs(int i,int j,boolean[][]flag,int last)
    {
   
         //越界
        if(i<0||i>=rows||j<0||j>=columns||matrix[i][j]<last)
            return ;
        //已经可以达到了。
        if(flag[i][j])
            return;
        flag[i][j]=true;
        dfs(i,j+1,flag,matrix[i][j]);
        dfs(i,j-1,flag,matrix[i][j]);
        dfs(i+1,j,flag,matrix[i][j]);
        dfs(i
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值