回溯算法的了解

目录

什么是回溯法

DFS深度优先遍历

      N皇后问题

      Offer 12 矩阵中的路径

      排列/组合/子集问题 

46. 全排列

 Offer 38字符串的排列​编辑

78子集

39组合总和

 17电话号码的字母组合 

401二进制手表

1079活字印刷

       494 目标和

       括号问题

22括号生成

        690员工的重要性

        733图像渲染

        岛屿问题

200岛屿数量​编辑

463岛屿的周长

BFS广度优先遍历 


什么是回溯法

  • 回溯算法本质就是一个暴力枚举的思路(动态是求最值这个类型,是有选择的枚举,具有重叠结构,和最优子结构)
  • 穷举的过程就遍历一颗多叉树的过程
  • 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解 条件时,就回溯返回,尝试别的路径。
  • 回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点 称为回溯点。也可以称为剪枝点,所谓的剪枝,指的是把不会找到目标,或者不必要的路径裁剪掉。
  • 许多复杂的,规模较大的问题都可以使用回溯法,有通用解题方法的美称。
  • 在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
  • 若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。 而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
  • 除过深度优先搜索,常用的还有广度优先搜索。

DFS深度优先遍历

全排列问题 

class Solution {
     List<List<Integer>> res=new LinkedList<>();
    public List<List<Integer>> permute(int[] nums) {
        LinkedList<Integer>track=new LinkedList<>();
        backtrack(nums,track);
        return res;
    }

    private void backtrack(int[] nums, LinkedList<Integer> track) {
        if (track.size()==nums.length){
            res.add(new LinkedList<>(track));
            return;
        }
        for (int i = 0; i <nums.length ; i++) {
            //排除不合法的选择 剪枝
            if (track.contains(nums[i])){
                continue;
            }
            //做选择
            track.add(nums[i]);
            //进入下一层的决策树
            backtrack(nums,track);
            //取消选择,返回上一层的树、
            track.removeLast();
        }
    }
}

  • 只要遍历到叶子节点,这个路径就是一个排列的组合 ,这里有剪枝的操作,就是当遇到链表中出现的数字,我们就不需要往下递归了

N皇后问题

  • 首先我们去考虑,如果不考虑列之间和斜线之间的冲突问题,那就是让我们去每一行去放一个皇后

  • 如果这样去考虑,相当于第一列代表1,第二列代表2,第三列代表3, 可以选1 1 1,1 1 2,1 1 3,1 2 1.....

这种考虑的多叉树

  •  走到叶子节点的路径就是我们的放置方法,我们在全部的方法中满足列和列之间不能攻击,列与列之间不能攻击的问题
  • 下面就是利用不能选择的条件去剪枝

  • 依靠这三个条件来判断能不能添加,进行剪枝 
class Solution {
   List<List<String>> res=new LinkedList<>();//棋盘的存储
    public List<List<String>> solveNQueens(int n) {
        char[][] board = new char[n][n];
        for (char[] c : board) {
            Arrays.fill(c, '.');
        }
        bakctrack(board,0);
        return res;
    }

    private void bakctrack(char[][] board, int row) {
        if(row==board.length){
            res.add(Array2List(board));
            return;
        }
        int n=board[0].length;
        for (int col = 0; col < n; col++) {
            //排除会发生冲突的格子
            if (!isValid(board,row,col)){
                continue;
            }
            //进行选择
            board[row][col]='Q';
            //进行下一行的放皇后
            bakctrack(board,row+1);
            //撤销选择
            board[row][col]='.';
        }
    }
    public List Array2List(char[][] chessboard) {
        List<String> list = new ArrayList<>();

        for (char[] c : chessboard) {
            list.add(String.copyValueOf(c));
        }
        return list;
    }

    private boolean isValid(char[][] board, int row, int col) {
        int n=board.length;
        //检查列是否有冲突
        for (int i = 0; i < n; i++) {
            if (board[i][col]=='Q'){
                return false;
            }
        }
        for (int i = row-1,j=col+1; i >=0&&j<n; i--,j++) {
                if (board[i][j]=='Q'){
                    return false;
                }
        }
        for (int i = row-1,j=col-1; i >=0&&j>=0; i--,j--) {
             if(board[i][j]=='Q'){
                 return false;
             }
        }
        return true;
    }
}

