总结
(感谢代码随想录博主 我回溯就在他那学的)
回溯算法模板 看到没有 这个模板不特么就是一种尝试吗?
三部曲:
回溯函数模板返回值以及参数
回溯函数终⽌条件
回溯搜索的遍历过程
回溯法其遍历过程就是:for循环横向遍历,递归纵向遍历,回溯不断调整结果集
void backtracking(参数) {
if (终⽌条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩⼦的数量就是集合的⼤⼩)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合问题和排列问题是在树形结构的叶⼦节点上收集结果
子集问题就是取树上所有节点的结果
1.组数总和问题
1.组合(k次全排列)
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
digui(new ArrayList<>(),n,1,k);
return res;
}
private void digui(ArrayList<Integer> list, int max, int startIndex, int k) {
if(k==0){//终止条件 存放结果
res.add(new ArrayList<>(list));//防止指针改变
return;
}
for(int i=startIndex;i<=max;i++){
/*剪枝优化 如果发现剩下的个数小于了需要的个数 就不需要继续循环了
if(max-i < k-list.size()-1)
return;
*/
list.add(i);//处理节点;
digui(list,max,i+1,k-1);//进行递归
list.remove(list.size()-1);//回溯,撤销处理结果
}
}
2.组合总和3(target和k次)
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
back(new ArrayList<Integer>(),n,k,1,0);
return res;
}
public void back(ArrayList<Integer> list, int target, int k, int startIndex,int sum){
if(k==0){ //base case
if(sum == target)//当前累加和到达了3个 就添加
res.add(new ArrayList<>(list));
return;
}
// 剪枝操作 已选元素总和如果已经⼤于n 往后遍历就没有意义了
if (sum > target) return;
//说明k不等于0 那么横向循环
for (int i = startIndex; i <=9; i++) {
list.add(i);//处理节点
back(list,target,k-1,i+1,sum+i);//进行递归
list.remove(list.size()-1);//回溯
}
}
3.组合总和(target和无重复数组元素可以多次选取)
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backFun(new ArrayList<Integer>(),candidates,target,0,0);
return res;
}
private void backFun(ArrayList<Integer> list, int[] arr, int target, int startIndex,int sum) {
//剪枝 如果sum已经大于了目标 则 不需要后面的循环了
if(sum>target) return;
if(sum == target){ //base case 添加元素
res.add(new ArrayList<>(list));
return;
}
//说明sum不够
for (int i = startIndex; i <arr.length ; i++) {
list.add(arr[i]);//处理当前元素
//递归 注意 这里说可以重复选取 则不需要从i+1开始循环了 但是还要去掉一样的组合 还是需要startIn
backFun(list,arr,target,i,sum+arr[i]);
list.remove(list.size()-1);//回溯 撤销处理结果
}
}
4.组合总和(target和有重复数组元素不可以多次选取)
力扣:数组有重复元素,但还不能有重复的组合 比上面更难 需要一个标志数组
used[i - 1] == true,说明同⼀树⽀candidates[i - 1]使⽤过
used[i - 1] == false,说明同⼀树层candidates[i - 1]使⽤过
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
//先排序 让相邻的元素连在一起
Arrays.sort(candidates);
backFun2(new ArrayList<>(),candidates,target,0,0,new boolean[candidates.length]);
return res;
}
//boolean[] used来判断前一个是否使用过
private void backFun2(ArrayList<Integer> list, int[] arr, int target, int startIndex, int sum, boolean[] used) {
//剪枝 如果sum已经大于了目标 则 不需要后面的循环了
if(sum>target) return;
if(sum == target){ //base case 添加元素
res.add(new ArrayList<>(list));
return;
}
//说明sum不够
for (int i = startIndex; i <arr.length ; i++) {
// 要对同⼀树层使⽤过的元素进⾏跳过
if (i > 0 && arr[i] == arr[i - 1] && used[i - 1] == false)
continue;
used[i]=true;
list.add(arr[i]);//处理当前元素
//递归 注意 这里说不可以重复选取 则需要从i+1开始
backFun2(list,arr,target,i+1,sum+arr[i],used);
list.remove(list.size()-1);//回溯 撤销处理结果
used[i]=false;
}
}
List<List<Integer>> res=new ArrayList<>();
2.电话号码的字母组合
List<String> res=new ArrayList<>();
public List<String> letterCombinations(String digits) {
if(digits.length()==0) return res;
back(new StringBuilder(),digits,0);
return res;
}
//数字映射表 为了方便下标 我把0 1 也弄进来
String[] map = {"","",
"abc", "def", "ghi", "jkl", "mno", "pqrs","tuv", "wxyz"};
// 2 3 4 5 6 7 8 9
private void back(StringBuilder sb, String str, int startIndex) {
//base case 来到末尾了 就该进行结算了
if(startIndex==str.length()){
//只有满足要求--》拼凑的长度==必须的长度 才添加
if(sb.length()==str.length())
res.add(sb.toString());
return;
}
for (int i = startIndex; i < str.length(); i++) {
int curNum=str.charAt(i)-'0';
String curString = map[curNum];//获得当前下标对应的字符串
for (int j = 0; j < curString.length(); j++) {
sb.append(curString.charAt(j));//处理节点 添加当前字符
back(sb,str,i+1);//递归
sb.delete(sb.length()-1,sb.length());//回溯 删除当前添加的
}
}
}
3.分割字符串
List<List<String>> res=new ArrayList<>();
public List<List<String>> partition(String s) {
digui(new ArrayList<>(),s,0);
return res;
}
private void digui(ArrayList<String> list, String s, int startIndex) {
//base case 切割完毕
if(startIndex==s.length()){
res.add(new ArrayList<>(list));
return;
}
for (int i = startIndex; i <s.length() ; i++) {
//如果当前不是回文串 跳过
if(!isPalindrome(s,startIndex,i)) continue;
//是回文串 就添加
String str = s.substring(startIndex, i+1);
list.add(str);
digui(list,s,i+1);
list.remove(list.size()-1);
}
}
public boolean isPalindrome(String s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--)
if (s.charAt(i) != s.charAt(j))
return false;
return true;
}
4.复原IP地址(牛客重点)
List<String> res=new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
//IP地址总长度超了或者不够,无法转换返回
if(s.length()==0 || s.length()>12) return res;
back(new StringBuilder(),s,0,0);
return res;
}
private void back(StringBuilder sb, String str, int startIndex,int pointNum) {
//当有4个标点 且到达了末尾
if(pointNum == 4 && startIndex==str.length()){
res.add(sb.toString());
return;
}
int curLength=sb.length();//当前字符串长度 方便回溯的时候 方便处理字符串和.(就是这回溯我卡了很久)
for (int i = startIndex; i <str.length() ; i++) {
String cur = str.substring(startIndex, i + 1);//substring方法是左闭右开
//如果这部分不合法 说明之前做的决定是错的 直接返回上一层
if(!isValid(cur)) break;
sb.append(cur);//添加字符串
if(i != str.length()-1)
sb.append(".");//没有到底末尾 说明可以添加小数点
back(sb,str,i+1,pointNum+1);//递归
sb.setLength(curLength);//回溯 很精髓 直接赋值截取长度
}
}
//验证这部分字符串是否合法
boolean isValid(String s) {
// 0开头的数字不合法
if (s.charAt(0) == '0' && 0 != s.length()-1) return false;
int num = 0;
for (int i = 0; i < s.length(); i++) {
num = num * 10 + (s.charAt(i) - '0');
if (num > 255) // 如果⼤于255了不合法
return false;
}
return true;
}
5.子集
1.无重合数组子集
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
digui(new ArrayList<>(),nums,0);
return res;
}
private void digui(ArrayList<Integer> list, int[] nums, int startIndex) {
//不需要结束判断 因为 是全排列 每次到了该次情况 都可以添加
res.add(new ArrayList<>(list));
for (int i = startIndex; i < nums.length; i++) {
list.add(nums[i]);
digui(list,nums,i+1);
list.remove(list.size()-1);
}
}
2.有重合数子集
//子集 有重复元素 需要去重
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
back(new ArrayList<>(),nums,0,new boolean[nums.length]);
return res;
}
private void back(ArrayList<Integer> list, int[] nums, int startIndex, boolean[] used) {
res.add(new ArrayList<>(list));
for (int i = startIndex; i < nums.length; i++) {
if(i>0 && nums[i-1]==nums[i] && used[i-1]==false)
continue;
list.add(nums[i]);
used[i]=true;
back(list,nums,i+1, used);
list.remove(list.size()-1);
used[i]=false;
}
}
6.所有的递增子序列
力扣:与前面的组数总和的去重不一样 前面的可以排序 而这道题求的是序列!无法排序 所以前面那种used[i-1]==false就不能这么搞了 但是去重都是一样 都是把该层重复的去掉。
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
digui(new ArrayList<Integer>(),nums,0);
return res;
}
private void digui(ArrayList<Integer> list, int[] nums, int startIndex) {
//长度超过了2就添加 但是 不要return
if(list.size()>=2) res.add(new ArrayList<>(list));
HashSet<Integer> set = new HashSet<>();//记录该层的元素是否使用过 注意是该层!
for (int i = startIndex; i < nums.length; i++) {
int last=Integer.MIN_VALUE;
if(list.size()>0)
last = list.get(list.size() - 1);//获得该集合的最后一个元素
//如果当前值 比集合最大的还大 且 没有使用过 就添加 否则下一个循环
if(last<=nums[i] && !set.contains(nums[i])){
list.add(nums[i]);
set.add(nums[i]); // 记录这个元素在本层⽤过了,本层后⾯不能再⽤了 不需要
digui(list,nums,i+1);
list.remove(list.size()-1);
}
}
}
7.全排列
排列问题的不同: 每层都是从0开始搜索而不是startIndex 需要used数组记录list⾥都放了哪些元素了
1.无重复字符全排列
List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
digui(new ArrayList<>(),nums,new boolean[nums.length]);
return res;
}
private void digui(ArrayList<Integer> list, int[] arr, boolean[] used) {
if(list.size() == arr.length){
res.add(new ArrayList<>(list));
return;
}//注意因为是全排列 下标需要从0开始 startIndex就没啥用了 所以还要加个used判断当前层数字是否被使用过 eg 123 {1}下次再从0开始的时候 发现1:used[0]==true 则直接跳过了 来到2发现used[1]==false 则收集{1,2} 然后继续置为true 下次来到下标0 1 发现都为true 来到下标2 used[2]==false 收集变为{1,2,3}
for (int i = 0; i < arr.length; i++) {
if(used[i]==true) continue;//⼀个排列⾥⼀个元素只能使⽤⼀次
list.add(arr[i]);
used[i]=true;
digui(list,arr,used);
list.remove(list.size()-1);
used[i]=false;
}
}
2.有重复字符全排列
List<List<Integer>> res=new ArrayList<>();
//有重复字符全排列
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
digui1(new ArrayList<>(),nums,new boolean[nums.length]);
return res;
}
private void digui1(ArrayList<Integer> list, int[] nums, boolean[] used) {
if(list.size()==nums.length){
res.add(new ArrayList<>(list));
return;
}
for (int i = 0; i < nums.length; i++) {
//这一层已经有相同元素开始的了 且和前面那个数相等 跳过
if(i>0 && nums[i-1]==nums[i] && used[i-1]==false) continue;
//这里主要是防止全排列的时候 遍历到了自己
if(used[i]==false){
list.add(nums[i]);//处理
used[i]=true;//将该元素置为false
digui1(list,nums,used);//递归
list.remove(list.size()-1);//回溯
used[i]=false;//回溯
}
}
}
8.N皇后
private int res=0;// 找到的方案数
public int totalNQueens(int n) {
dfs(new int[n], 0);
return res;
}
/**
* nums: 每一行放在哪个位置,如nums[0] = 4,表示第0行的皇后放在第4列(把行列互换来理解也一样)
* curRow: 当前已经放了几行
*/
public void dfs(int[] nums, int curRow) {
// cur == n 表示每一行都已经放了皇后,说明找到了一种方案
if (curRow == nums.length) {
res++;
return;
}
// 找到可访问的位置
boolean[] visited = new boolean[nums.length];
// 遍历之前的每一行(均已放置皇后),把当前行中所有会和之前的皇后冲突的位置都排除掉
// i表示判断到第几行,易知第i行的皇后在(i, nums[i])上
for (int i = 0; i < curRow; i++) {
// e表示第i行到当前行的距离,要根据这个距离来判断斜向冲突的位置
int e = curRow - i;
// v表示第i行的皇后放在哪一列,这一列需要被排除掉(visited[v] = true;)
int v = nums[i];
// r表示右下方向发生冲突的列, l表示左下方向发生冲突的列
// 比如num[0] = 3, curRow = 2这就说明第0行的皇后放在(0,3)
// 那么在当前行也就是第2行,(2, 1)和(2, 5)就与(0,3)在同一斜向
// 即r = v + e = num[0] + (cur-i) = 3 + (2-0) = 5
// l = v - e = num[0] - (cur-i) = 3 - (2-0)= 1
int r = v + e;
int l = v - e;
visited[v] = true;
if (l >= 0) visited[l] = true;
if (r < nums.length) visited[r] = true;
}
// 对当前行剩余所有可放皇后的位置,进行递归
for (int i = 0; i < nums.length; i++) {
// visited[i]表示当前行的第i列和已放的皇后冲突,跳过
if (visited[i]) continue;
nums[curRow] = i;
dfs(nums, curRow + 1);
}
}