回溯算法
1,回溯概念
回溯就是递归,递归就是暴力穷举。
回溯的思考过程就是一个二叉树的形成及遍历过程,二叉树的宽度取决于回溯集合的大小;二叉树的深度取决于回溯的深度。
2,力扣题型
77,组合
https://leetcode-cn.com/problems/combinations/
class Solution {
List<List<Integer>> result = new ArrayList();
LinkedList<Integer> path = new LinkedList();
public List<List<Integer>> combine(int n, int k) {
// 回溯+剪枝
backTrack(n,k,1);
return result;
}
/**
*index控制下一次遍历的索引
*/
public void backTrack(int n,int k,int index){
// 到达叶子节点,则一条路径完成
if(path.size()==k){
result.add(new ArrayList<>(path));
return;
}
// 横向遍历,取过之后就不会取值了防止重复
// 剪枝优化:不能到达最底层的路径不取:i<=n-(k-path.size())+1
for(int i=index;i<=n-(k-path.size())+1;i++){
// 回溯前添加元素
path.add(i);
backTrack(n,k,i+1);
// 回溯后移除元素
path.removeLast();
}
}
}
参考链接:https://programmercarl.com/0077.%E7%BB%84%E5%90%88.html
216,组合三
https://leetcode-cn.com/problems/combination-sum-iii/
class Solution {
List<List<Integer>> result = new ArrayList();
LinkedList<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum3(int k, int n) {
backTrack(k,n,1,0);
return result;
}
/**
* k,n,index:当前索引,sum:当前和
*/
public void backTrack(int k,int targetSum,int index,int sum){
// 剪枝
if(sum>targetSum){
return;
}
// 回溯的终止条件
if(path.size()==k){
if(sum==targetSum){
result.add(new ArrayList(path));
}
return;
}
for(int i=index;i<=9-(k-path.size())+1;i++){
path.add(i);
sum += i;
backTrack(k,targetSum,i+1,sum);
path.removeLast();
sum -= i;
}
}
}
17,电话号码的字母组合
https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/submissions/
给定1个2-9的字符串,他们按照键盘的对应顺序对应着相应字母,输出不同的字母组合。
class Solution {
LinkedList<String> res = new LinkedList();
public List<String> letterCombinations(String digits) {
if(digits == null || digits.length()==0){
return res;
}
String[] map = {
"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
backTrack(digits,map,0);
return res;
}
StringBuilder temp = new StringBuilder();
// 回溯
// index表示遍历到了第几个字母
public void backTrack(String digits,String[] map, int index){
if(digits.length()==index){
// temp是index对应位置上字母的组合
res.add(temp.toString());
return;
}
// s表示index对应位置上的字符串(对于样例:‘2’-0->(2,abc))
String s = map[digits.charAt(index)-'0'];
//对回溯集合横向遍历
for(int i=0;i<s.length();i++){
//一轮只拼接一个
temp.append(s.charAt(i));
backTrack(digits,map,index+1);
//满足一个时回溯
temp.deleteCharAt(temp.length()-1);
}
}
}
78,子集问题
子集问题和组合问题的区别:子集是获得递归树的所有节点,组合只获得叶子节点
class Solution {
LinkedList<Integer> path = new LinkedList();
List<List<Integer>> res = new ArrayList();
public List<List<Integer>> subsets(int[] nums) {
backTrack(nums,0);
return res;
}
public void backTrack(int[] nums,int index){
// 不需要判断k==path.size即是否叶子节点,有一个添加一个
// 为什么要new一下,因为它需要一个对象
res.add(new ArrayList(path));
for(int i=index;i<nums.length;i++){
path.add(nums[i]);
backTrack(nums,i+1);
path.removeLast();
}
}
}
39,组合总和
https://leetcode-cn.com/problems/combination-sum/
在无重复元素的数组中选若干元素,使得和为target;其中元素可以重复使用。
class Solution {
List<List<Integer>> res = new ArrayList();
LinkedList<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
backTrack(candidates,0,target,0);
return res;
}
public void backTrack(int[] candidates,int index,int target,int sum){
if(sum==target){
res.add(new ArrayList(path));
return;
}
// 在for循环中剪枝
for(int i=index;i<candidates.length && sum+candidates[i]<=target;i++){
sum += candidates[i];
path.add(candidates[i]);
// 重复选取i可以不加1,即下次还可以从当前位置选取
backTrack(candidates,i,target,sum);
sum -= candidates[i];
path.removeLast();
}
}
}
40,组合总和||
https://leetcode-cn.com/problems/combination-sum-ii/
在有重复元素的数组中选若干元素,使得和为target;其中元素不可以重复使用。
现在思路发生了变化,元素不能重复使用而且集合元素有重复。首先不能重复使用意味着,i++;但是这只能保证纵向的不重复;也就是说
例如当 candidates = [ 2 , 2 ] , t a r g e t = 2 \textit{candidates} = [2, 2],target=2 candidates=[2,2],target=2 时,上述算法会将列表 [2][2] 放入答案两次。
树枝去重和树层去重
class Solution {
List<List<Integer>> res = new ArrayList();
LinkedList<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backTrack(candidates,target,0,0);
return res;
}
public void backTrack(int[] candidates,int target,int sum,int index){
if(sum==target){
res.add(new ArrayList(path));
return;
}
for(int i=index;i<candidates.length && sum+candidates[i]<=target;i++){
// 同层去重,i控制着层数(斜上方的位置不能有重复元素)
if(i>index && candidates[i]==candidates[i-1]) continue;
sum+=candidates[i];
path.add(candidates[i]);
backTrack(candidates,target,sum,i+1);
sum -= candidates[i];
path.removeLast();
}
}
}
131,分割回文串
https://leetcode-cn.com/problems/palindrome-partitioning/
class Solution {
List<List<String>> res = new ArrayList();
LinkedList<String> path = new LinkedList();
public List<List<String>> partition(String s) {
// 分割成子集,判断子集是否是回文
backTrack(s,0);
return res;
}
public void backTrack(String s,int index){
if(index==s.length()){
res.add(new ArrayList(path));
return;
}
for(int i=index;i<s.length();i++){
// 关键步骤
String str = s.substring(index,i+1);
// 对每个子集进行判断,不行跳出
if(!isPrime(str)){
continue;
}
// 也可以将add加入if内,但也要有失败的处理逻辑
// if(isPrime(str)) path.add(str);
// else continue;
path.add(str);
backTrack(s,i+1);
path.removeLast();
}
}
public boolean isPrime(String s){
if(s.length()<=1 || s==null){
return true;
}
for(int i=0,j=s.length()-1;i<j;i++,j--){
if(s.charAt(i)!=s.charAt(j)) return false;
}
return true;
}
}
47,全排列||
https://leetcode-cn.com/problems/permutations-ii/submissions/
class Solution {
List<List<Integer>> res = new ArrayList();
LinkedList<Integer> path = new LinkedList();
public List<List<Integer>> permuteUnique(int[] nums) {
// 标记已经使用过的元素
boolean[] used = new boolean[nums.length];
Arrays.fill(used, false);
Arrays.sort(nums);
backTrack(nums,used);
return res;
}
private 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++){
// 从前到后的11和从后到前的11是一样的,需去重,控制使用顺序即可
if(i>0 && nums[i]==nums[i-1] && used[i-1]==false) continue;
// 横向去重
if(used[i]==true) continue;
used[i] = true;
path.add(nums[i]);
backTrack(nums,used);
path.removeLast();
used[i]=false;
}
}
}
3,总结
递归模板:
//全局变量res,存储结果集
//全局变量path,存储递归路径
void backTrack(选择数组,起始索引){
if(终止条件){
res.add(path)//存放结果
return;//回退
}
//纵向遍历和横向遍历同时进行
for(选择;索引需要参与){
//处理,满足即添加
path.add(选择);
backTrack(选择数组,下一索引);
//遍历到叶子节点后,开始回溯,撤销选择
path.removeLast();
}
}
关于组合问题
(存在顺序,需要借助index标记上次访问过的位置,其实横向纵向都会标记到)
题号 | 思路 | 题目 |
---|---|---|
39组合 | 排序,然后index不需自增 | 无重复,元素组合总和 |
40组合 ii | 排序,横向去重 | 有重复,元素组合总和 |
关于排列问题
(不存在顺序,所以不需要标记index;也正因为如此,会存在元素重复使用问题)
题号 | 思路 | 题目 |
---|---|---|
46全排列 | 借助标记,跳过纵向时已使用过的元素 | 无重复元素,排列 |
47全排列ii | 除了纵向,横向也需要去重,即当前元素和前一个元素相等时需要请一个元素同时不被使用 | 有重复元素,排列 |
关于子集问题
题号 | 思路 | 题目 |
---|---|---|
78子集 | 遍历递归树所有节点 | 无重复元素,集问题 |
90子集ii | 先排序再去重,横向去重,选择列表前个不等于后个 | 有重复元素,子集问题 |