Offer 12 矩阵中的路径

  •  不存在重叠子问题所以我们不能用动态规划,只能用回溯算法,回溯算法就是一颗多叉树,(我们可以任意选一个结点开始走)我们每次都可以有四个方向可以走,往左,往右,往上,往下,首先注意我们当前的坐标合法不合法,这是前提,然后比较当前的字符根我们的字符串的当前字符是否能匹配上,如果没有匹配,就不要继续往下走了(剪枝),如果匹配上了,这个格子的字符就不能再用了,我们需要将其变成了字符表示我们已经走过了,当我们不使用这个格子的时候,就将其还原(回溯)
class Solution {
    public boolean exist(char[][] board, String word) {
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                //从每个节点开始进行搜索
               if (searchHelper(board,i,j,word,0)){
                   return true;
               }
            }
        }
        return false;
    }

    private boolean searchHelper(char[][] board, int i, int j, String word, int length) {
        if (i<0||i>=board.length||j<0||j>=board[0].length||board[i][j]!=word.charAt(length)){
            return false;
        }
        length++;
        if (length==word.length()){
            return true;
        }
        board[i][j]='\0';
        boolean res= searchHelper(board,i+1,j,word,length)||searchHelper(board,i,j+1,word,length)
                || searchHelper(board,i-1,j,word,length)|| searchHelper(board,i,j-1,word,length);
        board[i][j]=word.charAt(length-1);
        return res;
    }
}

排列/组合/子集问题 

  • 形式一、元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次,这也是最基本的形式

以组合为例,如果输入 nums = [2,3,6,7],和为 7 的组合应该只有 [7]

  • 形式二、元素可重不可复选,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次

以组合为例,如果输入 nums = [2,5,2,1,2],和为 7 的组合应该有两种 [2,2,2,1] 和 [5,2]

  • 形式三、元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次

以组合为例,如果输入 nums = [2,3,6,7],和为 7 的组合应该有两种 [2,2,3] 和 [7]

  • 当然,也可以说有第四种形式,即元素可重可复选。但既然元素可复选,那又何必存在重复元素呢?元素去重之后就等同于形式三,所以这种情况不用考虑。

上面用组合问题举的例子,但排列、组合、子集问题都可以有这三种基本形式,所以共有 9 种变化。

形式一、元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次

/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
    // 回溯算法标准框架
    for (int i = start; i < nums.length; i++) {
        // 做选择
        track.addLast(nums[i]);
        // 注意参数
        backtrack(nums, i + 1);
        // 撤销选择
        track.removeLast();
    }
}

/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        // 剪枝逻辑
        if (used[i]) {
            continue;
        }
        // 做选择
        used[i] = true;
        track.addLast(nums[i]);

        backtrack(nums);
        // 撤销选择
        track.removeLast();
        used[i] = false;
    }
}
  •  全排列问题怎么防止我们重复用一个数字呢?用过一个数字的时候,就标记一下,不用的时候再还原
  • 子集和组合问题,我们就来确定选元素的起始位置,用过前面的值,我们以后就不用了,比如 1 2 3 ,我们用到2的时候,就只能用3了,不能用1了,因为组合和子集不在乎元素的顺序 ,1 2 / 2 1  是同一个元素

形式二、元素可重不可复选,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次

Arrays.sort(nums);
/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
    // 回溯算法标准框架
    for (int i = start; i < nums.length; i++) {
        // 剪枝逻辑,跳过值相同的相邻树枝
        if (i > start && nums[i] == nums[i - 1]) {
            continue;
        }
        // 做选择
        track.addLast(nums[i]);
        // 注意参数
        backtrack(nums, i + 1);
        // 撤销选择
        track.removeLast();
    }
}


