组合类算法问题

  最近一位同学做完求职笔试的时候,问我知不知道怎么划分子数组,我一懵,问输出结果是如何的,同学说,假如原始数字是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);

 

  之后可能再更新一些回溯方面的题目。

 

posted on 2020-07-25 16:21  heySarah  阅读(219)  评论(0编辑  收藏  举报

导航