【数据结构与算法】之深入解析“组合总和III”的求解思路与算法示例
2022/1/10 20:07:46
本文主要是介绍【数据结构与算法】之深入解析“组合总和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”的求解思路与算法示例的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-27消息中间件底层原理资料详解
- 2024-11-27RocketMQ底层原理资料详解:新手入门教程
- 2024-11-27MQ底层原理资料详解:新手入门教程
- 2024-11-27MQ项目开发资料入门教程
- 2024-11-27RocketMQ源码资料详解:新手入门教程
- 2024-11-27本地多文件上传简易教程
- 2024-11-26消息中间件源码剖析教程
- 2024-11-26JAVA语音识别项目资料的收集与应用
- 2024-11-26Java语音识别项目资料:入门级教程与实战指南
- 2024-11-26SpringAI:Java 开发的智能新利器