数据结构与算法之深入解析“组合总和III”的求解思路与算法示例
Posted Serendipity·y
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法之深入解析“组合总和III”的求解思路与算法示例相关的知识,希望对你有一定的参考价值。
一、题目要求
- 找出所有相加之和为 n 的 k 个数的组合,组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
- 说明:
-
- 所有数字都是正整数。
-
- 解集不能包含重复的组合。
- 示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
- 示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
二、求解算法
① 回溯+剪枝
- 本题就是在 [1,2,3,4,5,6,7,8,9] 这个集合中找到和为 n 的 k 个数的组合,相对于 【数据结构与算法】之N个数中有K个数可能的组合算法 ,无非就是多了一个限制,要找到和为 n 的 k 个数的组合,而整个集合已经是固定 [1,…,9],理解了N个数中有K个数可能的组合算法之后本题就比较简单了。
- k 相当于了树的深度,9(因为整个集合就是9个数)就是树的宽度。例如 k = 2,n = 4 的话,就是在集合[1,2,3,4,5,6,7,8,9] 中求 k(个数) = 2,n(和) = 4 的组合。
- 选取过程如下图所示,可以看出,只有最后取到集合(1,3)和为 4 符合条件:
- 回溯三部曲:
-
- 确定递归函数参数:
-
-
- 和 【数据结构与算法】之N个数中有K个数可能的组合算法 一样,依然需要一维数组 path 来存放符合条件的结果,二维数组 result 来存放结果集,这里依然定义 path 和 result 为全局变量,至于为什么取名为 path?从上面树形结构中,可以看出,结果其实就是一条根节点到叶子节点的路径。
-
vector<vector<int>> result; // 存放结果集
vector<int> path; // 符合条件的结果
-
-
- 接下来还需要如下参数:
-
-
-
-
- targetSum(int)目标和,也就是题目中的 n;
-
-
-
-
-
- k(int)就是题目中要求 k 个数的集合;
-
-
-
-
-
- sum(int)为已经收集的元素的总和,也就是 path 里元素的总和;
-
-
-
-
-
- startIndex(int)为下一层 for 循环搜索的起始位置。
-
-
-
-
- 所以代码如下:
-
vector<vector<int>> result;
vector<int> path;
void backtracking(int targetSum, int k, int sum, int startIndex)
-
-
- 其实这里 sum 这个参数也可以省略,每次 targetSum 减去选取的元素数值,然后判断如果 targetSum 为 0,说明收集到符合条件的结果,这里为了直观便于理解,还是加一个 sum 参数。还要强调一下,回溯法中递归函数参数很难一次性确定下来,一般先写逻辑,需要啥参数,再填什么参数。
-
-
- 确定终止条件:
-
-
- k 其实就已经限制树的深度,因为就取 k 个元素,树再往下深了没有意义,所以如果 path.size() 和 k 相等,就终止;
-
-
-
- 如果此时 path 里收集到的元素和(sum) 和 targetSum(就是题目描述的 n)相同,就用 result 收集当前的结果。
-
-
-
- 所以终止代码如下:
-
if (path.size() == k)
if (sum == targetSum) result.push_back(path);
return; // 如果path.size() == k 但sum != targetSum 直接返回
-
- 单层搜索过程:
-
-
- 和【数据结构与算法】之N个数中有K个数可能的组合算法 区别之一就是集合固定的就是 9 个数 [1,…,9],所以 for 循环固定 i<=9:
-
-
-
- 处理过程就是 path 收集每次选取的元素,相当于树型结构里的边,sum 来统计 path 里元素的总和:
-
for (int i = startIndex; i <= 9; i++)
sum += i;
path.push_back(i);
backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
sum -= i; // 回溯
path.pop_back(); // 回溯
-
- 别忘了处理过程和回溯过程是一一对应的,处理有加,回溯就要有减,因此 C++ 示例如下:
class Solution
private:
vector<vector<int>> result; // 存放结果集
vector<int> path; // 符合条件的结果
// targetSum:目标和,也就是题目中的n。
// k:题目中要求k个数的集合。
// sum:已经收集的元素的总和,也就是path里元素的总和。
// startIndex:下一层for循环搜索的起始位置。
void backtracking(int targetSum, int k, int sum, int startIndex)
if (path.size() == k)
if (sum == targetSum) result.push_back(path);
return; // 如果path.size() == k 但sum != targetSum 直接返回
for (int i = startIndex; i <= 9; i++)
sum += i; // 处理
path.push_back(i); // 处理
backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
sum -= i; // 回溯
path.pop_back(); // 回溯
public:
vector<vector<int>> combinationSum3(int k, int n)
result.clear(); // 可以不加
path.clear(); // 可以不加
backtracking(n, k, 0, 1);
return result;
;
- C 示例:
int *path;
int pathTop;
int **result;
int resultTop;
void backTrack(int k,int n,int sum ,int startIndex)
if(pathTop==k)
if(n == sum)
int *temp = malloc(sizeof(int)*k);
for(int i =0;i<k;i++)
temp[i] = path[i];
result[resultTop++] = temp;
return ;
for(int j=startIndex;j<=9-(k-pathTop)+1;j++)
sum = sum + j;
path[pathTop++] = j;
backTrack(k,n,sum,j+1);
sum = sum -j;
pathTop--;
int** combinationSum3(int k, int n, int* returnSize, int** returnColumnSizes)
pathTop = resultTop = 0;
path = malloc(sizeof(int) *k);
result = malloc(sizeof(int*) *100);
backTrack(k,n,0,1);
*returnSize = resultTop;
*returnColumnSizes = malloc(sizeof(int*) * resultTop);
for(int i=0 ; i<resultTop ; i++)
(*returnColumnSizes)[i] = k;
return result;
- 然后剪枝操作其实是很容易想到,如下图所示:
- 已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了,直接剪掉。那么剪枝的地方一定是在递归终止的地方剪,剪枝代码如下:
if (sum > targetSum) // 剪枝操作
return;
- 最后的 C++示例如下:
class Solution
private:
vector<vector<int>> result; // 存放结果集
vector<int> path; // 符合条件的结果
void backtracking(int targetSum, int k, int sum, int startIndex)
if (sum > targetSum) // 剪枝操作
return; // 如果path.size() == k 但sum != targetSum 直接返回
if (path.size() == k)
if (sum == targetSum) result.push_back(path);
return;
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) // 剪枝
sum += i; // 处理
path.push_back(i); // 处理
backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
sum -= i; // 回溯
path.pop_back(); // 回溯
public:
vector<vector<int>> combinationSum3(int k, int n)
result.clear(); // 可以不加
path.clear(); // 可以不加
backtracking(n, k, 0, 1);
return result;
;
- Java 示例:
class Solution
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n)
backTracking(n, k, 1, 0);
return result;
private void backTracking(int targetSum, int k, int startIndex, int sum)
// 减枝
if (sum > targetSum)
return;
if (path.size() == k)
if (sum == targetSum) result.add(new ArrayList<>(path));
return;
// 减枝 9 - (k - path.size()) + 1
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++)
path.add(i);
sum += i;
backTracking(targetSum, k, i + 1, sum);
// 回溯
path.removeLast();
// 回溯
sum -= i;
② 二进制(子集)枚举
- 组合中只允许含有 1−9 的正整数,并且每种组合中不存在重复的数字,意味着这个组合中最多包含
9 个数字,我们可以把原问题转化成集合 S=1,2,3,4,5,6,7,8,9,要找出 S 的当中满足如下条件的子集: -
- 大小为 k;
-
- 集合中元素的和为 n。
- 因此可以用子集枚举的方法来解决本题,即原序列中有 9 个数,每个数都有两种状态,「被选择到子集中」和「不被选择到子集中」,所以状态的总数为 29。用一个 9 位二进制数 mask 来记录当前所有位置的状态,从第到高第 i 位为 0 表示 i 不被选择到子集中,为 1 表示 i 被选择到子集中。当按顺序枚举 [0, 29−1] 中的所有整数的时候,就可以不重不漏地把每个状态枚举到,对于一个状态 mask,可以用位运算的方法得到对应的子集序列,然后再判断是否满足上面的两个条件,如果满足,就记录答案。
- 如何通过位运算来得到 mask 各个位置的信息?对于第 i 个位置,可以判断 (1 << i) & mask 是否为 0,如果不为 0 则说明 i 在子集当中。当然,这里要注意的是,一个 9 位二进制数 i 的范围是 [0,8],而可选择的数字是 [1,9],所以需要做一个映射,最简单的办法就是当我们知道 i 位置不为 0 的时候将 i+1 加入子集。
- 当然,子集枚举也可以用递归实现。
- C++ 示例:
class Solution
public:
vector<int> temp;
vector<vector<int>> ans;
bool check(int mask, int k, int n)
temp.clear();
for (int i = 0; i < 9; ++i)
if ((1 << i) & mask)
temp.push_back(i + 1);
return temp.size() == k && accumulate(temp.begin(), temp.end(), 0) == n;
vector<vector<int>> combinationSum3(int k, int n)
for (int mask = 0; mask < (1 << 9); ++mask)
if (check(mask, k, n))
ans.emplace_back(temp);
return ans;
;
- Java 示例:
class Solution
List<Integer> temp = new ArrayList<Integer>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
public List<List<Integer>> combinationSum3(int k, int n)
for (int mask = 0; mask < (1 << 9); ++mask)
if (check(mask, k, n))
ans.add(new ArrayList<Integer>(temp));
return ans;
public boolean check(int mask, int k, int n)
temp.clear();
for (int i = 0; i < 9; ++i)
if (((1 << i) & mask) != 0)
temp.add(i + 1);
if (temp.size() != k)
return false;
int sum = 0;
for (int num : temp)
sum += num;
return sum == n;
- 复杂度分析:
-
- 时间复杂度:O(M×2M),其中 M 为集合的大小,本题中 M 固定为 9,一共有 2M 个状态,每个状态需要 O(M+k)=O(M) 的判断 (k≤M),故时间复杂度为 O(M×2M);
-
- 空间复杂度:O(M),即 temp 的空间代价。
以上是关于数据结构与算法之深入解析“组合总和III”的求解思路与算法示例的主要内容,如果未能解决你的问题,请参考以下文章
数据结构与算法之深入解析“股票的最大利润”的求解思路与算法示例