Arrays.sort(nums);
/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        // 剪枝逻辑
        if (used[i]) {
            continue;
        }
        // 剪枝逻辑,固定相同的元素在排列中的相对位置
        if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
            continue;
        }
        // 做选择
        used[i] = true;
        track.addLast(nums[i]);

        backtrack(nums);
        // 撤销选择
        track.removeLast();
        used[i] = false;
    }
}
  • 对于这种拥有重复的重复的数据,我们必须要做排序的操作,为什么?我们为了将重复的元素弄到一起
  • 对于组合问题,我们怎么能不选择重复的元素呢?保持前面的操作,不要出现 1 2  2 1这种的情况,怎么做到不选重复的元素呢?因为排序好了,我们选了第一个元素,在选下一个元素,如果跟前面的一个元素一样的,那就跳过,不能选( 剪枝逻辑,值相同的相邻树枝,只遍历第一条),为什么大于start,因为对于同一层,我们第一个元素我们必须要加入,要从第同一层的第二个元素才能开始比较
  •   对于排列问题,我们在组合和子集的情况下,还得添加一个逻辑,因为组合和排列不一样,组合是不会区分顺序的,但是排列会,比如对于排列 1 2 ,2 1就是两个不同的元素,所以排列在乎的也不是不能出现 1 2 2  2 1 2这种问题,而是1 2 2' 1 2' 2这种问题,也就是重复元素的相对位置这种情况

假设输入为 nums = [1,2,2'],标准的全排列算法会得出如下答案:

[
    [1,2,2'],[1,2',2],
    [2,1,2'],[2,2',1],
    [2',1,2],[2',2,1]
]
  • 显然,这个结果存在重复,比如 [1,2,2'] 和 [1,2',2] 应该只被算作同一个排列,但被算作了两个不同的排列。所以现在的关键在于,如何设计剪枝逻辑,把这种重复去除掉?答案是,保证相同元素在排列中的相对位置保持不变,而我们的!used[i - 1]就是实现了这种逻辑,为什么呢?比如对于2 2',我们只要能保证2’必须出现在2后面,就可以排除这些重复的情况

如说 nums = [1,2,2'] 这个例子,我保持排列中 2 一直在 2' 前面。

这样的话,你从上面 6 个排列中只能挑出 3 个排列符合这个条件:

[ [1,2,2'],[2,1,2'],[2,2',1] ]

 形式三、元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次,只要删掉去重逻辑即可

/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
    // 回溯算法标准框架
    for (int i = start; i < nums.length; i++) {
        // 做选择
        track.addLast(nums[i]);
        // 注意参数
        backtrack(nums, i);
        // 撤销选择
        track.removeLast();
    }
}


/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        // 做选择
        track.addLast(nums[i]);
        backtrack(nums);
        // 撤销选择
        track.removeLast();
    }
}
  • 注意我们的组合和子集问题,还是需要来定一个开始的边界,虽然我们可以重复使用一个数,但是还是防止像 1 2 2    2 2 1这种情况的出现

46. 全排列

class Solution {
    LinkedList<Integer> list=new LinkedList<>();//用来存储当前的结果
    List<List<Integer>> lists=new LinkedList<>();//用来存储所有符合的结果
    boolean isUsed[];//用于标记数据是否使用过
    public List<List<Integer>> permute(int[] nums) {
            isUsed=new boolean[nums.length];
            backHelper(nums);
            return lists;
    }

    private void backHelper(int[] nums) {
        if (list.size()==nums.length){
            lists.add(new LinkedList<>(list));
        }
        for (int i = 0; i < nums.length; i++) {
             if (isUsed[i]){
                 continue;
             }
             //做出选择
             isUsed[i]=true;
             list.add(nums[i]);
             backHelper(nums);
             //撤销操作
             isUsed[i]=false;
             list.removeLast();
        }
    }
}

 Offer 38字符串的排列

  •   这里的字符串可能会出现的重复的字符,所以需要排序处理一下
  •  常规的全排序,需要判断一下元素是否用过,进入下一次树,更新对应的数据使用情况

class Solution {
        StringBuilder sb=new StringBuilder();//用来存储当前的结果
    LinkedList<String> list=new LinkedList<>();//用来存储所有的排列结构
    boolean []isUes;//用来判断当前数据是否用过
    public String[] permutation(String s) {
         permutationHelper(s.toCharArray());
         String arr[]=new String[list.size()];
        for (int i = 0; i < list.size(); i++) {
            arr[i]=list.get(i);
        }
        return arr;
         
    }

    private void permutationHelper(char[] date) {
        //将数据排序 为了处理重复数据的问题
        Arrays.sort(date);
        isUes=new boolean[date.length];
        backTrack(date);
    }

    private void backTrack(char[] date) {
        if (sb.length()==date.length){
            //说明到了叶子结点了
            list.add(sb.toString());
        }
        for (int i = 0; i <date.length; i++) {
            if (isUes[i]){
                //说明这个元素已经被用过了
                continue;
            }
            //保证相同的数据出现前后位置是固定的
            if (i>0&&date[i]==date[i-1]&&!isUes[i-1]){
                continue;
            }
            isUes[i]=true;
            sb.append(date[i]);
            backTrack(date);
            //撤销操作
            isUes[i]=false;
            sb.deleteCharAt(sb.length() - 1);
        }
    }

}

78子集

  •   这道题是不会出现重复元素,所以我们不需要做重复元素的处理,对于组合和子集的问题,我们利用起始数据的位置来防止出现 1 2   2 1这种情况的出现
  • 进入了下一层树,我们就要更新对应数据起始的位置

class Solution {
    LinkedList<Integer> list=new LinkedList<>();//用来存储当前的结果
    List<List<Integer>> lists=new LinkedList<>();//用来存储所有符合的结果
    public List<List<Integer>> subsets(int[] nums) {
           backHelper(nums,0);
           return lists;
    }

    private void backHelper(int[] nums, int start) {
        lists.add(new LinkedList<>(list));
        for (int j = start; j <nums.length ; j++) {
             list.add(nums[j] );
             backHelper(nums,j+1);
             list.removeLast();
        }
    }
}

39组合总和

  • 这是一个无重复可复选的组合子集问题,要判断当超过了这个值需要直接终止,因为每次加的数不是加1
class Solution {
 LinkedList<Integer> list=new LinkedList<>();
    List<List<Integer>> lists=new LinkedList<>();
    int sum;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
            backHelper(candidates,target,0);
            return lists;
    }

    private void backHelper(int[] candidates, int target,int start) {
        if (sum==target){
            lists.add(new LinkedList<>(list));
            return;
        }
        if (sum>target){
            return;
        }
        for (int i = start; i < candidates.length; i++) {
            sum+=candidates[i];
            list.add(candidates[i]);
            backHelper(candidates,target,i);
            sum-=candidates[i];
            list.removeLast();
        }
    }
}

 17电话号码的字母组合 

  •  这就是组合问题,因为返回的是所有的字母组合,就是将对应的数据处理一下
