回溯
2021.3.14-- 2021.4.11
22.78.77.46.17.40.131.93.491,47
回溯==dfs+剪枝,是特殊的递归
①流程:穷举所有的可能,一层层向下递归,找到答案=>尝试别的答案或返回答案,若没找到=>返回上层递归,尝试别的路径。
②解决的问题:组合:77切割:子集:排列:棋盘:
③理解:因为回溯解决的都是在集合中递归查找子集,所以可以将问题抽象为树形结构,集合的大小为树的宽度,递归的深度为树的深度。
④模板:
void backtracking(参数)
1)终止条件
if (终⽌条件) {
存放结果;
return;
}
2)遍历过程:for (选择:本层集合中元素(树中节点孩⼦的数量就是集合的⼤⼩)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
⑤常用参数:全局变量:List<List<Integer>> res = new ArrayList<>();//存放结果
Deque<Integer> path = new ArrayDeque<>();//存放路径,类似栈结构, Java 的官方文档推荐用 Deque ,一般而言,数组空间由于可以随机访问,如果没有频繁的扩容操作,只在末尾操作的话,性能是比 LinkedList 要好的,这是因为 LinkedList 创建结点和销毁结点,以及维护下一个结点的地址,这些都有开销
组合问题
77. 组合
问题
思路
先抽象成树形图
如图,第一个分支在2.3.4中取值,第二个分支就要在3,4中取值,①控制递归的起始就要引入start
,②终止条件:题中要求k个数组合,则path的大小为k时,这个路径结束,将该路径保存。 ③搜索过程:横向for循环从start
到n
,纵向递归的下一层从i+1
开始。
剪枝:如果for循环选择的起始位置之后的元素个数 已经不⾜ 我们需要的元素个数了,那么就没有必要搜索了。
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n,k,1);
return res;
}
private void backtracking(int n, int k, int start){
if(path.size()==k){
res.add(new ArrayList(path));
return;
}
for(int i = start; i<=n - (k - path.size()) + 1;i++){//剪枝,如果i之后的元素个数不足需要的个数,没必要搜索了
path.addLast(i);
backtracking(n,k,i+1);
path.removeLast();
}
}
}
216. 组合总和 III
问题
思路
和leetcode77相比,限制了和为n,则回溯的终止条件为k,收集path的条件为n,且可以用来剪枝。
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(k,n,1,0);
return res;
}
private void backtracking(int k, int n, int start,int sum){
if(path.size() == k){
if(n == sum){
res.add(new ArrayList(path));
return;
}
return;
}
for(int i = start; i<=9 && n-i>k-i && sum<=n; i++){//剪枝: i <= 9 - (k - path.size()) + 1;
path.addLast(i);
backtracking(k,n,i+1,sum+i);//没有在这里改变sum的值,所以不需要回溯sum
path.removeLast();
/*这样需要回溯sum
sum += i; // 处理
path.addLast(i); // 处理
backtracking(targetSum, k, sum, i + 1);
sum -= i; // 回溯
path.removeLast(); // 回溯
*/
}
}
}
17. 电话号码的字母组合
问题
思路
用map或数组来映射数字与字母,path用StringBuffer类型,可变,需要index来记录遍历到第几个数字了,终止条件为遍历到最后一个输入的数字,无剪枝
class Solution {
List<String> res = new ArrayList<>();
StringBuffer path = new StringBuffer();
String[] letterMap = new String[]{
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
};
public List<String> letterCombinations(String digits) {
if(digits.length() == 0){
return res;
}
backtracking(digits,0);
return res;
}
private void backtracking(String digits, int index){
if(index == digits.length()){
res.add(new String(path));
return;
}
int digit = digits.charAt(index)-'0';
String s = letterMap[digit];
for(int i =0; i<s.length();i++){
path.append(s.charAt(i));
backtracking(digits,index+1);
path.deleteCharAt(index);
}
}
}
39. 组合总和
问题
思路
和组合问题的区别在于,每个candidate可以被重复选中
横向的for循环从start到candidate结尾,纵向到sum>=tagret
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(candidates,target,0,0);
return res;
}
private void backtracking(int[] candidates,int target,int sum,int start){
if(sum > target){
return;
}
if(sum==target){
res.add(new ArrayList<>(path));
return;
}
for(int i =start; i< candidates.length && sum<target; i++){
path.addLast(candidates[i]);
backtracking(candidates,target,sum+candidates[i],i);//因为可重复,不需要i+1
path.removeLast();
}
}
}
40. 组合总和 II
问题
思路
和LeetCode39相比,candidates有重复,但结果集中不能重复挑选,所以需要标记来判断元素是否被使用过
如图,我们要去重的是同⼀树层上的“使⽤过”(candidates需要排序),同⼀树枝上的都是⼀个组合⾥的元素,不⽤去重。排序后
①判断当前是否和前一个相等且当前元素要是在当前path之后的节点,相等就跳过。
②用used[]
数组判断去重,相等且used[i-1]==0
,说明同一层上用过,used[i-1]==1
说明同一树枝上用过,如图。
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
int[] used = new int[candidates.length];
Arrays.fill(used,0);
backtracking(candidates,target,0,0);
return res;
}
private void backtracking(int[] candidates, int target, int sum, int start){
if(sum>target){
return;
}
if(sum == target){
res.add(new ArrayList<>(path));
return;
}
for(int i = start; i<candidates.length && sum<target; i++){
if(i>start && candidates[i-1]==candidates[i]){
continue;
}
path.addLast(candidates[i]);
backtracking(candidates,target,sum+candidates[i],i+1);//每个candidate只能用一次,所以i+1
path.removeLast();
}
}
}
131.分割回文串
问题
思路
切割问题,和组合类似,先抽象成树形如图,横向遍历从start开始,每次截取start到i的子串,判断是否回文串,加入path,因为同一树枝截取过就不能再用,所以纵向回溯需要i+1.
判断回文串
class Solution {
List<List<String>> res = new ArrayList<>();
Deque<String> path = new ArrayDeque<>();
public List<List<String>> partition(String s) {
backtracking(s,0);
return res;
}
private void backtracking(String s,int start){
if(start>=s.length()){
res.add(new ArrayList<>(path));
return;
}
for(int i = start; i<s.length(); i++){
if(checkPalindrome(s,start,i)){
path.addLast(s.substring(start,i+1));
backtracking(s,i+1);
path.removeLast();
}
}
}
private boolean checkPalindrome(String s,int i, int j){
while(i<j){
if(s.charAt(i)!=s.charAt(j)){
return false;
}
i++;
j--;
}
return true;
}
}
93. 复原 IP 地址
问题
思路
切割问题,因为ip地址格式的限制,可以剪枝
每个path为字符串格式,每组数字中间要加.
,可以设path
为StringBuilder
类型
class Solution {
List<String> res = new ArrayList<>();
StringBuilder path = new StringBuilder();
public List<String> restoreIpAddresses(String s) {
if(s.length() == 0){
return res;
}
backtracking(s, 0,0);
return res;
}
private void backtracking(String s, int start,int pointNum){
if(pointNum == 3){
if(isValid(s,start,s.length()-1)){//如果最后一段符合就加入,类似在for循环剪枝
path.append(s.substring(start,s.length()));
res.add(path.toString());
}
return;
}
for(int i = start; i<s.length();i++){
if(isValid(s,start,i)){
int pathLen = path.length();
path.append(s.substring(start,i+1));
path.append(".");
backtracking(s,i+1,pointNum+1);
path.setLength(pathLen);
}else{
break;
}
}
}
private boolean isValid(String s, int start, int end){
if(start>end){
return false;
}
if(s.charAt(start)=='0' && start!=end){
return false;
}
int num = 0;
for(int i = start; i<=end; i++){
num = num*10 + (s.charAt(i)-'0');
if(num>255){
return false;
}
}
return true;
}
}
78. 子集
问题
思路
求子集,遍历所有的结果,for循环结束就是回溯结束条件,因为每个节点就是一个
path
,所以每个回溯都要将path
加入结果集。
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
public List<List<Integer>> subsets(int[] nums) {
backtracking(nums,0);
return res;
}
public void backtracking(int[] nums, int start){
res.add(new ArrayList<>(path));
for(int i = start; i<nums.length; i++){
path.addLast(nums[i]);
backtracking(nums,i+1);
path.removeLast();
}
}
}
90. 子集 II
问题
思路
和LeetCode78比,元素可重复,所以同层需要去重
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
int[] used = new int[nums.length];
Arrays.fill(used,0);
Arrays.sort(nums);
backtracking(nums,used,0);
return res;
}
private void backtracking(int[] nums, int[] used,int start){
res.add(new ArrayList<>(path));
for(int i = start; i<nums.length; i++){
if(i>0 && used[i-1]==0 && nums[i-1]==nums[i]){//可以不用used[], if(i>start && nums[i-1]==nums[i])
continue;
}
used[i]=1;
path.addLast(nums[i]);
backtracking(nums,used,i+1);
path.removeLast();
used[i] = 0;
}
}
}
491. 递增子序列
问题
思路
递增不重复,递增可以通过比较当前元素和path的最后一个元素来限制。不重复可以通过标记数组来标记该元素是否在本层使用过,所以标记数组要在每次回溯时重新定义,只负责本层
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backtracking(nums,0);
return res;
}
private void backtracking(int[] nums,int start){
int[] flag = new int[201];
Arrays.fill(flag,0);
if(path.size()>=2){
res.add(new ArrayList<>(path));
}
for(int i = start; i<nums.length; i++){
if(!path.isEmpty() && path.getLast()>nums[i] ||flag[nums[i]+100] == 1){
continue;
}
flag[nums[i]+100] = 1;
path.addLast(nums[i]);
backtracking(nums,i+1);
path.removeLast();
}
}
}
46. 全排列
问题
思路
全排列,每个
path
包含所有元素,每次需要从头搜索,所以不需要start
。同一树枝(path
)不能重复使用元素,需要标记数组,used[]
记录的是path
里已收录的元素(同一节点下本层去重)
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
public List<List<Integer>> permute(int[] nums) {
int[] used = new int[nums.length];
Arrays.fill(used,0);
backtracking(nums,used);
return res;
}
private void backtracking(int[] nums,int[] used){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i = 0; i<nums.length; i++){
if(used[i]==1){
continue;
}
used[i] = 1;
path.addLast(nums[i]);
backtracking(nums,used);
path.removeLast();
used[i] = 0;
}
}
}
47. 全排列 II
问题
思路
和LeetCode46区别在于元素可重复,要在同一层去重,可以先排序,判断和前一位是否相等
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
public List<List<Integer>> permuteUnique(int[] nums) {
int[] used = new int[nums.length];
Arrays.fill(used,0);
Arrays.sort(nums);
backtracking(nums,used);
return res;
}
private void backtracking(int[] nums,int[] used){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i =0; i<nums.length; i++){
if(i>0 && nums[i] == nums[i-1] && used[i-1]==0){
continue;
}
if(used[i] ==0){
path.addLast(nums[i]);
used[i] = 1;
backtracking(nums,used);
path.removeLast();
used[i] = 0;
}
}
}
}
332. 重新安排行程
问题
思路
参考文献:饲养员、代码随想录,LeetCode题解