数据结构与算法之深入解析“组合总和II”的求解思路与算法示例
Posted Serendipity·y
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法之深入解析“组合总和II”的求解思路与算法示例相关的知识,希望对你有一定的参考价值。
一、题目要求
- 给你一个由候选元素组成的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
- candidates 中的每个元素在每个组合中只能使用一次 。
- 注意:解集不能包含重复的组合。
- 示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
- 示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
- 提示:
-
- 1 <= candidates.length <= 100
-
- 1 <= candidates[i] <= 50
-
- 1 <= target <= 30
二、求解算法
① 回溯法
- 这道题与【数据结构与算法】之深入解析“组合总和”的求解思路与算法示例 的区别在于:组合总和中的 candidates 中的数字可以无限制重复被选取;而本题中的 candidates 中的每个数字在每个组合中只能使用一次;但它们的相同点是:相同数字列表的不同排列视为一个结果。那么,最重要的就是如何去掉重复的集合。
- 为了使得解集不包含重复的组合,有以下 2 种方案:
-
- 使用哈希表天然的去重功能,但是编码相对复杂;
-
- 这里使用和“组合总和”类似的思路:不重复就需要按顺序搜索, 在搜索的过程中检测分支是否会出现重复结果 。
-
- 注意:这里的顺序不仅仅指数组 candidates 有序,还指按照一定顺序搜索结果。
- 我们知道,数组 candidates 有序,也是深度优先遍历过程中实现「剪枝」的前提。将数组先排序的思路来自于这个问题:去掉一个数组中重复的元素。很容易想到的方案是:先对数组升序排序,重复的元素一定不是排好序以后相同的连续数组区域的第 1 个元素。也就是说,剪枝发生在:同一层数值相同的结点第 2、3 … 个结点,因为数值相同的第 1 个结点已经搜索出了包含了这个数值的全部结果,同一层的其它结点,候选数的个数更少,搜索出的结果一定不会比第 1 个结点更多,并且是第 1 个结点的子集。
- Java 示例:
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
public class Solution
public List<List<Integer>> combinationSum2(int[] candidates, int target)
int len = candidates.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0)
return res;
// 关键步骤
Arrays.sort(candidates);
Deque<Integer> path = new ArrayDeque<>(len);
dfs(candidates, len, 0, target, path, res);
return res;
/**
* @param candidates 候选数组
* @param len 冗余变量
* @param begin 从候选数组的 begin 位置开始搜索
* @param target 表示剩余,这个值一开始等于 target,基于题目中说明的"所有数字(包括目标数)都是正整数"这个条件
* @param path 从根结点到叶子结点的路径
* @param res
*/
private void dfs(int[] candidates, int len, int begin, int target, Deque<Integer> path, List<List<Integer>> res)
if (target == 0)
res.add(new ArrayList<>(path));
return;
for (int i = begin; i < len; i++)
// 大剪枝:减去 candidates[i] 小于 0,减去后面的 candidates[i + 1]、candidates[i + 2] 肯定也小于 0,因此用 break
if (target - candidates[i] < 0)
break;
// 小剪枝:同一层相同数值的结点,从第 2 个开始,候选数更少,结果一定发生重复,因此跳过,用 continue
if (i > begin && candidates[i] == candidates[i - 1])
continue;
path.addLast(candidates[i]);
// 调试语句 ①
// System.out.println("递归之前 => " + path + ",剩余 = " + (target - candidates[i]));
// 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
dfs(candidates, len, i + 1, target - candidates[i], path, res);
path.removeLast();
// 调试语句 ②
// System.out.println("递归之后 => " + path + ",剩余 = " + (target - candidates[i]));
public static void main(String[] args)
int[] candidates = new int[]10, 1, 2, 7, 6, 1, 5;
int target = 8;
Solution solution = new Solution();
List<List<Integer>> res = solution.combinationSum2(candidates, target);
System.out.println("输出 => " + res);
- 打开上面的调试语句,针对输入 int[] candidates = new int[]10, 1, 2, 7, 6, 1, 5; 和 int target = 8; 控制台输出如下:
递归之前 => [1],剩余 = 7
递归之前 => [1, 1],剩余 = 6
递归之前 => [1, 1, 2],剩余 = 4
递归之后 => [1, 1],剩余 = 4
递归之前 => [1, 1, 5],剩余 = 1
递归之后 => [1, 1],剩余 = 1
递归之前 => [1, 1, 6],剩余 = 0
递归之后 => [1, 1],剩余 = 0
递归之后 => [1],剩余 = 6
递归之前 => [1, 2],剩余 = 5
递归之前 => [1, 2, 5],剩余 = 0
递归之后 => [1, 2],剩余 = 0
递归之后 => [1],剩余 = 5
递归之前 => [1, 5],剩余 = 2
递归之后 => [1],剩余 = 2
递归之前 => [1, 6],剩余 = 1
递归之后 => [1],剩余 = 1
递归之前 => [1, 7],剩余 = 0
递归之后 => [1],剩余 = 0
递归之后 => [],剩余 = 7
递归之前 => [2],剩余 = 6
递归之前 => [2, 5],剩余 = 1
递归之后 => [2],剩余 = 1
递归之前 => [2, 6],剩余 = 0
递归之后 => [2],剩余 = 0
递归之后 => [],剩余 = 6
递归之前 => [5],剩余 = 3
递归之后 => [],剩余 = 3
递归之前 => [6],剩余 = 2
递归之后 => [],剩余 = 2
递归之前 => [7],剩余 = 1
递归之后 => [],剩余 = 1
输出 => [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]
- C++ 示例:
// author:rmokerone
#include <iostream>
#include <vector>
using namespace std;
class Solution
private:
vector<int> candidates;
vector<vector<int>> res;
vector<int> path;
public:
void DFS(int start, int target)
if (target == 0)
res.push_back(path);
return;
for (int i = start; i < candidates.size() && target - candidates[i] >= 0; i++)
if (i > start && candidates[i] == candidates[i - 1])
continue;
path.push_back(candidates[i]);
// 元素不可重复利用,使用下一个即i+1
DFS(i + 1, target - candidates[i]);
path.pop_back();
vector<vector<int>> combinationSum2(vector<int> &candidates, int target)
sort(candidates.begin(), candidates.end());
this->candidates = candidates;
DFS(0, target);
return res;
;
② 递归 + 回溯(LeetCode 官方解法)
- 由于需要求出所有和为 target 的组合,并且每个数只能使用一次,因此可以使用递归 + 回溯的方法来解决这个问题。
- 用 dfs(pos,rest) 表示递归的函数,其中 pos 表示当前递归到了数组 candidates 中的第 pos 个数,而 rest 表示还需要选择和为 rest 的数放入列表作为一个组合;
- 对于当前的第 pos 个数,有两种方法:选或者不选。如果选了这个数,那么调用 dfs(pos+1,rest−candidates[pos]) 进行递归,注意这里必须满足 rest≥candidates[pos];如果不选这个数,那么调用 dfs(pos+1,rest) 进行递归;
- 在某次递归开始前,如果 rest 的值为 0,说明找到了一个和为 target 的组合,将其放入答案中。每次调用递归函数前,如果选了那个数,就需要将其放入列表的末尾,该列表中存储了我们选的所有数。在回溯时,如果我们选了那个数,就要将其从列表的末尾删除。
- 上述算法就是一个标准的递归 + 回溯算法,但是它并不适用于本题。这是因为题目描述中规定了解集不能包含重复的组合,而上述的算法中并没有去除重复的组合,例如当 candidates=[2,2],target=2 时,上述算法会将列表 [2] 放入答案两次。
- 因此,需要改进上述算法,在求出组合的过程中就进行去重的操作,可以考虑将相同的数放在一起进行处理,也就是说,如果数 x 出现了 y 次,那么在递归时一次性地处理它们,即分别调用选择 0,1,⋯,y 次 x 的递归函数,这样就不会得到重复的组合。具体地:
-
- 使用一个哈希映射(HashMap)统计数组 candidates 中每个数出现的次数。在统计完成之后,将结果放入一个列表 freq 中,方便后续的递归使用。列表 freq 的长度即为数组 candidates 中不同数的个数,其中的每一项对应着哈希映射中的一个键值对,即某个数以及它出现的次数。
-
- 在递归时,对于当前的第 pos 个数,它的值为 freq[pos][0],出现的次数为 freq[pos][1],那么可以调用:
-
- 即选择了这个数 i 次,这里 i 不能大于这个数出现的次数,并且 i×freq[pos][0] 也不能大于 rest。同时,需要将 i 个 freq[pos][0] 放入列表中。
- 这样一来,就可以不重复地枚举所有的组合了,我们还可以进行什么优化(剪枝)呢?一种比较常用的优化方法是,将 freq 根据数从小到大排序,这样我们在递归时会先选择小的数,再选择大的数。这样做的好处是,当我们递归到 dfs(pos,rest) 时,如果 freq[pos][0] 已经大于 rest,那么后面还没有递归到的数也都大于 rest,这就说明不可能再选择若干个和为 rest 的数放入列表了。此时,就可以直接回溯。
- C++ 示例:
class Solution
private:
vector<pair<int, int>> freq;
vector<vector<int>> ans;
vector<int> sequence;
public:
void dfs(int pos, int rest)
if (rest == 0)
ans.push_back(sequence);
return;
if (pos == freq.size() || rest < freq[pos].first)
return;
dfs(pos + 1, rest);
int most = min(rest / freq[pos].first, freq[pos].second);
for (int i = 1; i <= most; ++i)
sequence.push_back(freq[pos].first);
dfs(pos + 1, rest - i * freq[pos].first);
for (int i = 1; i <= most; ++i)
sequence.pop_back();
vector<vector<int>> combinationSum2(vector<int>& candidates, int target)
sort(candidates.begin(), candidates.end());
for (int num: candidates)
if (freq.empty() || num != freq.back().first)
freq.emplace_back(num, 1);
else
++freq.back().second;
dfs(0, target);
return ans;
;
- Java 示例:
class Solution
List<int[]> freq = new ArrayList<int[]>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> sequence = new ArrayList<Integer>();
public List<List<Integer>> combinationSum2(int[] candidates, int target)
Arrays.sort(candidates);
for (int num : candidates)
int size = freq.size();
if (freq.isEmpty() || num != freq.get(size - 1)[0])
freq.add(new int[]num, 1);
else
++freq.get(size - 1)[1];
dfs(0, target);
return ans;
public void dfs(int pos, int rest)
if (rest == 0)
ans.add(new ArrayList<Integer>(sequence));
return;
if (pos == freq.size() || rest < freq.get(pos)[0])
return;
dfs(pos + 1, rest);
int most = Math.min(rest / freq.get(pos)[0], freq.get(pos)[1]);
for (int i = 1; i <= most; ++i)
sequence.add(freq.get(pos)[0]);
dfs(pos + 1, rest - i * freq.get(pos)[0]);
for (int i = 1; i <= most; ++i) 以上是关于数据结构与算法之深入解析“组合总和II”的求解思路与算法示例的主要内容,如果未能解决你的问题,请参考以下文章
数据结构与算法之深入解析“股票的最大利润”的求解思路与算法示例