class Solution {
    // 每个数字到字母的映射
    String[] mapping = new String[] {
            "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
    };
    List<String> list=new LinkedList<>();//存储数据
    public List<String> letterCombinations(String digits) {
            if (digits.length()==0){
                return list;
            }
            backHelper(digits,0,new StringBuilder());
            return list;
    }

    private void backHelper(String digits, int start, StringBuilder sb) {
        if (sb.length()==digits.length()){
           list.add(new String(sb.toString()));
            return;
        }
        for (int i = start; i <digits.length(); i++) {
            int digit = digits.charAt(i) - '0';
            for (char c: mapping[digit].toCharArray()) {
                    sb.append(c);
                    backHelper(digits,i+1,sb);
                    sb.deleteCharAt(sb.length()-1);
            }
        }

    }
}

401二进制手表

  •  这就是一个组合问题,一共10个灯,不能重复选(用start来保证),且不存在重复的问题,给你一个k值,然后找到对应的数据
  • 关于表的处理,小时在前,分钟在后,用两个数组去完成
class Solution {
   int[] hours = new int[]{1, 2, 4, 8, 0, 0, 0, 0, 0, 0};
    int[] minutes = new int[]{0, 0, 0, 0, 1, 2, 4, 8, 16, 32};
    List<String> lists=new LinkedList<>();//用于存放
    public List<String> readBinaryWatch(int turnedOn) {
         backHelper(turnedOn,0,0,0);
         return lists;
    }

