组合类算法问题
最近一位同学做完求职笔试的时候,问我知不知道怎么划分子数组,我一懵,问输出结果是如何的,同学说,假如原始数字是3,那么输出结果就是
[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]
我想起之前做过类似的题目,但具体的思路没有想起来,于是上力扣上面搜了一下划分子数组,没有搜索到我曾做过的名为划分子数组的题目,但后来一想,这一题的输出结果其实和全排列是很像的,于是我找到全排列的题目,回忆起了解题方法是回溯,其实我之前笔试时也遇到过全排列的原题,当时以为稳了,但实际上在敲代码过程中被卡住了,最大问题就是我对回溯类的问题解题过程不清晰,接下来就从全排列开始,对组合类的算法问题做一个总结。
全排列问题我提交的代码为:
1 /** 2 * @param {number[]} nums 3 * @return {number[][]} 4 */ 5 6 var permute = function(nums) { 7 let res = []; 8 let swap = function(nums, i, j) { 9 let temp = nums[i]; 10 nums[i] = nums[j]; 11 nums[j] = temp; 12 } 13 let find = function(nums, begin, end, res) { 14 // 结束条件,当开始下标和结束下标相同,说明已经达到其要求的数组元素个数,将数组结果添加至结果集 15 if(begin == end) { 16 res.push([...nums]); //注意这里要先把参数类数组转换为数组 17 return ; 18 } 19 for(let start = begin; start <= end; start++) { 20 // 由于在全排列里面,数组下标和元素是严格对立的,所以通过交换数字来达到 对于两个数组,同一下标上有不同元素就当成是全排列的两个结果 21 swap(nums, start, begin); 22 // 开始回溯寻找路径 23 find(nums, begin + 1, end, res); 24 // 回溯后再替换为原先的数组元素 25 swap(nums, start, begin); 26 } 27 } 28 find(nums, 0, nums.length - 1, res); 29 return res; 30 };
一般解决回溯问题的时候,都会画决策树,画完后寻找大致的代码思路,这时需要思考个问题:
1.路径:怎么样走才算找到题意里的东西
2.选择列表:要做抉择的变量在什么可选择范围内
3.结束条件:到达决策树底层,需要return出去的if语句
关于结束条件,比较容易找,一般都是对长度做限制或者起始终止做限制,那么选择列表和路径要怎么理解呢,怎么把这个思想应用到代码中去呢?这些问题都隐藏在回溯算法的核心框架里了:
for 遍历选择操作时所需的变量 in 选择列表: //先尝试做选择 把已经做了的选择从选择列表中移除 在路径里面加入刚才选择的元素 backTrack(更新的路径,更新的选择列表) //回溯 //撤销刚才的选择,便于寻找另外的解 在刚才的路径里撤销所做选择 把该选择再次加入选择列表
组合问题也是用回溯去做,就比如,我现在给出了1-4这4个数字,要求你用高中的组合知识得出它的从1个数到4个数的所有组合结果,你给出的答案必然是(说不定顺序都和我一样)
1 2 3 4 1,2 1,3 1,4 2,3 2,4 3,4 1,2,3 1,2,4 1,3,4 2,3,4 1,2,3,4
力扣上的组合题,规定的是特定的组合数组长度,所以是上面所列举的组合数学问题的一个子集,我提交的代码为:
1 /** 2 * @param {number} n 3 * @param {number} k 4 * @return {number[][]} 5 */ 6 var combine = function(n, k) { 7 let arr = [], 8 res = []; 9 let findCombination = function(n, k, begin, arr) { 10 if(arr.length == k) { 11 res.push([...arr]); 12 return ; 13 } 14 for(let i = begin; i <= n - (k - arr.length) + 1; i++) { 15 arr.push(i); 16 findCombination(n, k, i + 1, arr); 17 arr.pop(); 18 } 19 } 20 findCombination(n, k, 1, arr); 21 return res; 22 };
没给注释,因为总体上就是套着回溯的代码框架来的。
你能看出组合题跟全排列的代码差别么?思考一下
----------------------------------------------------------------
答案揭晓!
其实有两点不同,在于二者回溯的终止条件不同和二者在不在意数组元素所在位置(全排列的话,[1,2,3]和[3,2,1]算作不同的结果,而组合的话,[1,2,3]和[3,2,1]由于含有的元素个数和值相同,所以看作同一个解,只取其一就可)。假如是全排列的话,最终的结果肯定跟原数组的长度是一致的,举个例子:假如让你说出[1,2,3]的全排列结果,你肯定会说,[1,3,2],[2,1,3]......对吧!所以排列后的数组长度跟原数组一样,都是3。那假如现在说让你说出[1,2,3]任意两个数的组合结果,那么你回溯到数组长度=2时,就要return了。
有了这一题的基础,就能够做出我同学的那道笔试了,聪明的你一定想到了,就是把 k 放到循环里面去遍历就可以了,所以最终的代码应该为:
1 function subArray(n) { 2 let arr = [], 3 res = []; 4 let findCombination = function(n, k, begin, arr) { 5 if(arr.length == k) { 6 res.push([...arr]); 7 return ; 8 } 9 for(let i = begin; i <= n - (k - arr.length) + 1; i++) { 10 arr.push(i); 11 findCombination(n, k, i + 1, arr); 12 arr.pop(); 13 } 14 } 15 for(let i = 1; i <= n; i++){ 16 findCombination(n, i, 1, arr); 17 } 18 console.log(res) 19 } 20 subArray(4);
之后可能再更新一些回溯方面的题目。