Leetcode 刷题笔记(十九) ——回溯算法篇之组合问题

系列文章目录

一、 数组类型解题方法一:二分法
二、数组类型解题方法二:双指针法
三、数组类型解题方法三:滑动窗口
四、数组类型解题方法四:模拟
五、链表篇之链表的基础操作和经典题目
六、哈希表篇之经典题目
七、字符串篇之经典题目
八、字符串篇之 KMP
九、解题方法:双指针
十、栈与队列篇之经典题目
十 一、栈与队列篇之 top-K 问题
十 二、二叉树篇之二叉树的前中后序遍历
十 三、二叉树篇之二叉树的层序遍历及相关题目
十 四、二叉树篇之二叉树的属性相关题目
十 五、 二叉树篇之二叉树的修改与构造
十 六、 二叉树篇之二叉搜索树的属性
十 七、二叉树篇之公共祖先问题
十 八、二叉树篇之二叉搜索树的修改与构造
更新中 …


前言

刷题路线来自 :代码随想录
组合问题:N个数里面按一定规则找出k个数的集合

回溯算法是一个纯暴力的搜索方法,是树形结构时 DFS (深度优先搜索) 的一种,本文用来解决通过暴力多层循环遍历,因循环的层数变化正常的暴力解无法写出的组合类型问题。同样也是穷举所有结果,但是在这过程中可以进行剪枝。
在回溯算法中通常使用全局变量记录路径和状态。在向下递归的过程中记录的路径和状态,在一条路径递归完,下层递归返回上层递归后,需要进行回溯(将状态回退到进入下层递归之前)。

题录

77. 组合

Leetcode 链接
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。

在这里插入图片描述
题解:

class Solution {
    List<List<Integer>> res = new ArrayList<>();  
    List<Integer> path = new ArrayList<>();       
    public List<List<Integer>> combine(int n, int k) {
        backtracking(n, k, 1);
        return res;
    }
    public void backtracking(int n, int k, int start) {
    	// 结束条件:path.size() == k
        if (path.size() == k) {
        	// 添加到结果集,并返回
            res.add(new ArrayList<>(path));
            return;
        }
		
        for (int i = start; i <= n; i++) {
            path.add(i);
            // 因为整数不能重复,下层递归需要从本层递归起始位置 i 加一位置开始
            backtracking(n, k, i + 1);
            // 回溯,移除本次递归之前添加的值
            path.remove(path.size() - 1);
        }
    }
}

剪枝优化:
已经储存的元素个数:path.size()
还需要找到元素个数:k - path.size()
起始位置至多从:n - (k - path.size()) + 1 开始遍历

       ...
       for (int i = start; i <= n - (k - path.size()) + 1; i++) {
       	  ...
       }

216. 组合总和 III

Leetcode 链接
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
在这里插入图片描述
题解:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    int sum = 0;
    public List<List<Integer>> combinationSum3(int k, int n) {
        backtracking(k, n, 1);
        return res;
    }
    public void backtracking(int k, int n, int start) {
    	// 结束条件
        if (path.size() > k || sum > n) {
        	// 和大于 n 或者 收集的元素个数大于 k
            return;
        }
        if (path.size() == k && sum == n) {
        	// 已经收集到 k 个数,并且和等于 n
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = start; i <= 9 - (k - path.size()) + 1; i++) {
            path.add(i);
            sum += i;
			// 数字不重复,下轮递归从 i + 1 开始
            backtracking(k, n, i + 1);
			// sum 和 path 都要回溯
            path.remove(path.size() - 1);
            sum -= i;
        }
    }
}

不使用全局变量的方式保存 sum ,将 sum 作为递归的参数隐藏回溯,代码更简洁

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> combinationSum3(int k, int n) {
        int sum = 0;
        backtracking(k, n, 1, sum);
        return res;
    }
    public void backtracking(int k, int n, int start, int sum) {
        if (path.size() > k || sum > n) {
            return;
        }
        if (path.size() == k && sum == n) {
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = start; i <= 9 - (k - path.size()) + 1; i++) {
            path.add(i);
           //sum += i;
           // 将 sum + i 作为参数,传入下层递归
            backtracking(k, n, i + 1, sum + i);
            path.remove(path.size() - 1);
            // 当将 sum + i 作为参数时,本层 sum 并未改变所,回到本层后就不用回溯
            //sum -= i;
        }
    }
}