    private void backHelper(int turnedOn, int h, int m, int start) {
        if (h>11||m>59){
            return;
        }
        if (0==turnedOn){
            StringBuilder sb = new StringBuilder();
            sb.append(h).append(':');
            if (m < 10) {
                sb.append('0');
            }
            sb.append(m);
            lists.add(sb.toString());
            return;
        }
        for (int i = start; i <10 ; i++) {

            backHelper(turnedOn-1,h+hours[i],m+minutes[i],i+1);
        }
    }
}

1079活字印刷

  •  这就是重复不可复选的排列问题
class Solution {
   int res;
    boolean isUsed[];//表示是否被使用过
    StringBuilder sb=new StringBuilder();
    public int numTilePossibilities(String tiles) {
            char arr[]=tiles.toCharArray();
            isUsed=new boolean[arr.length];
            Arrays.sort(arr);
            backHelper(arr);
            return res;
    }

    private void backHelper(char[] arr) {
        if (sb.length()>0){
            res++;
        }
        if (sb.length()>arr.length){
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            if (isUsed[i]){
                //说明使用过
                continue;
             }
            if (i>0&&arr[i-1]==arr[i]&&!isUsed[i-1]){
                continue;
            }
            isUsed[i]=true;
            sb.append(arr[i]);
            backHelper(arr);
            isUsed[i]=false;
            sb.deleteCharAt(sb.length()-1);

    }
}
}

494 目标和

 回溯写法(暴力递归)

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
            if (nums.length==0){
                return 0;
            }
             backHelper(nums,target,0,0);
            return result;
    }

    /**
     *
     * @param nums
     * @param target
     * @param sum
     * @param length
     * @return
     */
    int result;
    private void backHelper(int[] nums, int target, int sum, int length) {
        if (length==nums.length){
            if (sum==target){
                result++;
                return;
            }else {
                return ;
            }
        }
        //做出加的选择
        sum+=nums[length];
        length++;
        backHelper(nums,target,sum,length);
        //撤销操作
        length--;
        sum-=nums[length];
        //做出减的操作
        sum-=nums[length];
        length++;
        backHelper(nums,target,sum,length);
        length--;
        sum-=nums[length];
    }
}
  •  这种只有两种选择,我们想一想,如果当我们的nums[i]=0的时候,那么是不是加减的操作结果是一样的,那么就是存在了重叠子问题,存在一个重叠子问题,对这种树结构,肯定会存在更多的重复操作

备忘录

   HashMap<String,Integer>  map=new HashMap<>();
    public int findTargetSumWays(int[] nums, int target) {
           //一个很重要的事情,怎么做备忘录
           //我们应该记录什么?
           //记录当前的遍历到那个数据了,和经过当前的操作当前的值为sum多少,对应的result为多少?
           //如果使用二维数组,我们不能确定sum为多少,也就不好确实数组的大小
           //用啥呢? 效率高的,HashMap,用字符串去表示key i+,+sum
            return dp(nums,target,0,0);

    }

    private int dp(int[] nums, int target, int sum, int length) {
        if (length==nums.length){
            if (sum==target){
                return 1;
            }else {
                return 0;
            }
        }
        String key=length+","+sum;
        if (map.containsKey(key)){
            return map.get(key);
        }
        int result=dp(nums,target,sum+nums[length],length+1)+
                dp(nums,target,sum-nums[length],length+1);
        map.put(key,result);
        return result;
    }

括号问题

  • 1、一个「合法」括号组合的左括号数量一定等于右括号数量,这个很好理解
  • 2、对于一个「合法」的括号字符串组合 p,必然对于任何 0 <= i < len(p) 都有:子串 p[0..i] 中左括号的数量都大于或等于右括号的数量

22括号生成

class Solution {
     List<String> list=new LinkedList<>();
    StringBuilder sb=new StringBuilder();
    public List<String> generateParenthesis(int n) {
        backHelper(0,0,n);
        return list;
    }

    private void backHelper(int left, int right, int n) {
        if (left==n&&right==n){
            list.add(sb.toString());
        }
        if (left<right){
            return;
        }
        if (left>n||right>n){
            return;
        }
        //尝试左括号
        sb.append("(");
        backHelper(left+1,right,n);
        //撤销
        sb.deleteCharAt(sb.length()-1);
        //尝试右括号
        sb.append(")");
        backHelper(left,right+1,n);
        //撤销
        sb.deleteCharAt(sb.length()-1);
       
    }
}

