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