LC77 回溯算法解决不定数量的全组合问题|刷题打卡

325 阅读1分钟

本系列使用IDEA+LEETCODE EDITOR插件,题目描述统一英文题目链接
一、题目描述:

//Given two integers n and k, return all possible combinations of k numbers out 
//of 1 ... n. 
//
// You may return the answer in any order. 
//
// 
// Example 1: 
//
// 
//Input: n = 4, k = 2
//Output:
//[
//  [2,4],
//  [3,4],
//  [2,3],
//  [1,2],
//  [1,3],
//  [1,4],
//]
// 
//
// Example 2: 
//
// 
//Input: n = 1, k = 1
//Output: [[1]]
// 
//
// 
// Constraints: 
//
// 
// 1 <= n <= 20 
// 1 <= k <= n 

二、思路分析:
全排列是比较常见的可以用回溯解决的问题。回溯是算法中模板比较固定的一种解法,通常使用递归。 流程就是选择一个加入临时结果集,传入并调用下次递归,然后从临时结果集中移除刚才添加的项。 此题无非多了不固定返回条数这一点而已。并没有什么难度,勉强算得上是M。 用递归解决问题的性能要点,如何剪枝,这个才是这期的重点内容

三、AC 代码:

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        // 0. 题目描述中已经对n,k做了限制,此处不再进行异常数据判断,只初始化结果
        List<List<Integer>> result = new ArrayList<>();
        // 1. 求全部组合,一般都可以用回溯算法解决,根据题意可用题目给的n代替数组
        // 2. 调用回溯算法
        // 存储中间结果使用
        List<Integer> tmpList = new ArrayList<>();
        backtrace(n,k,1,result,tmpList);
        return result;
    }
    void backtrace(int n,int k,int currentIndex,List<List<Integer>> result,List<Integer> tmpList){
        // 添加到返回结果的条件
        if(tmpList.size() == k){
            // 数组数字有序唯一,此处不需要判重
            result.add(new ArrayList<>(tmpList));
            return;
        }
        // 进行递归
        for (int i = currentIndex; i <= n; i++) {
            tmpList.add(i);
            // 此处容易写错,因为i是变化的,要传入i的值而不是currentIndex
            backtrace(n,k,i + 1,result,tmpList);
            tmpList.remove(tmpList.size()-1);
        }
    }
}

此时并未进行剪枝,执行结果:

		解答成功:
			执行耗时:22 ms,击败了39.47% 的Java用户
			内存消耗:39.9 MB,击败了37.60% 的Java用户

并不令人满意,现在思考如何剪枝。 思考一下场景:
当n=4,k=3,currentIndex=4,tmpList=[3]的时候,是否还需要进行递归?

并不需要,那么你判断的依据是什么? 要返回k=3个数,但是之前只添加了一个数,此时index已经指向了最后一个数,1+1=2,2<3,所以哪怕tmpList.size()<k,也不需要继续执行浪费资源了,因为最后得到结果肯定不是我们要的。 所以n-currentIndex + 1的值也就是剩余可循环的数量,要大于等于k-tmpList.size()还需加入的数据的数量。在循环内currentIndex变为i,所以得出类似初中数学的方程式:
n-i + 1>=k-tmpList.size()
在for循环中需要改写为i<=xxxx的形式,变换为如下:
i<= n- (k-tmpList.size()) + 1
归纳为代码则内容如下

        for (int i = currentIndex; i <= n-(k - tmpList.size()) + 1; i++) {
            tmpList.add(i);
            // 此处容易写错,因为i是移动的,要传入i的值而不是currentIndex
            backtrace(n,k,i + 1,result,tmpList);
            tmpList.remove(tmpList.size()-1);
        }
		解答成功:
			执行耗时:2 ms,击败了93.47% 的Java用户
			内存消耗:39.7 MB,击败了74.64% 的Java用户

四、总结:

  1. 记住回溯模板,非常简单。记住口诀:剩余循环中-加-递归-删。
  2. 使用递归时切记要进行剪枝提高性能。
  3. 掌握基本的方程式变换用于算出生成循环的判断条件。

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情