690员工的重要性

/*
// Definition for Employee.
class Employee {
    public int id;
    public int importance;
    public List<Integer> subordinates;
};
*/

class Solution {
   Map<Integer,Employee> map=new HashMap<>();
    public int getImportance(List<Employee> employees, int id) {
            if(employees.isEmpty()){
                return 0;
            }

        for (Employee e:employees) {
            map.put(e.id,e);
        }
       return dfs(id);
    }

    private int dfs(int id) {
        Employee employee=map.get(id);
        int sum=employee.importance;
        for (int i:employee.subordinates) {
               sum+=dfs(i);
        }
        return sum;
    }
}

733图像渲染

class Solution {
   int arr[][];
    public int[][] floodFill(int[][] image, int sr, int sc, int color) {
        if (image==null){
            return image;
        }
        arr=new int[image.length][image[0].length];
        for (int i = 0; i <image.length; i++) {
            for (int j = 0; j <image[0].length; j++) {
                arr[i][j]=image[i][j];
            }
        }
        int oldColor=image[sr][sc];
        dfsHelper(image,sr,sc,color,oldColor);
        return image;
    }

    private void dfsHelper(int[][] image, int sr, int sc, int color,int oldColor) {
        if (sr<0||sr>=image.length||sc<0||sc>=image[0].length||arr[sr][sc]!=oldColor||image[sr][sc]==color){
            return;
        }
        image[sr][sc]=color;
        dfsHelper(image,sr+1,sc,color,oldColor);
        dfsHelper(image,sr-1,sc,color,oldColor);
        dfsHelper(image,sr,sc+1,color,oldColor);
        dfsHelper(image,sr,sc-1,color,oldColor);
    }
}

岛屿问题

// 二维矩阵遍历框架
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;
    }
    if (visited[i][j]) {
        // 已遍历过 (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); // 右
}
// 方向数组,分别代表上、下、左、右
int[][] dirs = new int[][]{{-1,0}, {1,0}, {0,-1}, {0,1}};

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;
    }
    if (visited[i][j]) {
        // 已遍历过 (i, j)
        return;
    }

    // 进入节点 (i, j)
    visited[i][j] = true;
    // 递归遍历上下左右的节点
    for (int[] d : dirs) {
        int next_i = i + d[0];
        int next_j = j + d[1];
        dfs(grid, next_i, next_j, visited);
    }
    // 离开节点 (i, j)
}

200岛屿数量

  •  怎么找岛屿的数量,因为我们知道一个岛屿就是几个1连接在一起的区域,所以我们在遍历一个岛屿的时候,就把这个岛屿的变成海,就不会重复去计算岛的数量。利用dfs去把一个岛屿变成0
class Solution {
    public int numIslands(char[][] grid) {
        int rse=0;
        for (int i = 0; i <grid.length; i++) {
            for (int j = 0; j <grid[0].length; j++) {
                if (grid[i][j]=='1'){
                    rse++;
                    dfsHelper(i,j,grid);//从这个结点开始,把所有与它相连的岛屿都变成海
                }
            }
        }
        return rse;
    }

    private void dfsHelper(int i, int j, char[][] grid) {
        if (i<0||i>=grid.length||j<0||j>=grid[0].length){
            return;
        }
        if (grid[i][j]=='0'){
            //说明是海域
            //不能继续走了
            return;
        }
        grid[i][j]='0';
        dfsHelper(i+1,j,grid);
        dfsHelper(i-1,j,grid);
        dfsHelper(i,j+1,grid);
        dfsHelper(i,j-1,grid);
    }
}

463岛屿的周长

如何在 DFS 遍历时求岛屿的周长
求岛屿的周长其实有很多种方法,如果用 DFS 遍历来求的话,有一种很简单的思路:岛屿的周长就是岛屿方格和非岛屿方格相邻的边的数量。注意,这里的非岛屿方格,既包括水域方格,也包括网格的边界。我们可以画一张图,看得更清晰:

 将这个“相邻关系”对应到 DFS 遍历中,就是:每当在 DFS 遍历中,从一个岛屿方格走向一个非岛屿方格,就将周长加 1

