力扣刷题记录-回溯算法相关题目
首先介绍一下回溯算法
回溯通常在递归函数中体现,本质也是一种暴力的搜索方法,但可以解决一些用for循环暴力解决不了的问题,其应用有:
1.组合问题:
例:1 2 3 4这些数中找出组合为2的组合,有12(或21也行),13,14,23,24,34这些;
2.切割问题:
例:给一个字符串,要得出指定结果,求解有几种切割方式。
3.子集问题:
例:1 2 3 4这些数的子集:1 2 3 4 12 13 14 23 24 34 123 124 234 1234。
4.排列问题(有顺序要求):如12和21是不一样的
5.棋盘问题:N皇后,解数独
回溯法可以抽象成n叉树,树的深度就是递归的过程,树的宽度就是处理的集合的大小;对于子集问题是针对每个结点收集结果,组合、切割、排列等在叶子结点收集结果,如图(图片来自代码随想录公众号)
其函数一般没有返回值,参数一般较多,可以在需要用到的时候添加;其代码一般形式如下:
void backTracking(参数){if(终止条件){收集结果;return;}for(横向遍历集合元素集){处理结点;backTracking(参数);//回溯撤销处理;//相当于回退到结点的上一层}
}
组合问题
力扣 77. 组合
这个问题可以抽象为下面的树形结构:
n相当于树的宽度,k就相当于树的深度;这棵树上的叶子结点就是我们需要求的结果集合。
针对这题的结果的特征,需要定义两个全局变量list去存放结果:
List<List<Integer>> res=new ArrayList<>();//存放最终结果
List<Integer> path= new ArrayList<>();//记录搜索过程
public void backTrack(int n,int k,int startIndex){}
if(path.size()==k){res.add(new ArrayList<>(path));return;
}
3.单层搜索过程
在for循环中,从i=startIndex开始,遍历到n为止,每次将当前的i加入path,然后调用回溯函数自身,最后在这次中撤销本次加入的i;
for(int i=startIndex;i<=n;i++){path.add(i);backTrack(n,k,i+1);path.remove(path.size()-1);
}
class Solution {List<List<Integer>> res=new ArrayList<>();List<Integer> path= new ArrayList<>();public List<List<Integer>> combine(int n, int k) {backTrack(n,k,1);return res;}public void backTrack(int n,int k,int startIndex){if(path.size()==k){res.add(new ArrayList<>(path));return;}for(int i=startIndex;i<=n;i++){path.add(i);backTrack(n,k,i+1);path.remove(path.size()-1);}}
}
所以i要小于等于n-(k-path.size())+1。
改进代码如下:
class Solution {List<List<Integer>> res=new ArrayList<>();List<Integer> path= new ArrayList<>();public List<List<Integer>> combine(int n, int k) {backTrack(n,k,1);return res;}public void backTrack(int n,int k,int startIndex){if(path.size()==k){res.add(new ArrayList<>(path));return;}for(int i=startIndex;i<=n-(k-path.size())+1;i++){path.add(i);backTrack(n,k,i+1);path.remove(path.size()-1);}}
}
216. 组合总和 III
之所以先做这题而不是这题的两道前置题,是因为其思想、方法和77非常相似。
在回溯函数的终止条件里,不能当path.size()==k时直接将新path加入res中,因为这题还需要符合在抽象二叉树中搜索路径的总和等于n的条件;
if(path.size()==k){if(sum==n)res.add(new ArrayList<>(path));return;
}
在对宽度进行遍历的时候(for循环里),i的限制也应该是<=9;
for(int i=startIndex;i<=9;i++)
完整代码:
里面的sum参数其实可以省略,只要在每层回溯的时候,将回溯函数参数n减去这次遍历到的值,最后终止条件判断n是否等于0即可;
class Solution {List<List<Integer>>res=new ArrayList<>();List<Integer>path=new ArrayList<>();public List<List<Integer>> combinationSum3(int k, int n) {backTrack(k,n,1,0);return res;}public void backTrack(int k,int n,int startIndex,int sum){if(path.size()==k){if(sum==n)res.add(new ArrayList<>(path));return;}for(int i=startIndex;i<=9;i++){path.add(i);sum+=i;backTrack(k,n,i+1,sum);path.remove(path.size()-1);sum-=i;}}
}
class Solution {List<List<Integer>>res=new ArrayList<>();List<Integer>path=new ArrayList<>();public List<List<Integer>> combinationSum3(int k, int n) {backTrack(k,n,1,0);return res;}public void backTrack(int k,int n,int startIndex,int sum){if(sum>n)return;//剪枝if(path.size()==k){if(sum==n)res.add(new ArrayList<>(path));return;}for(int i=startIndex;i<=9-(k-path.size())+1;i++){path.add(i);sum+=i;backTrack(k,n,i+1,sum);path.remove(path.size()-1);sum-=i;}}
}
力扣 39. 组合总和
class Solution {List<List<Integer>>res=new ArrayList<>();List<Integer>path=new ArrayList<>();public List<List<Integer>> combinationSum(int[] candidates, int target) {Arrays.sort(candidates);//先对其排序,方便后面剪枝backTrack(candidates,target,0);return res;}public void backTrack(int[]candidates,int target,int startIndex){if(target==0){res.add(new ArrayList<>(path));return;}for(int i=startIndex;i<candidates.length;i++){//剪枝优化,路径总和超出target,终止遍历(配合主函数的排序使用)if(target-candidates[i]<0)break;path.add(candidates[i]);backTrack(candidates,target-candidates[i],i);//不用i+1(可以重复选一个数)path.remove(path.size()-1);}}
}
力扣 40. 组合总和 II
class Solution {List<List<Integer>>res=new ArrayList<>();List<Integer>path=new ArrayList<>();public List<List<Integer>> combinationSum2(int[] candidates, int target) {Arrays.sort(candidates);//先对其排序,方便去重backTrack(candidates,target,0);return res;}public void backTrack(int[]candidates,int target,int startIndex){if(target==0){res.add(new ArrayList<>(path));return;}for(int i=startIndex;i<candidates.length;i++){if(target-candidates[i]<0)break;//不符合target,及时退出for循环//i>startIndex,保证i-1不会小于for循环区间if(i>startIndex&&candidates[i]==candidates[i-1])continue;path.add(candidates[i]);backTrack(candidates,target-candidates[i],i+1);path.remove(path.size()-1);}}
}
力扣 17. 电话号码的字母组合
这题主要需要解决两个问题:
1.数字和字母的映射关系;
2.映射之后如何求出数字所代表的所有的字母组合;
对于映射关系,可以用一个String数组来存储0-9所对应的字符串,其中索引0、1没有对应的值,为空。numMap[i]就是数字映射出的字符串:
String[] numMap={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
class Solution {List<String>res=new ArrayList<>();String[] numMap={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};StringBuilder str=new StringBuilder();public List<String> letterCombinations(String digits) {if(digits.length()==0)return res;backTrack(digits,0);return res;}public void backTrack(String digits,int index){//当遍历完数字字符串digits一次,就记录一次遍历的结果,再回溯if(index==digits.length()){res.add(str.toString());return;}//numStr代表当前数字对应的字符串,使代码简洁一点String numStr=numMap[digits.charAt(index)-'0'];for(int i=0;i<numStr.length();i++){str.append(numStr.charAt(i));backTrack(digits,index+1);//处理数字字符串digits的下一个索引str.deleteCharAt(str.length()-1);//回退}}
}
分割问题
力扣 131. 分割回文串
这题主要需要解决两点:
1.如何分割字符串;
2.判断分割的字符串是否为回文串;
与组合问题类似,需要全局变量res记录所有分割方案;全局变量path记录单个分割方案;
List<List<String>>res=new ArrayList<>();
List<String>path=new ArrayList<>();
对于这个问题使用回溯进行分割:
1.确定参数:
需要遍历字符串s,还需要记录每次递归时的遍历的起始位置startIndex;
public void backTrack(String s,int startIndex){}
2.递归函数终止条件:
当startIndex走到字符串s的末尾时,就会产生一个切割方案,此时需要将path加入res中,并且返回上一层递归;
if(startIndex==s.length()){res.add(new ArrayList<>(path));return;
}
for(int i=startIndex;i<s.length();i++){if(isPalindrome(s,startIndex,i)){path.add(s.substring(startIndex,i+1));}else continue;backTrack(s,i+1);path.remove(path.size()-1);
}
public boolean isPalindrome(String s,int start,int end){for(int i=start,j=end;i<j;i++,j--){if(s.charAt(i)!=s.charAt(j))return false;}return true;
}
class Solution {List<List<String>>res=new ArrayList<>();List<String>path=new ArrayList<>();public List<List<String>> partition(String s) {backTrack(s,0);return res;}public void backTrack(String s,int startIndex){if(startIndex==s.length()){res.add(new ArrayList<>(path));return;}for(int i=startIndex;i<s.length();i++){if(isPalindrome(s,startIndex,i)){path.add(s.substring(startIndex,i+1));}else continue;backTrack(s,i+1);path.remove(path.size()-1);}}public boolean isPalindrome(String s,int start,int end){for(int i=start,j=end;i<j;i++,j--){if(s.charAt(i)!=s.charAt(j))return false;}return true;}
}
力扣 93. 复原 IP 地址
class Solution {List<String> res=new ArrayList<>();//记录最终结果List<String> path=new ArrayList<>();//记录每次分割好的数字子串public List<String> restoreIpAddresses(String s) {if(s.length()>12)return res;//s长度超过12不可能有合法分割方案backTrack(s,0);return res;}public void backTrack(String s,int startIndex){//当path已经有4个地址,并且分割线遍历字符串s结束if(path.size()==4&&startIndex==s.length()){res.add(pathToRes(new ArrayList<>(path)));return;}for(int i=startIndex;i<s.length();i++){//剪枝,在还没添加新的子串进入path前,已经有四个合理ip了//说明数字多了,直接回溯到上层if(path.size()==4)return;String str=s.substring(startIndex,i+1);//若数字子串符合ip地址格式,加入pathif(isIpAddress(str))path.add(str);else break;//不合法,则后面的也不合法,直接退出这层横向遍历backTrack(s,i+1);path.remove(path.size()-1);}}//将分割好的数字字符子串中间用"."拼接,转化成题目要求格式public String pathToRes(List<String> path){StringBuilder sb=new StringBuilder();for(int i=0;i<path.size();i++){sb.append(path.get(i));//最后一个子串后面不用加"."if(i!=path.size()-1)sb.append(".");}return sb.toString();//最后还需要转化成String类型}//判断截取的数字子串是否合法public boolean isIpAddress(String str){//当子串第一个数字为0且子串不止一个数字的时候,非法if(str.charAt(0)=='0'&&str.length()!=1)return false;//str转数字与255比较,不能超过255if(Integer.valueOf(str)>255)return false;return true;}
}
子集问题
力扣 78. 子集
子集问题和前面的组合问题、分割问题不同之处在于:子集问题需要收集抽象二叉树搜索过程中的每个结点,而组合、分割问题是收集最后的叶子结点;
class Solution {List<List<Integer>>res=new ArrayList<>();List<Integer>path=new ArrayList<>();public List<List<Integer>> subsets(int[] nums) {backTrack(nums,0);return res;}public void backTrack(int[] nums,int startIndex){//收集子集,要在终止条件之前,否则会漏了自己res.add(new ArrayList<>(path));//if(startIndex>=nums.length)return;//终止条件可以不加for(int i=startIndex;i<nums.length;i++){path.add(nums[i]);backTrack(nums,i+1);path.remove(path.size()-1);}}
}
力扣 90. 子集 II
这题其实是力扣 78. 子集和力扣 40. 组合总和 II的结合,数组nums中有重复的元素,因此也会带来求子集的时候会有重复的子集出现的问题,因此需要去除重复子集。
首先对nums数组进行排序,这样方便后续遍历到一样的数字的时候可以去重;
递归终止条件设置为startIndex=nums.length,代表遍历到了nums的末尾,需要回退到上一层;
class Solution {List<List<Integer>>res=new ArrayList<>();List<Integer>path=new ArrayList<>();public List<List<Integer>> subsetsWithDup(int[] nums) {Arrays.sort(nums);//注意先将数组排序,方便去重backTrack(nums,0);return res;}public void backTrack(int[] nums,int startIndex){//要在终止条件之前加入res,否则可能漏掉自身res.add(new ArrayList<>(path));//终止条件,遍历到nums末尾if(startIndex==nums.length)return;for(int i=startIndex;i<nums.length;i++){//前后数字一样的时候进行去重,直接跳过当前数字//不跳过的话可能走出一条和前一个数字一样的数字路径if(i>startIndex&&nums[i]==nums[i-1])continue;path.add(nums[i]);backTrack(nums,i+1);path.remove(path.size()-1);}}
}
力扣 491. 递增子序列
这题和 力扣 90. 子集 II很像,但是细节之处又有很多不同。
首先,注意不能改变数组元素的相对位置,即不能先对数组排序再求递增子序列,因为本题求的是在原数组元素相对位置上找递增的子序列;所以这题不能像之前一样先对数组排序再遍历进行去重;
其次,相等的数字也被视为递增的一种情况,如{1,2,2}也是递增的;
class Solution {List<List<Integer>>res=new ArrayList<>();;List<Integer>path=new ArrayList<>();public List<List<Integer>> findSubsequences(int[] nums) {backTrack(nums,0);return res;}public void backTrack(int[] nums,int startIndex){if(path.size()>1)res.add(new ArrayList<>(path));//进入for循环前设置numMap,这样只会记录本层的元素是否使用//新的一层会重新定义numMapint[] numMap=new int[201];元素大小范围在[-100,100]for(int i=startIndex;i<nums.length;i++){//在path非空前提下,当前遍历元素小于path最后一个元素,说明非递增//或这个元素在本层之前已经使用过了,这两种情况都要跳过if(!path.isEmpty()&&nums[i]<path.get(path.size()-1)||numMap[nums[i]+100]==1)continue;//标记元素已经使用过numMap[nums[i]+100]=1;path.add(nums[i]);backTrack(nums,i+1);path.remove(path.size()-1);}}
}
排列问题
排列问题相比于组合问题不同在于,所求出的元素集就算相同,但只要排列顺序不一样,就是两个不同的排列。
力扣 46. 全排列
class Solution {List<List<Integer>>res=new ArrayList<>();List<Integer>path=new ArrayList<>();public List<List<Integer>> permute(int[] nums) {//标记前面的元素是否已经在path中,不在为false,在为trueboolean[] used=new boolean[nums.length];backTrack(nums,used);return res;}public void backTrack(int[] nums,boolean[] used){//收集叶子结点if(path.size()==nums.length){res.add(new ArrayList<>(path));return;}for(int i=0;i<nums.length;i++){//若path已经有了该元素,跳过,去重if(used[i])continue;used[i]=true;path.add(nums[i]);backTrack(nums,used);path.remove(path.size()-1);used[i]=false;}}
}
力扣 47. 全排列 II
需要先对nums数组进行排序,这样方便下一层递归中,for循环从i=0开始重新遍历的时候,比较前后nums的元素相同的时候,根据是否访问过(uesd[i-1])进行去重;
因为是全排列,所以每次递归中,for循环需要从i=0开始遍历,这样才能保证最后叶子结点的元素数量等于nums.length;
class Solution {List<List<Integer>>res=new ArrayList<>();List<Integer>path=new ArrayList<>();boolean[] used;//标记数组public List<List<Integer>> permuteUnique(int[] nums) {used=new boolean[nums.length];Arrays.sort(nums);backTrack(nums);return res;}public void backTrack(int[] nums){//到达叶子结点if(path.size()==nums.length){res.add(new ArrayList<>(path));return;}//每次递归遍历从i=0,开始,借助used数组去重for(int i=0;i<nums.length;i++){//uesd[i-1]=true,说明同一树枝的nums[i-1]被使用过//used[i-1]=false,说明同一树层的nums[i-1]被使用过//在nums排序之后的nums[i]==nums[i-1],若同一树层的nums[i-1]未被使用过//则直接跳过当前的nums[i],否则进入下一层递归for循环,i从0开始//又会把前面相同的元素加入path,出现重复(如1 1 2)if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false)continue;if(used[i]==false){used[i]=true;path.add(nums[i]);backTrack(nums);path.remove(path.size()-1);used[i]=false;}}}}
//树层去重
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false)continue;
//树枝去重
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==true)continue;
②树枝去重:
相当于在一个树枝上,碰到上下一样的数字,前一个数字标志为使用过,若不跳过当前数字,也会出现重复读取的情况;
从图中可以看出,树层去重效率更高,因为它可以在递归一开始的地方就进行剪枝,可以更快剪去更多树枝;
子集、组合、排列问题复杂度
·子集问题:
时间复杂度O(n2^n),因为每个元素状态就是取或者不取,这一块时间复杂度就是O(2的n次方),构造出来的每一组子集都要填进res数组,需要O(n),最终需要O(n2的n次方);
空间复杂度:O(n),递归深度为n,栈空间为O(n),每一层递归所用空间为常数级别,而res和path一般是全局变量,就算放在参数传递,也只是传引用,没有新申请内存空间,最终空间复杂度为O(n);
·组合问题:
时间复杂度O(n*2^n),组合问题其实也是一种子集问题,它最坏情况也不会超过子集问题时间复杂度;
空间复杂度:O(n),同子集问题;
·排列问题:
时间复杂度:O(n!),排列的树形图可以得到,第一层结点数n,第二层每一个分支都延伸n-1个分支,再往下n-2个分支……一直到叶子结点一共就是nn-1n-2*····*1=n!;
空间复杂度:同理子集问题;
棋盘问题
力扣 51. N 皇后
在开始遍历棋盘之前,首先要对棋盘进行初始化,将二维字符数组赋“.”字符:
char[][] chessBoard=new char[n][n];
for(char[] c:chessBoard)Arrays.fill(c,'.');
char[][] chessBoard=new char[n][n];
char[] c=new char[2n];
Arrays.fill(c,'.');
Arrays.fill(chessBoard,c);
public void backTrack(int n,int row,char[][] chessBoard){}
if(row==n){//res是List<List<String>>类型的,无法直接加char[][]类型的棋盘//需要将chessBoard转化成List<String>类型res.add(charToList(chessBoard));return;
}
for(int col=0;col<n;col++){if(isValid(row,col,n,chessBoard)){chessBoard[row][col]='Q';backTrack(n,row+1,chessBoard);chessBoard[row][col]='.';}
}
最后,为了完成回溯算法,需要写两个函数进行辅助:
①判断当前位置是否符合N皇后要求的函数:
不用检查行数是否符合要求,因为每次for循环都是在一行内遍历,每一行选择出一个位置后就会进入下一层递归;
public boolean isValid(int row,int col,int n,char[][] chessBoard){//检查同一列上是否有皇后(行数--)for(int i=0;i<row;i++)if(chessBoard[i][col]=='Q')return false;//检查左边45°斜线for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--)if(chessBoard[i][j]=='Q')return false;//检查右边45°for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++)if(chessBoard[i][j]=='Q')return false;return true;
}
②将chessBoard二维字符数组类型的棋盘转化为List< String>类型:
//转换chessBoard为List<String>类型
public List charToList(char[][] chessBoard){List<String>list=new ArrayList<>();//用c[]遍历chessBoard的每一行for(char[] c:chessBoard)//copyValueOf可以将字符数组转成字符串list.add(String.copyValueOf(c));return list;
}
class Solution {List<List<String>>res=new ArrayList<>();public List<List<String>> solveNQueens(int n) {//chessBoard模拟棋盘,先将其初始化为全“.”char[][] chessBoard=new char[n][n];for(char[] c:chessBoard)Arrays.fill(c,'.');backTrack(n,0,chessBoard);return res;}public void backTrack(int n,int row,char[][] chessBoard){//当行数遍历到最后一行(从0-n-1共n行),说明棋盘遍历结束if(row==n){//res是List<List<String>>类型的,无法直接加char[][]类型的棋盘//需要将chessBoard转化成List<String>类型res.add(charToList(chessBoard));return;}for(int col=0;col<n;col++){if(isValid(row,col,n,chessBoard)){chessBoard[row][col]='Q';backTrack(n,row+1,chessBoard);chessBoard[row][col]='.';}}}//转换chessBoard为List<String>类型public List charToList(char[][] chessBoard){List<String>list=new ArrayList<>();for(char[] c:chessBoard)list.add(String.copyValueOf(c));return list;}public boolean isValid(int row,int col,int n,char[][] chessBoard){//检查同一列上是否有皇后(行数--)for(int i=0;i<row;i++)if(chessBoard[i][col]=='Q')return false;//检查左边45°斜线for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--)if(chessBoard[i][j]=='Q')return false;//检查右边45°for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++)if(chessBoard[i][j]=='Q')return false;return true;}
}
37. 解数独
private boolean solveSudokuHelper(char[][] board){}
递归终止条件
正常的递归回溯需要在走到最下面的叶子结点处,记录结果并且返回上一层;但是9宫格问题不一样,题目规定只会有一个唯一解,因此能走到最下面,即全部遍历完成的就是正确答案,这时候直接返回true即可,就不需要终止条件了;单层递归遍历逻辑
就是两层for循环遍历行和列,第三层for循环遍历0-9数字字符;
private boolean solveSudokuHelper(char[][] board){//「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」for (int i = 0; i < 9; i++){ // 遍历行for (int j = 0; j < 9; j++){ // 遍历列if (board[i][j] != '.'){ // 跳过原始数字continue;}for (char k = '1'; k <= '9'; k++){ // (i, j) 这个位置放k是否合适//只有当数字符合要求,才填入9宫格if (isValidSudoku(i, j, k, board)){board[i][j] = k;if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回return true;}board[i][j] = '.';}}// 9个数都试完了,都不行,那么就返回falsereturn false;// 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!// 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」}}// 遍历完没有返回false,说明找到了合适棋盘位置了return true;
}
除此2之外,还需要判断填上当前数字后是否符合数独要求:
/*** 判断棋盘是否合法有如下三个维度:* 同行是否重复* 同列是否重复* 9宫格里是否重复*/
private boolean isValidSudoku(int row, int col, char val, char[][] board){// 同行是否重复(列数变化)for (int i = 0; i < 9; i++)if (board[row][i] == val)return false;// 同列是否重复(行数变化)for (int j = 0; j < 9; j++)if (board[j][col] == val)return false;// 9宫格里是否重复//9宫格行起始:0,3,6;列起始:0,3,6//因此只要除3再乘就可以获取起始行列int startRow = (row / 3) * 3;int startCol = (col / 3) * 3;for (int i = startRow; i < startRow + 3; i++)for (int j = startCol; j < startCol + 3; j++)if (board[i][j] == val)return false;//如果都符合,那么可以填入return true;
}
完整代码如下:
class Solution {public void solveSudoku(char[][] board) {solveSudokuHelper(board);//不需要返回九宫格}private boolean solveSudokuHelper(char[][] board){//「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」for (int i = 0; i < 9; i++){ // 遍历行for (int j = 0; j < 9; j++){ // 遍历列if (board[i][j] != '.'){ // 跳过原始数字continue;}for (char k = '1'; k <= '9'; k++){ // (i, j) 这个位置放k是否合适//只有当数字符合要求,才填入9宫格if (isValidSudoku(i, j, k, board)){board[i][j] = k;if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回return true;}board[i][j] = '.';}}// 9个数都试完了,都不行,那么就返回falsereturn false;// 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!// 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」}}// 遍历完没有返回false,说明找到了合适棋盘位置了return true;}/*** 判断棋盘是否合法有如下三个维度:* 同行是否重复* 同列是否重复* 9宫格里是否重复*/private boolean isValidSudoku(int row, int col, char val, char[][] board){// 同行是否重复(列数变化)for (int i = 0; i < 9; i++)if (board[row][i] == val)return false;// 同列是否重复(行数变化)for (int j = 0; j < 9; j++)if (board[j][col] == val)return false;// 9宫格里是否重复//9宫格行起始:0,3,6;列起始:0,3,6//因此只要除3再乘就可以获取起始行列int startRow = (row / 3) * 3;int startCol = (col / 3) * 3;for (int i = startRow; i < startRow + 3; i++)for (int j = startCol; j < startCol + 3; j++)if (board[i][j] == val)return false;//如果都符合,那么可以填入return true;}
}
其他问题
力扣 332. 重新安排行程
题目理解:
给你一沓机票,用它去飞(遍历)图中的城市(节点),机票要用光(遍历完所有的边),返回出访问城市的路径,且机票不能重复用(遍历过的边要拆掉)。
题意说,用完机票所走的路径一定存在,找出一条即可。没找到用完机票的路径就是:你困在一个城市,手里有不合适的机票,用不出去。对应到图就是,到了一个点,没有邻接点能访问,但你还有边没遍历。
class Solution {//path记录路线,res存所有路线List<String> path = new ArrayList<>();List<List<String>> res = new ArrayList<>();//used数组用于标记同一树枝不能重复使用!即不能重复使用一张票boolean[] used = new boolean[301];boolean find;public List<String> findItinerary(List<List<String>> tickets) {//先按字典序从小到大排列降落地//重写sort规则tickets.sort((o1, o2) -> o1.get(1).compareTo(o2.get(1)));path.add("JFK");backTracking(tickets, "JFK");return res.get(0);}void backTracking(List<List<String>> tickets, String outset) {//算个小剪枝吧,找到一条就行if (find) {return;}//因为这些航班肯定会有一条路线是正确的//所以我们加入path的size如果等于tickets.size()+1说明我们找到路线了if (path.size() == tickets.size() + 1) {find = true;res.add(new ArrayList<>(path));return;}for (int i = 0; i < tickets.size(); i++) {//如果出发地和上一个的降落地相同 并且 同一条路线中没有重复使用一张票if(tickets.get(i).get(0).equals(outset) && !used[i]){//标记该票已经使用过used[i]= true;path.add(tickets.get(i).get(1));//把现在的降落地加入递归函数//即把当前机票终点作为下一站的起点backTracking(tickets, tickets.get(i).get(1));//回溯! 该票标记为未使用 路线中移除该票used[i]=false;path.remove(path.size()-1);}}}
}
class Solution {private LinkedList<String> res;private Map<String, Map<String, Integer>> map;public List<String> findItinerary(List<List<String>> tickets) {//<起点,<终点,航行剩余次数>>,//“航行剩余次数”大于零,说明目的地还可以飞,如果如果“航行剩余次数”等于零说明目的地不能飞了//map是一张全局表,统计所有的机票信息map = new HashMap<String, Map<String, Integer>>();res = new LinkedList<>();// 将票放入全局map中。// for循环中只有两个处理步骤// 1.确定map的value值,也就是<终点,计数值>,程序里用temp接收// -逻辑1:机票起点相同,["JFK","SFO"],["JFK","ATL"] // -逻辑2:机票起点不同,["JFK","ATL"],["SFO","ATL"]// 2.将起点和<终点,计数值>分别作为map的key-value放入map中。for(List<String> t : tickets){// t 就是一张ticket["JFK","MUC"]// 升序Map,会对传入的key进行了大小排序Map<String, Integer> temp = new TreeMap<>();// 逻辑1:机票起点相同// t.get(0) : 机票起点 t.get(1) : 机票终点if(map.containsKey(t.get(0))){// 获取机票统计信息 <终点,航行剩余次数>temp = map.get(t.get(0));// 更新机票统计信息,temp.put(t.get(1), temp.getOrDefault(t.get(1), 0) + 1);}else{ // 逻辑2 : 机票起点不同["JFK","ATL"],["SFO","ATL"]temp.put(t.get(1), 1);}map.put(t.get(0), temp);}//先放入起点机场res.add("JFK");//调用递归函数backTracking(tickets.size());return res;}//注意返回值是boolean类型//因为我们只需要找到一个行程private boolean backTracking(int ticketNum){if(res.size() == ticketNum + 1)return true;String last = res.getLast();if(map.containsKey(last)){//防止出现null for(Map.Entry<String, Integer> target : map.get(last).entrySet()){// target.getValue() 就是剩余航行次数,如果大于0,说明还能飞if(target.getValue() > 0){// 将航行目的地添加到结果集res.add(target.getKey());target.setValue(target.getValue() - 1);if(backTracking(ticketNum)) return true;res.removeLast();target.setValue(target.getValue() + 1);}}}return false;}
}
回溯总结篇
carl大神的全面总结(自己全写下来工作量太大了,在这记录一下,先把后面的知识冲了)
力扣刷题记录-回溯算法相关题目相关推荐
- 力扣刷题记录-单调栈相关题目
单调栈是指栈里的元素保持升序或者降序. 判别是否需要使用单调栈:通常是一维数组里面,需要寻找一个元素左边或者右边第一个比自己大或者小的元素的位置,则可以考虑使用单调栈:这样的时间复杂度一般为O(n). ...
- 力扣刷题记录--哈希表相关题目
当遇到需要快速判断一个元素是否出现在集合里面的时候,可以考虑哈希法,牺牲一定的空间换取查找的时间. java常用的哈希表有HashMap.HashSet以及用数组去模拟哈希,这几种方法各有优劣. 数组 ...
- 力扣刷题-python-回溯算法-1(回溯算法模板、题型)
文章目录 1.回溯算法 2.回溯算法模板 3.回溯实例(77.216.17.39.40.131.93.78.90.491.46.47) 4.总结 1.回溯算法 回溯算法的本质就是穷举,最多再加上剪枝, ...
- 力扣刷题记录-动态规划问题总结
百度百科里对于动态规划问题是这样解释的: 在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果.因此各个阶段 ...
- 力扣刷题记录_字符串(自学)
字符串 一.字符串 1.反转字符串(力扣344) 2.反转字符串 II(力扣541) 3.替换空格(剑指 Offer 05) 4.翻转字符串里的单词(力扣151) 5.左旋转字符串(剑指 Offer ...
- 力扣刷题记录--位运算问题
这里写目录标题 一.n&(n-1) 1. 求一个数的二进制表示中的1的个数 力扣 191. 位1的个数 AcWing 801. 二进制中1的个数 2. 判断一个数是否是2的方幂 二.n& ...
- LeetCode力扣刷题——千奇百怪的排序算法
排序算法 一.常见的排序算法 以下是一些最基本的排序算法.虽然在 C++ 里可以通过 std::sort() 快速排序,而且刷题时很少需要自己手写排序算法,但是熟习各种排序算法可以加深 ...
- python力扣刷题记录——204. 计数质数
题目: 统计所有小于非负整数 n 的质数的数量. 方法一: 暴力法 class Solution:def countPrimes(self, n: int) -> int:count = 0if ...
- 力扣刷题记录---快排算法
AcWing 785. 快速排序 对快排算法思想就不描述了,针对快排递归过程中边界的取值做了总结: x为每次递归中,选取的基准数(枢轴) 如果x = q[i]或者x = q[l + r >> ...
最新文章
- python小白逆袭大神课程心得_Python小白逆袭大神学习心得
- stm32官方例程在哪找_正点原子Linux第十一章模仿STM32驱动开发格式实验
- UA MATH563 概率论的数学基础1 概率空间3 概率测度
- DL之DNN:利用DNN【784→50→100→10】算法对MNIST手写数字图片识别数据集进行预测、模型优化
- C#中的默认访问修饰符
- java cucumber_为Java + STANDARD值引入Cucumber
- nginx tcp转发_Nginx学习(九):负载均衡服务
- python教育学_跟着老男孩教育学Python开发【第三篇】:Python函数
- java8 Stream的实现原理 (从零开始实现一个stream流)
- Android的启动模式(上)
- NameError: name 'reload' is not defined等python版本问题解决方案
- IDEA 之because it is included into a circular dependency循环依赖的解决办法
- NotifyIcon用法
- python开根号_python 开根号
- 如何在UltraCompare中编辑文件?
- 【期末复习】现代管理科学基础
- linux 防火墙reject,CentOS 防火墙配置与REJECT导致没有生效问题
- Android沉浸式的两种方法
- IT培训班有用吗?IT培训包就业是真的吗?
- 【淘宝API开发系列】获取商品详情,商品评论、卖家订单接口