78.子集
把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
由于子集问题也算是一种组合问题,它的结果集是无序的,因此在回溯算法的for循环遍历中,也需要用到startIndex指针。
子集问题抽象成树形结构为:
思路比较简单,根据树形结构图可以实现出来。回溯三步走
1、明确参数返回值
2、明确终止条件:startIndex >= nums.length(类似分割问题)
其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了。
3、明确单次逻辑:
求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树。
result.push_back(path); // 收集子集,要放在终止条件上面,否则会漏掉空集
90.子集II
与78.子集 (opens new window)区别就是集合里有重复元素了,需要对求取的子集要去重。
本题和40.组合总和II (opens new window)是一个套路。理解“树层去重”和“树枝去重”。
去重需要先对集合排序
代码实现:
同样没有使用used数组,先排序后根据startIndex指针实现了for循环去重。
ps:如果要是全排列的话,每次要从0开始遍历,为了跳过已入栈的元素,需要使用used。
491.递增子序列
难点:
1、子集长度>1,即不能把全部节点纳入结果集
2、在不对序列重新排序的情况下去重
新的去重逻辑:
新建一个set集合,用来记录本层中的元素是否重复使用了。详细看以下代码
//递增子序列
Deque<Integer> path = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
findBacktrack(nums, 0);
return result;
}
public void findBacktrack(int[] nums, int startIndex){
if (path.size() > 1){ // 如果path>1,存入结果集
result.add(new ArrayList<>(path));
}
if (startIndex >= nums.length) return; // 递归到序列尾部,结束
//是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!所以没有对应的remove操作
HashSet<Integer> sets = new HashSet<>();
for (int i = startIndex; i < nums.length; i++){
if ((path.size() != 0 && nums[i] < path.getLast())||sets.contains(nums[i])){
continue;
}
sets.add(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
path.addLast(nums[i]);
findBacktrack(nums, i+1);
path.removeLast();
}
}
46.全排列
之前学习了77.组合问题 (opens new window)、 131.分割回文串 (opens new window)和78.子集问题 (opens new window),接下来看一看排列问题。
排列问题需要一个used数组,标记已经选择的元素。
used数组,其实就是记录树枝层面此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。
排列问题抽象成树形结构:
代码实现:常规的回溯三步走。
47.全排列 II
对46.全排列问题的树层方面去重,与40.组合总和II (opens new window)、90.子集II (opens new window)是一样的套路。
先排序后去重。
代码实现:利用used数组。
used[i] == 1 (true),说明同一树枝nums[i]使用过
used[i] == 0 (false),说明将在同一树层使用nums[i]
判断同一树层是否重复有两点:
1、当前值与前一个值相等。
2、前一个值将在本树层使用。used[i-1] != true
332.重新安排行程
递归三步走:
1、明确参数和返回值:
我们不遍历整个树,找到结果就返回,所以有返回值。
2、明确终止条件:
最终结果的路径=tickets列表长度+1
3、明确单次递归逻辑:
如果used=false且单个行程的第一个地点和当前pathString路径最后一个地点一致,递归并回溯。
//重新安排行程
List<String> pathString = new LinkedList<>();
List<String> resultString = new LinkedList<>();
public List<String> findItinerary(List<List<String>> tickets) {
Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1))); //先排序
boolean[] used = new boolean[tickets.size()];
pathString.add("JFK");
findItineraryBacktrack(tickets, used);
return resultString;
}
//明确返回值:我们不遍历整个树,找到结果就返回,所以有返回值。
public static boolean findItineraryBacktrack(List<List<String>> tickets, boolean[] used){
//明确终止条件:最终结果的路径=tickets列表长度+1
if (pathString.size() == tickets.size() + 1){
resultString = new LinkedList<>(pathString);
return true;
}
//明确单次递归逻辑:如果used=false且单个行程的第一个地点和当前pathString路径最后一个地点一致,递归并回溯
for (int i = 0; i < tickets.size(); i++){
if (!used[i] && tickets.get(i).get(0).equals(pathString.get(pathString.size() - 1))){
used[i] = true;
pathString.add(tickets.get(i).get(1));
if (findItineraryBacktrack(tickets, used)){
return true;
}
pathString.remove(pathString.size()-1);
used[i] = false;
}
}
return false;
}
51. N皇后
回溯三步走:
1、明确参数和返回值:
参数:int n;int row(表示第几行);char[][] chessboard(二维字符数组,记录棋盘状态)
2、明确终止条件:
遇到叶子结点就终止,并把当前棋盘格存入结果集。
叶子结点:row == n (在上图,属于棋盘第4行。但三皇后无解,不存在叶子结点)
3、明确单次递归逻辑:
for(int col = 0; col < n; ++col) 循环条件 (++col和col++在for循环中的区别是?)
判断该行当前情况下的皇后是否合法。
如果合法,递归,进入下一行,回溯。(回溯比较特殊,递归是数组值设为Q,回溯数组值设为. )