参考力扣大神鲂姐的帖子和自己的理解进行了整合
鲂姐原贴:https://leetcode-cn.com/circle/discuss/VFVW01/
回溯类题目的关键点在于:
1、终止条件及相关处理操作
2、对称结构(即先存入后取出)
3、需要另外开辟一个数组来记录“节点是否被访问”的情况
回溯类题目大致分类有:全排列类,组合类,子集类,单词搜索类。
这些题目中,对称结构一般都不会太大改变,区别大多数在于题目中使用到的存储集合类型要适合题意,且终止条件和相关处理要细心,即边界情况要足够理解。
最经典的回溯问题,应该是全排列了
题目:给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [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
permute(int[] nums) {
int len = nums.length;
if(len==0)
return null;
List
list = new ArrayList<>();
boolean[] reached = new boolean[len];//reached数组记录当前是否已经探访过
List path = new ArrayList<>();
dfs(len,nums,reached,0,path,list);
return list;
}
public void dfs(int len,int[] nums,boolean[] reached,int depth,List path,List
list){
if(depth == len){
list.add(new ArrayList(path));
return;
}
for(int i=0;i
if(reached[i]==false){
path.add(nums[i]);
reached[i] = true;
dfs(len,nums,reached,depth+1,path,list);
reached[i] = false;
path.remove(path.size()-1);
}
}
}
}
可以看到,之前提及的回溯题目中的几个要点,其对应:
1、运用 List集合来存储数据
2、对称结构,即遵循“放入子情况-标记其已遍历-继续DFS直至边界情况-抹除其已遍历标记-从子情况中拿出”
3、对于边界条件的理解:本题的题意中,已经指明了这些数字不会有重复情况出现,所以只需要判断是否已经达到边界条件即可,即depth==len。
回到题目本身,考虑题目的变式,该题目中没有提及“原始数据含重复数字”的情况,也没有提到如果子集没有长度限制也可以满足需求的情况(例如原始数据1,2,3。返回[1,2],[1],[]也算满足子集要求,不一定要子集长度为3)。那么随着题目的改动,相应的存储结构和边界条件判断,也会发生改变。
看下一个例子:
题目:给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
示例:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
和上一题不同,这里的题目要求返回不重复全排列,但原始数据可包含重复数字。
先看题解:
class Solution {
public List
permuteUnique(int[] nums) {
List
res = new ArrayList<>();
List path = new ArrayList<>();
if(nums.length==0)
return res;
int len = nums.length;
Arrays.sort(nums);
boolean[] isVisited = new boolean[len];
dfs(len,nums,0,res,path,isVisited);
return res;
}
public void dfs(int len,int[] nums,int depth,List
res,List path,boolean[] isVisited){
System.out.println("=============================");
System.out.println(“这是第 “+depth+” 层的dfs情况:”);
if(depth==len){
System.out.println(“到达底层,path加入res”);
res.add(new ArrayList(path));
return;
}
for(int i=0;i
if(isVisited[i]==true){
System.out.println(“isVisited[i]==true” + " 下标:" + i + " 数值:" + nums[i] );
continue;
}
if(i>0&&nums[i]==nums[i-1]&&isVisited[i-1]==true){
System.out.println(“nums[i]==nums[i-1]” + " i下标:" + i + " i-1数值:" + nums[i] );
continue;
}
path.add(nums[i]);
isVisited[i] = true;
dfs(len,nums,depth+1,res,path,isVisited);
isVisited[i] = false;
path.remove(path.size()-1);
}
}
}
这里我加上了sout各层情况的语句,各位有兴趣的话可以自己尝试一下,加深理解。
可以看到,这里有一些细节处理,首先是对于原始数据进行了排序,以便之后判断数据重复情况时更为方便而不需要扫描。而存储结构并没有改变,仍然是 List。
回到dfs方法里,我们可以看到,其边界条件也没有改变,仍然是达到长度要求即可。对称结构也没有改变。
if中的语句:(i>0&&nums[i]==nums[i-1]&&isVisited[i-1]==true)
首先,i>0是为了后面的i-1不越界,nums[i]==nums[i-1]则是判断是否前后数值一致。
而isVisited[i-1]==true这条语句则是关键,这判断了当前节点的前一个节点是否已经被扫描过并纳入子集。这样就完成了剪枝。
另外的题目有:
这两道题的思路与上面的两道题一致,只不过其数据类型改变了,可以考虑用更灵活的集合来优化。
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
先放源码:
class Solution {
public List letterCombinations(String digits) {
List combinations = new ArrayList();
if(digits.length()==0)
return combinations;
Map
phoneMap = new HashMap<>(); phoneMap.put(‘2’,“abc”);
phoneMap.put(‘3’,“def”);
phoneMap.put(‘4’,“ghi”);
phoneMap.put(‘5’,“jkl”);
phoneMap.put(‘6’,“mno”);
phoneMap.put(‘7’,“pqrs”);
phoneMap.put(‘8’,“tuv”);
phoneMap.put(‘9’,“wxyz”);
backtrack(combinations,phoneMap,digits,0,new StringBuffer());
return combinations;
}
public void backtrack(List combinations,Map
phoneMap,String digits,int index,StringBuffer combination){ if(index==digits.length()){
combinations.add(combination.toString());
}else{
char digit = digits.charAt(index);
String letters = phoneMap.get(digit);
int lettersCount = letters.length();
for(int i=0;i
combination.append(letters.charAt(i));
backtrack(combinations,phoneMap,digits,index+1,combination);
combination.deleteCharAt(index);
}
}
}
}
可以看到,其实与之前的题目思路类似,但是需要对集合理解更为熟练。
还记得在全排列题目时,提到“子集没有长度限制也可以满足需求的情况(例如原始数据1,2,3。返回[1,2],[1],[]也算满足子集要求,不一定要子集长度为3)”的这种情况么?
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
这里改变的就是边界条件了,我们习惯性将边界条件设置为子情况满足长度,那么这时只需要取消这个边界条件即可。即每次dfs中都先进行add操作即可。
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
可以拿全排列II+子集I的思路相结合,很轻松能完成这道题的思路构建。
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board =
[
[‘A’,‘B’,‘C’,‘E’],
[‘S’,‘F’,‘C’,‘S’],
[‘A’,‘D’,‘E’,‘E’]
]
给定 word = “ABCCED”, 返回 true
给定 word = “SEE”, 返回 true
给定 word = “ABCB”, 返回 false
单词搜索,可谓是相当经典的回溯题目了
放上解题里很漂亮的一种解法:
public class Solution {
private static final int[][] DIRECTIONS = {
{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
private int rows;
private int cols;
private int len;
private boolean[][] visited;
private char[] charArray;
private char[][] board;
public boolean exist(char[][] board, String word) {
rows = board.length;
if (rows == 0) {
return false;
}
cols = board[0].length;
visited = new boolean[rows][cols];
this.len = word.length();
this.charArray = word.toCharArray();
this.board = board;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (dfs(i, j, 0)) {
return true;
}
}
}
return false;
}
private boolean dfs(int x, int y, int begin) {
if (begin == len - 1) {
return board[x][y] == charArray[begin];
}
if (board[x][y] == charArray[begin]) {
visited[x][y] = true;
for (int[] direction : DIRECTIONS) {
int newX = x + direction[0];
int newY = y + direction[1];
if (inArea(newX, newY) && !visited[newX][newY]) {
if (dfs(newX, newY, begin + 1)) {
return true;
}
}
}
visited[x][y] = false;
}
return false;
}
private boolean inArea(int x, int y) {
return x >= 0 && x < rows && y >= 0 && y < cols;
}
}
除此外类似的还有路径搜索,和单词搜索II。有兴趣的朋友可以尝试下。