class Solution {
   int res;
    public int islandPerimeter(int[][] grid) {
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                if (grid[i][j]==1){
                 dfsHelper(grid,i,j);
                 break;
                }
            }
        }
        return res;
    }

    private void dfsHelper(int[][] grid, int i, int j) {
        if (i<0||i>=grid.length||j<0||j>=grid[0].length){
            //说明从一个岛屿走到了边界 周长加1
            res++;
            return;
        }
        if (grid[i][j]==0){
            //说明从一个岛屿走到了水域
            res++;
            return;
        }
        if (grid[i][j]==2){
            //说明这个是岛屿,且这个地方已经来过了
            return;
        }
        grid[i][j]=2;
        dfsHelper(grid,i+1,j);
        dfsHelper(grid,i-1,j);
        dfsHelper(grid,i,j+1);
        dfsHelper(grid,i,j-1);
    }
}

BFS广度优先遍历 

// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
    Queue<Node> q; // 核心数据结构
    Set<Node> visited; // 避免走回头路
    
    q.offer(start); // 将起点加入队列
    visited.add(start);
    int step = 0; // 记录扩散的步数

    while (q not empty) {
        int sz = q.size();
        /* 将当前队列中的所有节点向四周扩散 */
        for (int i = 0; i < sz; i++) {
            Node cur = q.poll();
            /* 划重点:这里判断是否到达终点 */
            if (cur is target)
                return step;
            /* 将 cur 的相邻节点加入队列 */
            for (Node x : cur.adj()) {
                if (x not in visited) {
                    q.offer(x);
                    visited.add(x);
                }
            }
        }
        /* 划重点:更新步数在这里 */
        step++;
    }
}

492N叉树的层序遍历


class Solution {
        public List<List<Integer>> levelOrder(Node root) {
        List<List<Integer>> lists=new LinkedList<>();
        if (root==null){
            return lists;
        }
        Deque<Node> queue=new ArrayDeque<>();
        queue.offer(root);
        while (!queue.isEmpty()){
            int size=queue.size();
            List<Integer> list=new LinkedList<>();
            for (int i = 0; i < size; i++) {
                Node node=queue.poll();//取出头节点
                list.add(node.val);
                for (Node child: node.children) {
                    if (child!=null){
                        queue.offer(child);
                    }
                }
            }
            lists.add(list);
        }
        return lists;
    }
}

 

 

994腐烂的橘子

 

  • 利用层序遍历,第一层为已经腐烂的橘子,第二层为刚刚腐烂的橘子,直到这个矩阵中没有新鲜的橘子,或者没有腐烂新的腐烂的橘子就停止
class Solution {
        public int orangesRotting(int[][] grid) {
        int count=0;//表示新鲜橘子
        Deque<int []> queue=new ArrayDeque<>();
        for (int i = 0; i <grid.length ; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                  if (grid[i][j]==1){
                      count++;
                  }
                  if (grid[i][j]==2){
                      //腐烂的橘子 ,进入队列作为第一层
                      queue.offer(new int []{i,j});
                  }
            }
        }
        int time=0;
        while (count>0&&!queue.isEmpty()){
            int size=queue.size();
            for (int i = 0; i <size; i++) {
                int orange[]=queue.poll();
                int row=orange[0];
                int col=orange[1];
                if (row-1>=0&&grid[row-1][col]==1){
                    //如果是新鲜橘子,且坐标合法 作为下一层的传播者
                    grid[row-1][col]=2;
                    count--;
                    queue.offer(new int[]{row-1,col});
                }
                if (row+1<grid.length&&grid[row+1][col]==1){
                    //如果是新鲜橘子,且坐标合法 作为下一层的传播者
                    grid[row+1][col]=2;
                    count--;
                    queue.offer(new int[]{row+1,col});
                }
                if (col-1>=0&&grid[row][col-1]==1){
                    //如果是新鲜橘子,且坐标合法 作为下一层的传播者
                    grid[row][col-1]=2;
                    count--;
                    queue.offer(new int[]{row,col-1});
                }
                if (col+1<grid[0].length&&grid[row][col+1]==1){
                    //如果是新鲜橘子,且坐标合法 作为下一层的传播者
                    grid[row][col+1]=2;
                    count--;
                    queue.offer(new int[]{row,col+1});
                }
            }
            time++;
        }
        if (count>0){
            return -1;
        }else {
            return time;
        }
    }


}

 

