回溯算法总结

总结

(感谢代码随想录博主 我回溯就在他那学的)
回溯算法模板 看到没有 这个模板不特么就是一种尝试吗?

三部曲:

回溯函数模板返回值以及参数

回溯函数终⽌条件

回溯搜索的遍历过程

回溯法其遍历过程就是:for循环横向遍历,递归纵向遍历,回溯不断调整结果集

void backtracking(参数) {
 	if (终⽌条件) {
 		存放结果;
 		return;
 	}
 	for (选择:本层集合中元素(树中节点孩⼦的数量就是集合的⼤⼩)) {
 		处理节点;
 		backtracking(路径,选择列表); // 递归
 		回溯,撤销处理结果
 	}	
}

组合问题和排列问题是在树形结构的叶⼦节点上收集结果

子集问题就是取树上所有节点的结果

1.组数总和问题

1.组合(k次全排列)

组合1

在这里插入图片描述

 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次)

组合3

  	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);
        }
    }

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值