回溯是 DFS 的一种,它不是用在遍历图的节点上,而是用于求解 排列组合 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串。
在程序实现时,回溯需要注意对元素进行标记的问题。使用递归实现的回溯,在访问一个新元素进入新的递归调用,此时需要将新元素标记为已经访问,这样才能在继续
递归调用时不用重复访问该元素;但是在递归返回时,需要将该元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,而在不同的递归链是可以访问
已经访问过但是不在当前递归链中的元素。
回溯算法的标准格式:
回溯法伪代码 back(c,digits,offset,res){ if(offset==length){ res.add(); return; } for(char c:String.toCharArray()){ back(c,digits,offset+1,res); } }
leetcode 17
最终代码
class Solution { public static final String[] key={"","","abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; // 回溯法伪代码 // back(c,digits,offset,res){ // if(offset==length){ // res.add(); // return; // } // for(char c:String.toCharArray()){ // back(c,digits,offset+1,res); // } // } public List<String> letterCombinations(String digits) { List<String> res=new ArrayList<>(); if(digits!=null&&digits.length()!=0){ back("",digits,0,res); } return res;//如果最终结果不对,可以直接诊断return附近哪里逻辑写错了 } public void back(String pre,String digits,int offset,List<String> res){ if(offset==digits.length()){ res.add(pre); return; } String letters=key[digits.charAt(offset)-'0']; for(char ch:letters.toCharArray()){ back(pre+ch,digits,offset+1,res); } } }
leetcode 79
这道题也毫无疑问地用回溯的迷宫找路完整版模版 back(c,digits,offset,res){ if(offset==length){ res.add(); return; } for(char c:String.toCharArray()){ back(c,digits,offset+1,res); } } ****************************************** exit(){ for (int i=0;i<m;i++){ for(int j=0;j<n;j++){ if(back()) return res; } } } back(char[][] board, String word, int start, int x, int y){ if(start==word.length){ return true; } //越界等找不到满足条件的路径的时候 if(x<0||x>m||y<0||y<n||vis[][]==true||board[x][y]!=word.charAt(start)){ return false; } board[x][y]=true; for(int i=0;i<4;i++){ int nextx=x+dir[i][0]; int nexty=y+dir[i][1]; if(back(board,word,start+1,nextx,nexty)) return true; } //最重要的是如果四个方向都没有找到合适的路径,说明当前路径不合适,则应该对访问值进行回归 vis[x][y]=false; return false; }
本题的完整版代码:
class Solution { // 这道题也毫无疑问地用回溯的迷宫找路完整版模版 // back(c,digits,offset,res){ // if(offset==length){ // res.add(); // return; // } // for(char c:String.toCharArray()){ // back(c,digits,offset+1,res); // } // } // ****************************************** // exit(){ // for (int i=0;i<m;i++){ // for(int j=0;j<n;j++){ // if(back()) return res; // } // } // } // back(char[][] board, String word, int start, int x, int y){ // if(start==word.length){ // return true; // } // //越界等找不到满足条件的路径的时候 // if(x<0||x>m||y<0||y<n||vis[][]==true||board[x][y]!=word.charAt(start)){ // return false; // } // board[x][y]=true; // for(int i=0;i<4;i++){ // int nextx=x+dir[i][0]; // int nexty=y+dir[i][1]; // if(back(board,word,start+1,nextx,nexty)) return true; // } // //最重要的是如果四个方向都没有找到合适的路径,说明当前路径不合适,则应该对访问值进行回归 // vis[x][y]=false; // return false; // } public int[][] dir={{1,0},{-1,0},{0,1},{0,-1}}; public boolean exist(char[][] board, String word) { int m=board.length; int n=board[0].length; boolean[][] vis=new boolean[m][n]; for (int i=0;i<m;i++){ for(int j=0;j<n;j++){ if(back(board,word,0,i,j,vis)) return true; } } return false; } public boolean back(char[][] board, String word, int start, int x, int y,boolean[][] vis){ int m=board.length; int n=board[0].length; if(start==word.length()){ return true; } //越界等找不到满足条件的路径的时候 if(x<0||x>=m||y<0||y>=n||vis[x][y]==true||board[x][y]!=word.charAt(start)){ return false; } vis[x][y]=true; for(int i=0;i<4;i++){ int nextx=x+dir[i][0]; int nexty=y+dir[i][1]; if(back(board,word,start+1,nextx,nexty,vis)) return true; } //最重要的是如果四个方向都没有找到合适的路径,说明当前路径不合适,则应该对访问值进行回归 vis[x][y]=false; return false; } }
leetcode 93
- substring(0,1):表示从0开始(包括0),不包括1
- sb.substring(2):表示从2开始截取知道末尾,其中包括2
- Integer.valueOf(string):将字符转换为整型
-
substring都是小写
class Solution { public List<String> restoreIpAddresses(String s) { List<String> res=new ArrayList<>(); if(s!=null||s.length()!=0) back(res,"",0,s); return res; } public void back(List<String> res,String p,int k,String s){ if(k==4||s.length()==0){ if(k==4&&s.length()==0){ res.add(p); return; } return; } for(int i=0;i<=2&&s.length()>i;i++){ // if (i != 0 && s.charAt(0) == '0') break;//这种情况说明出现了 String ch=s.substring(0,i+1); if(Integer.valueOf(ch)<=255){ back(res,p.length()!=0?p+"."+ch:ch,k+1,s.substring(i+1)); } } } }class Solution { public List<String> restoreIpAddresses(String s) { List<String> res=new ArrayList<>(); if(s!=null||s.length()!=0) back(res,"",0,s); return res; } public void back(List<String> res,String p,int k,String s){ if(k==4||s.length()==0){ if(k==4&&s.length()==0){ res.add(p); return; } return; } for(int i=0;i<=2&&s.length()>i;i++){ // if (i != 0 && s.charAt(0) == '0') break;//这种情况说明出现了 String ch=s.substring(0,i+1); if(Integer.valueOf(ch)<=255){ back(res,p.length()!=0?p+"."+ch:ch,k+1,s.substring(i+1)); } } } }
leetcode 46
class Solution { public List<List<Integer>> permute(int[] nums) { List<List<Integer>> res =new ArrayList<>(); List<Integer> tmp=new ArrayList<>(); boolean[] vis=new boolean[nums.length]; back(res,vis,nums,tmp); return res; } public void back(List<List<Integer>>res,boolean[] vis,int[] nums,List<Integer> tmp ){ if(tmp.size()==nums.length){ res.add(new ArrayList(tmp));//这里稍微注意一下 return; } for(int i=0;i<nums.length;i++){ if(vis[i]) continue; vis[i]=true;//对称 tmp.add(nums[i]);//对称 back(res,vis,nums,tmp); tmp.remove(tmp.size()-1);//对称 vis[i]=false;//对称 } } }
leetcode 47
关键点在于怎么让最终的结果不重复:在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。
为什么?因为如果当前值和前一个值相同,且前一个vis为false,则说明当前2,2已经算过了,现在算的是从第二个2继续排序为2,2说明有重复则可以不加入最终结果用continue而不是return
class Solution { public List<List<Integer>> permuteUnique(int[] nums) { List<List<Integer>> res =new ArrayList<>(); List<Integer> tmp=new ArrayList<>(); boolean[] vis=new boolean[nums.length]; Arrays.sort(nums); back(nums,res,vis,tmp); return res; } public void back(int[] nums,List<List<Integer>> res,boolean[] vis,List<Integer> tmp){ if(tmp.size()==nums.length){ res.add(new ArrayList<Integer>(tmp));//这里的写法要注意可以省略<Integer> return; } for(int i=0;i<nums.length;i++){ if(i!=0&&nums[i-1]==nums[i]&&vis[i-1]==false) continue; if(vis[i]) continue; vis[i]=true; tmp.add(nums[i]); back(nums,res,vis,tmp); tmp.remove(tmp.size()-1);//这里容易写成nums[i] vis[i]=false; } } }
leetcode 77
class Solution { public List<List<Integer>> combine(int n, int k) { List<List<Integer>> res= new ArrayList<>(); List<Integer> tmp=new ArrayList<>(); back(1,k,n,tmp,res); return res; } public void back(int start,int k,int n,List<Integer> tmp,List<List<Integer>> res){ if(k==0){ res.add(new ArrayList(tmp)); return; } for(int i=start;i<=n-k+1;i++){//注意这里的循环边界判断 //不能让后面没有选数字的空间 tmp.add(i); // back(start+1,k-1,n,tmp,res);这种写法是错误的 back(i+1,k-1,n,tmp,res);//是当前选定的第一个数字的下一个开始选,而不是start,如果是start,这个值到下一轮for的时候i变了,但是start没变就会出问题 tmp.remove(tmp.size()-1); } } }
leetcode 39
class Solution { public List<List<Integer>> combinationSum(int[] candidates, int target) { //注意这种类型的题目,只要是不重复调用back函数的时候就要向前看(用i而不是定植) List<List<Integer>> res=new ArrayList<>(); back(res,0,target,new ArrayList<>(),candidates); return res; } public void back(List<List<Integer>> res,int start ,int target,List<Integer> list,int[] candidates){ if(target<=0){ res.add(new ArrayList(list));//注意这里一定要包装成正确的格式,不然会是空 return; } for(int i=start;i<candidates.length;i++){ if(candidates[i]<=target){ list.add(candidates[i]); back(res,i,target-candidates[i],list,candidates); list.remove(list.size() - 1); } } } }
leetcode 40
这道题就是组合类的高阶版了,主要是由于最后的结果不能重复的情况下,而且待选的数字里面也会有重复的情况,这两个情况都能通过往前继续搜索不回头看解决。
class Solution { public List<List<Integer>> combinationSum2(int[] candidates, int target) { List<List<Integer>> res=new ArrayList<>(); boolean[] vis=new boolean[candidates.length]; Arrays.sort(candidates); back(res,target,candidates,0,vis,new ArrayList<>() ); return res; } public void back(List<List<Integer>> res,int target,int[] candidates,int start,boolean[] vis,List<Integer> tmp){ if(target<=0){res.add(new ArrayList(tmp));return;}//!!!!!!!这里已经不止一次在写的时候忘记包装了!!!!!!!!!! for(int i=start;i<candidates.length;i++){ if( i!=0&&candidates[i-1]==candidates[i]&&vis[i-1]==false){continue;}//这里vis[i-1]==false是i-1 //注意这里为什么是continue不是return,说明这个数字可以不用往后for循环了,之前的数字已经循环过了结果是相同的,如果代码进行下去最终的结果会出现重复 if(candidates[i]<=target){ vis[i]=true; tmp.add(candidates[i]);//!!!!!!!这里不用包装!!!!!!!!!! back(res,target-candidates[i],candidates,i+1,vis,tmp); tmp.remove(tmp.size()-1); vis[i]=false; } } } }
leetcode 78
这道题的变化点在于要求的组合的长度是变长的,之前由于是固定的所以只需要back(k),但是现在要循环遍历所有可能的长度。
class Solution { public List<List<Integer>> res; public List<Integer> tmp; public List<List<Integer>> subsets(int[] nums) { res=new ArrayList<>(); tmp=new ArrayList<>(); for(int i=0;i<=nums.length;i++){//注意这里可以等于0 back(i,0,nums); } return res; } public void back(int k,int start,int[] nums){ if(tmp.size()==k){ res.add(new ArrayList<>(tmp)); return; } for(int i=start;i<nums.length;i++){ tmp.add(nums[i]); back(k,i+1,nums); tmp.remove(tmp.size()-1); } } }
leetcode 90
主要在于备选数字里面有重复的数字,那么就要考虑一行代码
if(i!=0&& nums[i]==nums[i-1]&&vis[i-1]==false) continue;
class Solution { public List<List<Integer>> res; public List<Integer> tmp; public boolean[] vis; public List<List<Integer>> subsetsWithDup(int[] nums) { res=new ArrayList<>(); tmp=new ArrayList<>(); Arrays.sort(nums); vis=new boolean[nums.length]; for(int i=0;i<=nums.length;i++){ back(i,0,nums); } return res; } public void back(int k,int start,int[] nums){ if(tmp.size()==k){ res.add(new ArrayList<>(tmp)); return; } for(int i=start;i<nums.length;i++){ if(i!=0&&nums[i-1]==nums[i]&&vis[i-1]==false) continue;//这里还是有点模糊,再仔细想想 //这里之所以会用continue是因为两个数字一样的话没有必要重新遍历一遍 tmp.add(nums[i]); vis[i]=true; back(k,i+1,nums);// tmp.remove(tmp.size()-1); vis[i]=false; } } }
leetcode 131
class Solution { List<List<String>> res; List<String> tmp; public List<List<String>> partition(String s) { res=new ArrayList<>(); tmp=new ArrayList<>(); back(s);//注意如果是涉及到字符,那么绝对会用到很多的substring //复习一下substring的用法 // substring(0,1):表示从0开始(包括0),不包括1 // sb.substring(2):表示从2开始截取知道末尾,其中包括2 // Integer.valueOf(string):将字符转换为整型 // substring都是小写 return res; } public void back(String s){ if(s.length()<=0){ res.add(new ArrayList(tmp)); return; } for(int i=0;i<s.length();i++){//由于例子里面没有aab结果,所以边界不能用等号 //这里之所以用i=0不是start是因为字符串在用substring减少 if(ishui(s,0,i)){ tmp.add(s.substring(0,i+1));//注意这里是i+1,由于不包含i+1 back(s.substring(i+1)); tmp.remove(tmp.size()-1); } } } public boolean ishui(String s,int i,int j){ while(i<j){ if(s.charAt(i)!=s.charAt(j)){ return false; } i++; j--; } return true; } }
leetcode 37 数独问题
数独问题的核心思想是首先遍历整个矩阵,主要是对行列以及小格子里面的矩阵进行初始化,之后在back函数里面更新的部分是要先找到为.的地方,然后再进行后续的判定。
class Solution { public boolean[][] rowu=new boolean[9][10]; public boolean[][] colu=new boolean[9][10]; public boolean[][] cubeu=new boolean[9][10]; public char[][]board; public void solveSudoku(char[][] board) { this.board=board; for(int i=0;i<9;i++){ for(int j=0;j<9;j++){ if(board[i][j]!='.'){ rowu[i][board[i][j]-'0']=true; colu[j][board[i][j]-'0']=true; cubeu[kk(i,j)][board[i][j]-'0']=true; } } } for(int i=0;i<9;i++){ for(int j=0;j<9;j++){ back(i,j); } } } public boolean back(int row,int col){ //注意这个函数在做的实际上就是以一个点为起点去遍历整个矩阵和这个点相关(行列块)的'.'进行处理 //找到'.'的位置 while(row<9&&board[row][col]!='.'){ // row=col==8?row:row+1;// row=col==8?row+1:row; col=col==8?0:col+1; } if(row==9) return true; for(int num=1;num<10;num++){//注意这里容易写错 //这里实际上就是对每一个'.'进行数字填充,从而确定出唯一可行的解 if (rowu[row][num] || colu[col][num] || cubeu[kk(row, col)][num]) continue; rowu[row][num] = colu[col][num] = cubeu[kk(row, col)][num]=true; board[row][col]=(char)(num+'0'); if(back(row,col)) return true; board[row][col]='.'; rowu[row][num] = colu[col][num] = cubeu[kk(row, col)][num]=false; } return false; } public int kk(int i,int j){ return (i/3)*3+j/3; } }