127单词接龙

 

  • 1.通过BFS, 首先用beginWord带出转换一个字母之后所有可能的结果
  • 2.每一步都要把队列中上一步添加的所有单词转换一遍,最短的转换肯定在这些单词当中, 所有这些词的转换只能算一次转换,因为都是上一步转换出来的,这里对于每个单词的每个位置都可以用26个字母进行转换,所以一个单词一次转换的可能有:单词的长度 * 26
  • 3.把转换成功的新词入队,进行下一步的转换
  • 4.最后整个转换的长度就和BFS执行的次数相同

 

class Solution {
         public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        Set<String> WordDict=new HashSet<>();
        for (String word:wordList) {
            WordDict.add(word);
        }
        Set<String> useWord=new HashSet<>();//用于记录已经实现的字符,防止走回头路
        useWord.add(beginWord);
        Deque<String> queue=new ArrayDeque<>();
        queue.offer(beginWord);
        int res=1;
        while (!queue.isEmpty()){
            int size=queue.size();
            for (int i = 0; i < size; i++) {
                String currWord=queue.poll();//获得当前的单词
                //进行对每个字符进行处理
                for (int j = 0; j < currWord.length(); j++) {
                    StringBuilder newWord = new StringBuilder(currWord);
                    for (char ch = 'a'; ch <= 'z'; ch++) {
                        newWord.setCharAt(j, ch);

                        //如果这个单词是禁止出现的,或者之前我们已经试过了,那么就剪枝
                        String changeWord = newWord.toString();
                        if (!WordDict.contains(changeWord) || useWord.contains(changeWord)) {
                            continue;
                        }
                        if (changeWord.equals(endWord)) {
                            return res + 1;
                        }
                        //没有转换成功,那么新的字符串进队
                        useWord.add(changeWord);
                        queue.offer(changeWord);
                    }
                }
            }
            res++;
        }
        return 0;
    }

}

752打开转盘锁

 

  • 首先是对于字符串的处理,我们将其分开,用函数去处理
  • 一般来说在找最短路径的时候使用 BFS
class Solution {
     // 将 s[j] 向上拨动一次
    String plusOne(String s, int j) {
        char[] ch = s.toCharArray();
        if (ch[j] == '9')
            ch[j] = '0';
        else
            ch[j] += 1;
        return new String(ch);
    }
    // 将 s[i] 向下拨动一次
    String minusOne(String s, int j) {
        char[] ch = s.toCharArray();
        if (ch[j] == '0')
            ch[j] = '9';
        else
            ch[j] -= 1;
        return new String(ch);
    }
    public int openLock(String[] deadends, String target) {
        Deque<String> queue=new ArrayDeque<>();//用于BFS
        Set<String>  dead=new HashSet<>();
        for (String word: deadends) {
            dead.add(word);
        }
        Set<String> useWord=new HashSet<>();
        useWord.add("0000");
        queue.offer("0000");
        int step=0;
        while (!queue.isEmpty()){
            int size=queue.size();
            for (int i = 0; i < size; i++) {
                String currentPassword=queue.poll();
                if (dead.contains(currentPassword)){
                    continue;
                }
                if (currentPassword.equals(target)){
                    return step;
                }
                for (int j = 0; j < currentPassword.length(); j++) {
                  String up= plusOne(currentPassword,j);//将j位的数字上调一位
                    if (!useWord.contains(up)){
                        //如果没有用过,可以继续尝试
                        useWord.add(up);
                        queue.offer(up);
                    }
                    String down=minusOne(currentPassword,j);
                    if (!useWord.contains(down)){
                        //如果没有用过,可以继续尝试
                        useWord.add(down);
                        queue.offer(down);
                    }

                }
            }
            step++;
        }
        return -1;
    }
}

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

库里不会投三分

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

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

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

打赏作者

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

抵扣说明:

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

余额充值