目录
相关算法专题
🌞回溯算法概念
🌻算法思想
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。也可以称为剪枝点,所谓的剪枝,指的是把不会找到目标,或者不必要的路径裁剪掉。
除过深度优先搜索,常用的还有广度优先搜索。
🌂深度优先搜索例题
💧全排列问题
☔全排列
题目:
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例:
输入:nums = [1,2,3]
输出:[ [1,2,3] , [1,3,2] , [2,1,3] , [2,3,1] , [3,1,2] , [3,2,1] ]
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
List<Integer> output = new ArrayList<Integer>();
//先把一种方法写进去
for (int num : nums) {
output.add(num);
}
int n = nums.length;
backtrack(n, output, res, 0);
return res;
}
public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
// 所有数都填完了
if (first == n) {
res.add(new ArrayList<Integer>(output));
}
for (int i = first; i < n; i++) {
// 动态维护数组
Collections.swap(output, first, i);
// 继续递归填下一个数
backtrack(n, output, res, first + 1);
// 撤销操作
Collections.swap(output, first, i);
}
}
}
☔字符串的排列
题目:
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = "abc"
输出:[ "abc","acb","bac","bca","cab","cba" ]
class Solution {
public String[] permutation(String s) {
HashSet<String> set= new HashSet<>();
List<String> ans = new ArrayList<>();
boolean[] flag = new boolean[s.length()];
dfs(new StringBuffer(),s,0,ans,flag,set);
return ans.toArray(new String[ans.size()]);
}
public void dfs(StringBuffer str,String s,int i,List<String> ans,boolean[] flag,HashSet<String> set){
if(s.length()==i){
if(set.contains(str.toString())){
return;
}
ans.add(str.toString());
//按照题目要求去重
set.add(str.toString());
return;
}
for(int j=0;j<s.length();++j){
if(flag[j]==true){
continue;
}
str.append(s.charAt(j));
//做一个标记,用过的字母就不要再用了
flag[j]=true;
dfs(str,s,i+1,ans,flag,set);
flag[j]=false;
str.deleteCharAt(str.length()-1);
}
}
}
☔子集
题目:
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例:
输入:nums = [1,2,3]
输出:[ [ ] ,[1] , [2] , [1,2] , [3] , [1,3] , [2,3] , [1,2,3] ]
class Solution {
List<List<Integer>> ans;
public List<List<Integer>> subsets(int[] nums) {
ans = new ArrayList<>();
List<Integer> list = new ArrayList<>();
dfs(0,nums,list);
return ans;
}
public void dfs(int i,int[] nums,List<Integer> list){
//如果递归完成,就入链表
if(i==nums.length){
ans.add(new ArrayList<>(list));
return;
}
list.add(nums[i]);
//加当前元素的情况
dfs(i+1,nums,list);
list.remove(list.size()-1);
//不加当前元素的情况
dfs(i+1,nums,list);
}
}
☔括号生成
题目:
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例:
输入:n = 3
输出:[ "( ( ( ) ) ) " , "( ( ) ( ) ) " , "( ( ) ) ( )" , "( ) ( ( ) )" , "( ) ( ) ( )" ]
class Solution {
public List<String> generateParenthesis(int n) {
List<String> list = new ArrayList<String>();
backtrack(list, new StringBuilder(), 0, 0, n);
return list;
}
public void backtrack(List<String> list, StringBuilder s, int left, int right, int n) {
if(n*2==s.length()){
list.add(s.toString());
return;
}
//左边未匹配括号数量小于n,进行加左括号的回溯操作
if(left<n){
s.append('(');
backtrack(list,s,left+1,right,n);
s.deleteCharAt(s.length()-1);
}
//同理
if(right<left){
s.append(')');
backtrack(list,s,left,right+1,n);
s.deleteCharAt(s.length()-1);
}
}
}
☔电话号码的字母组合
题目:
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:digits = "23"
输出:[ "ad","ae","af","bd","be","bf","cd","ce","cf" ]
class Solution {
public List<String> letterCombinations(String digits) {
List<String> list = new ArrayList<String>();
if (digits.length() == 0) {
return list;
}
Map<Character, String> map = new HashMap<Character, String>() {{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
dfs(list, map, digits, 0, new StringBuffer());
return list;
}
public void dfs(List<String> list, Map<Character, String> map, String digits, int i, StringBuffer s) {
//递归完成就加入链表
if(i==digits.length()){
list.add(s.toString());
}else{
//套用回溯模板
String str = map.get(digits.charAt(i));
for(int j=0;j<str.length();++j){
s.append(str.charAt(j));
dfs(list,map,digits,i+1,s);
s.deleteCharAt(i);
}
}
}
}
☔组合总和
题目:
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例:
输入:candidates = [2,3,6,7], target = 7
输出:[ [ 2,2,3 ] , [ 7 ] ]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> list = new ArrayList<Integer>();
dfs(candidates, target, ans, list, 0);
return ans;
}
public void dfs(int[] candidates, int target, List<List<Integer>> ans, List<Integer> list, int i) {
if (i == candidates.length) {
return;
}
if (target == 0) {
ans.add(new ArrayList<Integer>(list));
return;
}
//只要西澳娱目标值,就进行回溯操作
if (target - candidates[i] >= 0) {
list.add(candidates[i]);
dfs(candidates, target - candidates[i], ans, list, i);
list.remove(list.size() - 1);
}
//下一个元素的递归
dfs(candidates, target, ans, list, i + 1);
}
}
☔活字印刷
题目:
你有一套活字字模 tiles,其中每个字模上都刻有一个字母 tiles[i]。返回你可以印出的非空字母序列的数目。
注意:
本题中,每个活字字模只能使用一次。
示例:
输入:"AAB"
输出:8
解释:
可能的序列为 "A", "B", "AA", "AB", "BA", "AAB", "ABA", "BAA"。
class Solution {
public int numTilePossibilities(String tiles) {
boolean[] visited = new boolean[tiles.length()];
//引入哈希集合去重
Set<String> set = new HashSet<>();
dfs(visited, set, tiles, new StringBuilder());
return set.size() - 1;
}
public void dfs(boolean[] visited, Set<String> set, String tiles, StringBuilder sb) {
set.add(sb.toString());
for (int i = 0; i < tiles.length(); i++) {
if (!visited[i]) {
//做标记,如果用过这个字母,就不进行此次操作
visited[i] = true;
sb.append(tiles.charAt(i));
dfs(visited, set, tiles, sb);
sb.deleteCharAt(sb.length() - 1);
visited[i] = false;
}
}
}
}
💧树形结构问题
☔员工的重要性
题目:
给定一个保存员工信息的数据结构,它包含了员工 唯一的 id ,重要度 和 直系下属的 id 。
比如,员工 1 是员工 2 的领导,员工 2 是员工 3 的领导。他们相应的重要度为 15 , 10 , 5 。那么员工 1 的数据结构是 [1, 15, [2]] ,员工 2的 数据结构是 [2, 10, [3]] ,员工 3 的数据结构是 [3, 5, []] 。注意虽然员工 3 也是员工 1 的一个下属,但是由于 并不是直系 下属,因此没有体现在员工 1 的数据结构中。
现在输入一个公司的所有员工信息,以及单个员工 id ,返回这个员工和他所有下属的重要度之和。
示例:
输入:[ [1, 5, [2, 3] ], [2, 3, [ ] ], [3, 3, [ ] ] ], 1
输出:11
解释:
员工 1 自身的重要度是 5 ,他有两个直系下属 2 和 3 ,而且 2 和 3 的重要度均为 3 。因此员工 1 的总重要度是 5 + 3 + 3 = 11 。
提示:
一个员工最多有一个 直系 领导,但是可以有多个 直系 下属
/*
// Definition for Employee.
class Employee {
public int id;
public int importance;
public List<Integer> subordinates;
};
*/
class Solution {
public int dfs(Map<Integer,Employee>info,int id){
//下属为空是边界,下属为空时,return的是0
Employee curE = info.get(id);
int sum = curE.importance;
for(int subId : curE.subordinates){
//每次先加第一个下属的重要性
//按照相同的操作再去加下属的第一个下属的重要性
sum+=dfs(info,subId);
}
return sum;
}
public int getImportance(List<Employee> employees, int id) {
if(employees.isEmpty())
return 0;
Map<Integer,Employee> info = new HashMap<>();
for(Employee e:employees){
info.put(e.id,e);
}
//从第一个员工开始
return dfs(info,id);
}
}
☔二叉树中和为某一值的路径
题目:
给你二叉树的根节点 root
和一个整数目标和 targetSum
,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
示例:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[ [5,4,11,2] , [5,8,4,5] ]
/**
* 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 {
List<List<Integer>> list = new LinkedList<>();
Deque<Integer> queue = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int target) {
dfs(root,target);
return list;
}
public void dfs(TreeNode root , int target){
if(root==null){
return;
}
queue.offerLast(root.val);
target-=root.val;
//符合元素的加入到链表
if(root.left==null&&root.right==null&&target==0){
list.add(new LinkedList<>(queue));
}
//递归进行左右子树的查询
dfs(root.left,target);
dfs(root.right,target);
//回溯
queue.pollLast();
}
}
💧二维矩阵问题
☔图像渲染
题目:
有一幅以 m x n 的二维整数数组表示的图画 image ,其中 image[i][j] 表示该图画的像素值大小。
你也被给予三个整数 sr , sc 和 newColor 。你应该从像素 image[sr][sc] 开始对图像进行 上色填充 。
为了完成 上色工作 ,从初始像素开始,记录初始坐标的 上下左右四个方向上 像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应 四个方向上 像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为 newColor 。
最后返回 经过上色渲染后的图像 。
示例:
输入: image = [[1,1,1],[1,1,0],[1,0,1]],sr = 1, sc = 1, newColor = 2
输出: [[2,2,2],[2,2,0],[2,0,1]]
解析:
在图像的正中间,(坐标(sr,sc)=(1,1)),在路径上所有符合条件的像素点的颜色都被更改成2。
注意:
右下角的像素没有更改为2,因为它不是在上下左右四个方向上与初始点相连的像素点。
class Solution {
int[] di = {1, 0, 0, -1};
int[] dj = {0, 1, -1, 0};
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
boolean[][] flag = new boolean[image.length][image[0].length];
dfs(image,flag,sr,sc,image[sr][sc],newColor);
return image;
}
public void dfs(int[][] image,boolean[][] flag,int i,int j,int oldColor,int newColor){
image[i][j] = newColor;
flag[i][j] = true;
//搜索上下左右
for(int k=0;k<4;++k){
int newI=i+di[k];
int newJ=j+dj[k];
//是否越界
if(newI>=image.length||newJ>=image[0].length||newI<0||newJ<0){
continue;
}
//是否需要渲染
if(!flag[newI][newJ]&&image[newI][newJ]==oldColor){
dfs(image,flag,newI,newJ,oldColor,newColor);
}
}
}
}
☔岛屿的周长
题目:
给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。
网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。
示例:
输入:grid = [ [0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0] ]
· 输出:16
解释:
它的周长是上面图片中的 16 个黄色的边
class Solution {
//这道题的思路是,一个坐标,如果他是边界或者相邻的坐标为0,则有一个边
public int islandPerimeter(int[][] grid) {
int m=grid.length;
int n=grid[0].length;
int count=0;
for(int i=0;i<m;++i){
for(int j=0;j<n;++j){
if(grid[i][j]==1){
if(i==0||grid[i-1][j]==0){
count++;
}
if(i==m-1||grid[i+1][j]==0){
count++;
}
if(j==0||grid[i][j-1]==0){
count++;
}
if(j==n-1||grid[i][j+1]==0){
count++;
}
}
}
}
return count;
}
}
☔被围绕的区域
题目:
给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。
示例:
输入:board = [ ["X","X","X","X"] , ["X","O","O","X"] , ["X","X","O","X"] , ["X","O","X","X"] ]
输出:[ ["X","X","X","X"] , ["X","X","X","X"] , ["X","X","X","X"] , ["X","O","X","X"] ]
解释:
被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
class Solution {
//这道题的思路是,从边界出发,把边界岛屿都更改为A,
//再次遍历,将所有O改为X,也就是说将不是边界的岛屿全部改为X
//再将A改回O
public void solve(char[][] board) {
int m = board.length;
int n = board[0].length;
for(int i=0;i<n;++i){
dfs(board,0,i);
dfs(board,m-1,i);
}
for(int i=1;i<m-1;++i){
dfs(board,i,0);
dfs(board,i,n-1);
}
for(int i=0;i<m;++i){
for(int j=0;j<n;++j){
if(board[i][j]=='O'){
board[i][j]='X';
}
if(board[i][j]=='A'){
board[i][j]='O';
}
}
}
}
public void dfs(char[][] board,int i,int j){
int m = board.length;
int n = board[0].length;
if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] != 'O') {
return;
}
board[i][j] = 'A';
dfs(board, i + 1, j);
dfs(board, i - 1, j);
dfs(board, i, j + 1);
dfs(board, i, j - 1);
}
}
☔岛屿数量
题目:
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例:
输入:grid = [ ["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"] ]
输出:3
class Solution {
//思路:找到的岛屿就利用递归将整个岛屿改成2,每找到一个岛屿计数一次
public int numIslands(char[][] grid) {
int n = grid[0].length;
int m = grid.length;
int count=0;
for(int i=0;i<m;++i){
for(int j=0;j<n;++j){
if(grid[i][j]=='1'){
count++;
dfs(grid,i,j);
}
}
}
return count;
}
public void dfs(char[][] grid,int i,int j){
int n = grid[0].length;
int m = grid.length;
if(i>=m||j>=n||i<0||j<0||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);
}
}
☔岛屿的最大面积
题目:
给你一个大小为 m x n 的二进制矩阵 grid 。
岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
岛屿的面积是岛上值为 1 的单元格的数目。
计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。
示例:
输入:grid = [ [0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0] ]
输出:6
解释:
答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。
class Solution {
//思路:将找到的岛屿利用递归操作全部改成0,顺便记录岛屿的最大面积
public int maxAreaOfIsland(int[][] grid) {
int n = grid[0].length;
int m = grid.length;
int max=0;
for(int i=0;i<m;++i){
for(int j=0;j<n;++j){
if(grid[i][j]==1){
max=Math.max(dfs(grid,i,j),max);
}
}
}
return max;
}
public int dfs(int[][] grid,int i,int j){
int n = grid[0].length;
int m = grid.length;
if(i>=m||j>=n||i<0||j<0||grid[i][j]!=1){
return 0;
}
grid[i][j]=0;
int count=1;
count+=dfs(grid,i+1,j);
count+=dfs(grid,i-1,j);
count+=dfs(grid,i,j-1);
count+=dfs(grid,i,j+1);
return count;
}
}
☔N皇后
题目:
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例:
输入:n = 4
输出:[ [".Q..","...Q","Q...","..Q."] , ["..Q.","Q...","...Q",".Q.."] ]
解释:
如上图所示,4 皇后问题存在两个不同的解法。
class pair{
public int x;
public int y;
public pair(int x, int y){
this.x = x;
this.y = y;
}
}
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<pair>> solutions = new ArrayList<>();
List<pair> solution = new ArrayList<>();
nQueensBacktrack(solutions, solution, 0, n);
return transResult(solutions, n);
}
void nQueensBacktrack(List<List<pair>> solutions,List<pair> solution, int curRow, int n) {
if (curRow == n){
List<pair> newS = new ArrayList<>();
for(pair p : solution){
newS.add(p);
}
solutions.add(newS);
}
for (int col = 0; col < n; ++col) {
if (isValid(solution, curRow, col)) {
solution.add(new pair(curRow, col));
nQueensBacktrack(solutions, solution, curRow + 1, n);
solution.remove(solution.size() - 1);
}
}
}
boolean isValid(List<pair> solution, int row, int col) {
for (pair i : solution){
if (i.y == col || i.x + i.y == row + col|| i.x - i.y == row - col){
return false;
}
}
return true;
}
List<List<String>> transResult(List<List<pair>> solutions, int n) {
List<String> tmp = new ArrayList<>();
List<List<String>> ret = new ArrayList<>();
for (List<pair> solution : solutions) {
List<StringBuilder> solutionString = new ArrayList<>();
for(int i = 0; i < n; ++i){
StringBuilder sb = new StringBuilder();
for(int j = 0; j < n; ++j){
sb.append('.');
}
solutionString.add(sb);
}
for (pair i : solution) {
solutionString.get(i.x).setCharAt(i.y, 'Q');
}
List<String> curRet = new ArrayList<>();
for(StringBuilder sb : solutionString){
curRet.add(sb.toString());
}
ret.add(curRet);
}
return ret;
}
}
☔单词搜索
题目:
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
输入:board = [ ["A","B","C","E"],["S","F","C","S"],["A","D","E","E"] ], word = "ABCCED"
输出:true
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for(int i=0;i<board.length;++i){
for(int j=0;j<board[0].length;++j){
if(dfs(board,words,i,j,0)){
return true;
}
}
}
return false;
}
public boolean dfs(char[][] board,char[] words,int i,int j,int k){
if(i<0||j<0||i>board.length-1||j>board[0].length-1||board[i][j]!=words[k]){
return false;
}
if(k==words.length-1){
return true;
}
board[i][j]='\0';
boolean res = dfs(board,words,i+1,j,k+1)||dfs(board,words,i,j-1,k+1)||dfs(board,words,i,j+1,k+1)||dfs(board,words,i-1,j,k+1);
board[i][j]=words[k];
return res;
}
}
🌂广度优先搜索
💧转盘锁问题
☔打开转盘锁
题目:
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0','0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。
列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。
示例:
输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。
class Solution {
public int openLock(String[] deadends, String target) {
HashSet<String> dict = new HashSet<>();
HashSet<String> book = new HashSet<>();
Queue<String> queue = new LinkedList<>();
int step = 0;
//设置哈希集合存放禁止的数字
for(String s : deadends){
dict.add(s);
}
//如果禁止数字里有起始数字,那就直接返回-1
if(dict.contains("0000")){
return -1;
}
//模板
book.add("0000");
queue.offer("0000");
while(!queue.isEmpty()){
int size = queue.size();
while(size--!=0){
String cur = queue.poll();
//找到了直接返回
if(cur.equals(target)){
return step;
}
for(int i=0;i<4;++i){
//分别有+操作和-操作
//需要注意的是回环
//如果当前是9,0时,向前,向后拨动需要变成最小最大
StringBuffer s1 = new StringBuffer(cur);
char ch1 = s1.charAt(i);
if(ch1=='9'){
ch1='0';
}else{
ch1++;
}
s1.setCharAt(i,ch1);
//没在禁止里并且没操作过就入队
if(!book.contains(s1.toString())&&!dict.contains(s1.toString())){
queue.offer(s1.toString());
book.add(s1.toString());
}
//另一种操作
StringBuffer s2 = new StringBuffer(cur);
char ch2 = s2.charAt(i);
if(ch2=='0'){
ch2='9';
}else{
ch2--;
}
s2.setCharAt(i,ch2);
if(!book.contains(s2.toString())&&!dict.contains(s2.toString())){
queue.offer(s2.toString());
book.add(s2.toString());
}
}
}
//计数
step++;
}
//完事之后都没找到,那就返回-1
return -1;
}
}
☔单词接龙
题目:
字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk:
每一对相邻的单词只差一个字母。
对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。
sk == endWord
给你两个单词 beginWord 和 endWord 和一个字典 wordList ,返回 从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0 。
示例:
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:
一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Queue<String> queue = new LinkedList<>();
int step = 1;
//找过的放在这里
HashSet<String> book = new HashSet<>();
//所有的放在这里
HashSet<String> dict = new HashSet<>();
//将给的单词全部加到dict里面去
for(String s : wordList){
dict.add(s);
}
queue.offer(beginWord);
book.add(beginWord);
while(!queue.isEmpty()){
int size = queue.size();
while(size--!=0){
String cur = queue.poll();
//如果当前字符等于结尾字符那就说明找到了
if(cur.equals(endWord)){
return step;
}
//修改单词的某一个字符
for(int i=0;i<cur.length();++i){
StringBuffer s = new StringBuffer(cur);
for(char ch = 'a';ch<='z';++ch){
s.setCharAt(i,ch);
//判断新的单词是不是在dict中,是不是不在book中
if(dict.contains(s.toString())&&!book.contains(s.toString())){
queue.offer(s.toString());
book.add(s.toString());
}
}
}
}
step++;
}
return 0;
}
}
☔最小基因变化
题目:
基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 'A'、'C'、'G' 和 'T' 之一。
假设我们需要调查从基因序列 start 变为 end 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。
例如,"AACCGGTT" --> "AACCGGTA" 就是一次基因变化。
另有一个基因库 bank 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。
给你两个基因序列 start 和 end ,以及一个基因库 bank ,请你找出并返回能够使 start 变化为 end 所需的最少变化次数。如果无法完成此基因变化,返回 -1 。
注意:
起始基因序列 start 默认是有效的,但是它并不一定会出现在基因库中。
示例:
输入:start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"]
输出:1
class Solution {
public int minMutation(String start, String end, String[] bank) {
Queue<String> queue = new LinkedList<>();
int step = 0;
//找过的放在这里
HashSet<String> book = new HashSet<>();
//所有的放在这里
HashSet<String> dict = new HashSet<>();
//将给的单词全部加到dict里面去
for(String s : bank){
dict.add(s);
}
queue.offer(start);
book.add(start);
while(!queue.isEmpty()){
int size = queue.size();
while(size--!=0){
String cur = queue.poll();
//如果当前字符等于结尾字符那就说明找到了
if(cur.equals(end)){
return step;
}
//修改单词的某一个字符
for(int i=0;i<cur.length();++i){
StringBuffer s = new StringBuffer(cur);
for(char ch = 'A';ch<='Z';++ch){
s.setCharAt(i,ch);
//判断新的单词是不是在dict中,是不是不在book中
if(dict.contains(s.toString())&&!book.contains(s.toString())){
queue.offer(s.toString());
book.add(s.toString());
}
}
}
}
step++;
}
return -1;
}
}
💧树形结构问题
☔N叉树的层序遍历
题目:
给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔。
示例:
输入:root = [1,null,3,2,4,null,5,6]
输出:[[1],[3,2,4],[5,6]]
/*
// Definition for a Node.
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
//经典的广度优先了,没什么好说的,这就是典型的模板
public List<List<Integer>> levelOrder(Node root) {
if (root == null) {
return new ArrayList<>();
}
List<List<Integer>> ans = new ArrayList<>();
Queue<Node> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int cnt = queue.size();
List<Integer> level = new ArrayList<Integer>();
for (int i = 0; i < cnt; ++i) {
Node cur = queue.poll();
level.add(cur.val);
for (Node child : cur.children) {
queue.offer(child);
}
}
ans.add(level);
}
return ans;
}
}
💧二维矩阵问题
☔腐烂的橘子
题目:
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
值 0 代表空单元格;值 1 代表新鲜橘子;值 2 代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
示例:
输入:grid = [[2,1,1],[1,1,0],[0,1,1]]
输出:4
//橘子的坐标
class Pair{
int x;
int y;
public Pair(int x,int y){
this.x=x;
this.y=y;
}
}
class Solution {
public int orangesRotting(int[][] grid) {
int n = grid.length;
int m = grid[0].length;
Queue<Pair> queue = new LinkedList<>();
//刚开始就腐烂的橘子,全部入队
for(int i=0;i<n;++i){
for(int j=0;j<m;++j){
if(grid[i][j]==2){
queue.offer(new Pair(i,j));
}
}
}
int[][] nextP = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } };
int count=0;
//模板
while(!queue.isEmpty()){
int size = queue.size();
boolean flag=false;
while(size--!=0){
Pair cur = queue.poll();
for(int i =0;i<4;++i){
int nx = cur.x+nextP[i][0];
int ny = cur.y+nextP[i][1];
if(nx>n-1||ny>m-1||nx<0||ny<0){
continue;
}
if(grid[nx][ny]==1){
grid[nx][ny]=2;
flag=true;
queue.offer(new Pair(nx,ny));
}
}
}
//有新橘子被腐烂,就计数
if(flag){
count++;
}
}
//还有没腐烂的,就返回-1
for(int i=0;i<n;++i){
for(int j=0;j<m;++j){
if(grid[i][j]==1){
return -1;
}
}
}
return count;
}
}
😀总结
许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
🌈深度优先算法模板
dfs(){
判断边界
for(){
尝试当下的每一种可能
确定一种可能之后,继续下一步dfs
撤销操作
}
}
🌈广度优先算法模板
bfs(){
创建一个队列,入队根节点
while(队列不为空){
int size = 这一层长度
while(这一层){
找相关节点
符合条件,相关节点入队
}
}
}