算法之「组合总和」问题

简介

本篇文章介绍「组合总和」各种变形题的解题思路,争取能够攻克所有组合数问题。

题目

39. 组合总和
40. 组合总和 II
216. 组合总和 III
377. 组合总和 Ⅳ

下面通过例子由浅至深详细讲解。
所有例题均来自leetcode,所示代码均通过所有测试。

解析

39. 组合总和

题目描述

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

  • 所有数字(包括 target)都是正整数。
  • 解集不能包含重复的组合。
    示例 1:

输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]

提示:

  • 1 <= candidates.length <= 30
  • 1 <= candidates[i] <= 200
  • candidate 中的每个元素都是独一无二的。
  • 1 <= target <= 500
分析
  1. 回溯。这类题目很容易想到回溯解法,尝试所有的组合,并将满足条件的加入结果集。
    在回溯过程中注意剪枝,减少不必要的尝试。
代码
class Solution {
    List<List<Integer>> res;
    int target;
    int[] candidates;
    List<Integer> tmp;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        res = new ArrayList<>();
        this.target = target;
        this.candidates = candidates;
        tmp = new ArrayList<>();
        dfs(0, 0);
        return res;
    }
    
    private void dfs(int i, int total) {
        if (total == target) {
            res.add(new ArrayList<>(tmp));
            return;
        }
        if (i >= candidates.length) {
            return;
        }
        
        for (int k = i; k < candidates.length; k++) {
            if (candidates[k] + total <= target) {
                tmp.add(candidates[k]);
                dfs(k, total + candidates[k]);
                tmp.remove(tmp.size() - 1);
            }
        }
    }
}

上面代码中 “if (candidates[k] + total <= target)” 就是为了剪枝,只进行必要的递归,不必要的递归会浪费递归调用栈。

40. 组合总和 II

题目描述

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

说明:

所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]

分析
  1. 回溯。
    和上题区别在于不用重复使用数字,所以递归过程中每次递归需要将索引加1。同时为了避免组合中存在重复的组合,需要先将候选数组排序,然后遍历时发现和上次遍历的数字相同,则不进行递归。
代码
class Solution {
    List<List<Integer>> res;
    List<Integer> tmp;
    int target;
    int[] candidates;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        res = new ArrayList<>();
        tmp = new ArrayList<>();
        this.target = target;
        Arrays.sort(candidates);
        this.candidates = candidates;
        dfs(0, 0);
        return res;
    }

    private void dfs(int i, int total) {
        if (total == target) {
            res.add(new ArrayList<>(tmp));
            return;
        }

        for (int k = i; k < candidates.length; k++) {
        	// 避免重复组合
            if (k > i && candidates[k] == candidates[k-1]) { 
                continue;
            }
            int sum = total + candidates[k];
            if (sum <= target) {
                tmp.add(candidates[k]);
                dfs(k + 1, sum);
                tmp.remove(tmp.size() - 1);
            }
        }
    }
}

216. 组合总和 III

题目描述

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

所有数字都是正整数。
解集不能包含重复的组合。
示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]

分析
  1. 回溯。
    这道题限制了组合中数字的个数,可以加在剪枝条件中。
代码
class Solution {
    List<List<Integer>> res;
    List<Integer> tmp;
    int target;
    int k;
    public List<List<Integer>> combinationSum3(int k, int n) {
        res = new ArrayList<>();
        tmp = new ArrayList<>();
        target = n;
        this.k = k;
        dfs(1, 0, 0);
        return res;
    }

    private void dfs(int i, int total, int count) {
        if (count == k && total == target) {
            res.add(new ArrayList<>(tmp));
            return;
        }
        if (count == k) {
            return;
        }

        for (int j = i; j < 10; j++) {
            int sum = total + j;
            if (sum > target || count + 1 > k || sum == target && count + 1 < k || sum < target && count + 1 == k) {
                continue;
            }
            tmp.add(j);
            dfs(j + 1, sum, count + 1);
            tmp.remove(tmp.size() - 1);
        }
    }
}

377. 组合总和 Ⅳ

题目描述

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

示例 1:

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 1000
  • nums 中的所有元素 互不相同
  • 1 <= target <= 1000
分析
  1. 回溯。延续前面三道题的思想,很容易想到回溯。但是这道题继续使用回溯会超时不通过。
    观察提示中给定的数据长度,比第一题的“candidates.length <= 30”大了一个量级。而回溯的时间复杂度是阶乘级别的,可想而知继续使用回溯肯定超时了。
  2. 动态规划。和上面三道题不同之处是这道题只需要求解组合的数量,而组合具体是什么不需要求解。
    定义dp[x],表示组合之和为x的组合数,那么 d p [ x ] = ∑ n u m : n u m s d p [ x − n u m ] dp[x] = \sum_{num:nums} dp[x - num] dp[x]=num:numsdp[xnum]。这是一道类似的背包问题题目。即需要装价值为target的物品,有多少中不同的装法。
    时间复杂度:两次遍历,O(kn),k为nums长度,n=target。相比回溯解法,时间上优化了太多。详细见下面代码。
    关于动态规划的题目详解,可以参考这篇文章
代码
class Solution {
    public int combinationSum4(int[] nums, int target) {
        // dp[i] 表示和为i的种类数
        int n = nums.length;
        int[] dp = new int[target + 1];
        dp[0] = 1;
        
        for (int i = 1; i <= target; i++) {
            for (int num: nums) {
                if (i >= num) {
                    dp[i] += dp[i - num];
                }
            }
        }
        return dp[target];
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值