39. 组合总和

Leetcode 链接
在这里插入图片描述
题解:
为了代码简洁 sum 依然放在递归参数中

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backtracking(candidates, target, 0, 0);
        return res;
    }

    public void backtracking(int[] nums, int target, int start, int sum) {
        if (sum > target) {
            return;
        }
        if (sum == target) {
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = start; i < nums.length; i++) {
            path.add(nums[i]);
            // 一个数可以使用多次,下标 i 用加一
            backtracking(nums, target, i, sum + nums[i]);
            path.remove(path.size() - 1);
        }
    }
}

40. 组合总和 II

Leetcode 链接
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
在这里插入图片描述
题解:
先排序,可以让相同的数字在一起
因为数组中有重复数字,二返回列表中不允许重复组合的存在如:
数组: nums [1,1,2,3] target = 4
[1,3] 和 [1,3],第二个 1 因为和前一个 1 重复,所以第二个 [1,3] 显然需要剪枝,但是半段条件不能只有 nums [i - 1] = nums [ i ],再看 [1,1,2],在第一个 1 进入第二层递归是这里的第二个 1 是有用的,因为这里的 1 是本层递归的起始位置

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    	// 排序
        Arrays.sort(candidates);
        backtracking(candidates, target, 0, 0);
        return res;
    }
    
    public void backtracking(int[] nums, int target, int start, int sum) {
// 此处结束条件放在上层的 for 循环判断条件中了,简洁代码,并减少了递归次数
//        if (sum > target) {
//            return;
//        }
		// 结束条件
        if (sum == target) {
        	// 添加并 return
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = start; i < nums.length && sum + nums[i] <= target; i++) {
            if (i > start && nums[i] == nums[i - 1]) {
            	// 不是起始位置,要是和前一个数重复了,直接跳过
                continue;
            }
            path.add(nums[i]);
            backtracking(nums, target, i + 1, sum + nums[i]);
            // 回溯
            path.remove(path.size() - 1);
        }
    }
}

17. 电话号码的字母组合

Leetcode 链接
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
在这里插入图片描述
题解:

  1. 数组保存数字与字符串的映射关系
  2. 每层递归的字符串与递归的深度有关,而递归的深度由 digits 的下标决定,所以参数列表需要一个 deep 来记录
  3. 每次遍历前需要先得到该层递归要遍历的字符串,也就是递归的深度对应的数字的映射字符串
class Solution {
    List<String> res = new ArrayList<>();
    // 这里的 sb 也可以作为递归的参数,隐藏回溯
    StringBuilder sb = new StringBuilder();
    public List<String> letterCombinations(String digits) {
        if (digits.equals("") || digits == null) {
            return res;
        }
        // 数组保存映射关系
        String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        backtracking(digits, numString, 0);
        return res;
    }
    public void backtracking(String digits, String[] numString, int deep) {
    	// 结束条件
        if (deep == digits.length()) {
        	// 添加到结果集并返回
            res.add(sb.toString());
            return;
        }
        // 映射数组是从 0 下标开始,digits.charAt(deep) 得到数字的字符形式,
        // digits.charAt(deep) - '0' 得到相应下标
        String s = numString[digits.charAt(deep) - '0'];
        for (int i = 0; i < s.length(); i++) {
            sb.append(s.charAt(i));
            backtracking(digits, numString, deep + 1);
            // 回溯
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}

总结

回溯模板:

public void backtracking(参数) {
	if (终止条件) {
		// 存放结果并返回
		添加到返回列表中;
		return;
	}
	if (其他终止条件) {
		// 直接返回,也可以放到 for 循环中判断
		return;
	}

	for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
		处理结点;
		backtracking(路径,选择列表);
		回溯,撤销处理结果;
	}
}
  1. 返回列表一般作为全局变量,因为简洁,减少递归的参数列表
  2. 类似 sum、字符串等需要在递归过程中动态维护,并回溯的,除作为全局变量外,也可以作为递归的参数,隐藏回溯。
  3. 剪枝通常修改 for 循环结束条件类似上边几道题
  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值