LeetCode分类刷题--回溯

本文详细介绍了如何使用回溯算法解决LeetCode中的组合、子集、排列等问题,包括77. 组合、216. 组合总和 III、17. 电话号码的字母组合等题目。通过实例解析了回溯算法的通用模板,以及在不同问题中的应用和剪枝策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

回溯

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循环从startn,纵向递归的下一层从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为字符串格式,每组数字中间要加.,可以设pathStringBuilder类型
在这里插入图片描述

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题解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值