使用回溯法求解“组合总和”问题【原理+代码+详解】

191 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

使用回溯法求解“组合总和”问题

一、问题简述

找出所有符合下列要求的组合,并返回它的总数:

要求:-从 1 -9 中选择 k 个数总和为 n

(每一种组合当前不允许出现重复的数字,组合内元素可以是任意顺序)

二、算法分析

相信有不少小伙伴看到这里会说;“这有什么难的,我几层for循环就解决了。”

确实,这种暴力解法也是可以通过的,但是 你可以写多少层for循环呢?三层、五层还是十层呢? 要注意这里的 k 是不确定大小的,如果 k 的值取的比较大的话,你就需要写很多层for循环,这显然不是一个很合适的方法。

所以,为了解决要写多层for循环的问题,我们现在考虑使用递归回溯的方法来解题。

其实递归回溯法的本质也是使用了for循环,只不过在回溯里面,你就不需要写多层for循环了,它用递归帮你完成。

其实这也像是对for循环的一种“for循环”,想想一个场景:

如果我们需要给一个储存10个元素的数组赋初值,你会怎么做呢?该不会是一个个写赋值操作吧?肯定是写for循环遍历赋值的; 然后再想想我们这里的问题:需要写多层循环;那么我们有没有一种办法可以像数组赋值那样优化呢?

其实是可以的,我们可以通过递归回溯法,具体实现看看下面的解释就懂了

三、算法实现

先定义两个全局变量:

      List<List<Integer>> result = new ArrayList<>();
      List<Integer> path = new ArrayList<>();

主函数:

    public List<List<Integer>> combinationSum3(int k, int n) {
        backtracking(n, k, 0, 1);
        return result;
    }

对下面回溯递归的感悟:

与其说这里像是用循环实现时的最内层循环,还不如说这里是循环实现中的每一层循环 其实在这里无法是递归还是循环,本质上都是一样的,需要选多少个数,就需要多少次遍历 循环中每一层遍历就体现为一层循环, 而递归里只有一层循环,但是它通递归,不断地重复利用同一个循环来进行不同的循环,从而实现了和循环遍历一样的效果 从这里看来: 递归回溯本质上就是如何通过一层循环来实现多层循环的效果。

回溯递归:

   private void backtracking (int targetSum, int k, int sum, int startIndex) {
        if (path.size() == k) {
            if (sum == targetSum) {
                result.add(new ArrayList<>(path));
            }
            return;
        }

        for (int i = startIndex; i <= 9; i++) {
            sum += i;
            path.add(i);
            backtracking(targetSum, k, sum, i + 1);
            sum -= i;
            path.remove(path.size() - 1);
            // path.removeLast();
        }
    }

四、算法总结

看了上面代码的实现,下面我们一起来总结一下:

首先这道题有一个重要的点需要提前明白的: 为什么这里需要用到递归,或者说什么时候应该用递归??

这道题的意思可以转为:从 1 - 9的数字中选出 k 个使得他们之和为 n 的,像这样的组合有哪些?(有多少种)

这是问你具体的组合的,所以我们需要一个个遍历 首先,选了一个 1 则只能从 2 - 9 中选择 k - 1个了

如果选了 1 和 2 那么就只能从 3 - 9 中选择 k - 2 个了 其他情况,以此类推

其实这种情况是可以通过循环来实现的:

第一层循环选定第一个数,第二层循环选定第 二 个数,要选多少个就需要多少层循环; 然而如果需要选的个数很多,那么就需要写很多很多层循环,为了避免或者说是改善这种情况,所以引入了回溯递归 在回溯递归中,每一层递归就相当于一层循环 因为每一层递归都会在参数中指定一个选数,这就和循环中固定选数是等效的