leetcode题解之39. 组合总和

Posted 刷题之路1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了leetcode题解之39. 组合总和相关的知识,希望对你有一定的参考价值。

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

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

说明:

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

示例 1:

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

示例 2:

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

思路:根据示例 1:输入: candidates = [2,3,6,7]target = 7

  • 候选数组里有 2 ,如果找到了 7 - 2 = 5 的所有组合,再在之前加上 2 ,就是 7 的所有组合;
  • 同理考虑 3,如果找到了 7 - 3 = 4 的所有组合,再在之前加上 3 ,就是 7 的所有组合,依次这样找下去;
  • 上面的思路就可以画成下面的树形图。

其实这里思路已经介绍完了,大家可以自己尝试在纸上画一下这棵树。然后编码实现,如果遇到问题,再看下面的文字。

技术图片

说明:

  • 蓝色结点表示:尝试找到组合之和为该数的所有组合,怎么找呢?逐个减掉候选数组中的元素即可;
  • target = 7 为根结点,每一个分支做减法;
  • 减到 00 或者负数的时候,到了叶子结点;
  • 减到 00 的时候结算,这里 “结算” 的意思是添加到结果集;
  • 从根结点到叶子结点(必须为 0)的路径,就是题目要我们找的一个组合。

把文字的部分去掉。

技术图片

如果这样编码的话,会发现提交不能通过,这是因为递归树画的有问题,下面看一下是什么原因。

技术图片

画出图以后,我看了一下,我这张图画出的结果有 4400,对应的路径是 [[2, 2, 3], [2, 3, 2], [3, 2, 2], [7]],而示例中的解集只有 [[7], [2, 2, 3]],很显然,重复的原因是在较深层的结点值考虑了之前考虑过的元素,因此我们需要设置“下一轮搜索的起点”即可(这里可能没有说清楚,已经尽力了)

去重复

  • 在搜索的时候,需要设置搜索起点的下标 begin ,由于一个数可以使用多次,下一层的结点从这个搜索起点开始搜索;
  • 在搜索起点 begin 之前的数因为以前的分支搜索过了,所以一定会产生重复。

剪枝提速

  • 如果一个数位搜索起点都不能搜索到结果,那么比它还大的数肯定搜索不到结果,基于这个想法,我们可以对输入数组进行排序,以减少搜索的分支;

  • 排序是为了提高搜索速度,非必要;

  • 搜索问题一般复杂度较高,能剪枝就尽量需要剪枝。把候选数组排个序,遇到一个较大的数,如果以这个数为起点都搜索不到结果,后面的数就更搜索不到结果了。

参考代码:

这里感谢 @rmokerone 提供的 C++ 代码实现。

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;

public class Solution {

<span class="hljs-keyword">public</span> List&lt;List&lt;Integer&gt;&gt; combinationSum(<span class="hljs-keyword">int</span>[] candidates, <span class="hljs-keyword">int</span> target) {
    List&lt;List&lt;Integer&gt;&gt; res = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;();
    <span class="hljs-keyword">int</span> len = candidates.length;

    <span class="hljs-comment">// 排序是为了提前终止搜索</span>
    Arrays.sort(candidates);

    dfs(candidates, len, target, <span class="hljs-number">0</span>, <span class="hljs-keyword">new</span> ArrayDeque&lt;&gt;(), res);
    <span class="hljs-keyword">return</span> res;
}

<span class="hljs-comment">/**
 * <span class="hljs-doctag">@param</span> candidates 数组输入
 * <span class="hljs-doctag">@param</span> len        输入数组的长度,冗余变量
 * <span class="hljs-doctag">@param</span> residue    剩余数值
 * <span class="hljs-doctag">@param</span> begin      本轮搜索的起点下标
 * <span class="hljs-doctag">@param</span> path       从根结点到任意结点的路径
 * <span class="hljs-doctag">@param</span> res        结果集变量
 */</span>
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">dfs</span><span class="hljs-params">(<span class="hljs-keyword">int</span>[] candidates,
                 <span class="hljs-keyword">int</span> len,
                 <span class="hljs-keyword">int</span> residue,
                 <span class="hljs-keyword">int</span> begin,
                 Deque&lt;Integer&gt; path,
                 List&lt;List&lt;Integer&gt;&gt; res)</span> </span>{
    <span class="hljs-keyword">if</span> (residue == <span class="hljs-number">0</span>) {
        <span class="hljs-comment">// 由于 path 全局只使用一份,到叶子结点的时候需要做一个拷贝</span>
        res.add(<span class="hljs-keyword">new</span> ArrayList&lt;&gt;(path));
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = begin; i &lt; len; i++) {

        <span class="hljs-comment">// 在数组有序的前提下,剪枝</span>
        <span class="hljs-keyword">if</span> (residue - candidates[i] &lt; <span class="hljs-number">0</span>) {
            <span class="hljs-keyword">break</span>;
        }

        path.addLast(candidates[i]);
        dfs(candidates, len, residue - candidates[i], i, path, res);
        path.removeLast();

    }
}

}


from typing import List

class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
size = len(candidates)
if size == 0:
return []

    <span class="hljs-comment"># 剪枝是为了提速,在本题非必需</span>
    candidates.sort()
    <span class="hljs-comment"># 在遍历的过程中记录路径,它是一个栈</span>
    path = []
    res = []
    <span class="hljs-comment"># 注意要传入 size ,在 range 中, size 取不到</span>
    self.__dfs(candidates, <span class="hljs-number">0</span>, size, path, res, target)
    <span class="hljs-keyword">return</span> res

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__dfs</span><span class="hljs-params">(self, candidates, begin, size, path, res, target)</span>:</span>
    <span class="hljs-comment"># 先写递归终止的情况</span>
    <span class="hljs-keyword">if</span> target == <span class="hljs-number">0</span>:
        <span class="hljs-comment"># Python 中可变对象是引用传递,因此需要将当前 path 里的值拷贝出来</span>
        <span class="hljs-comment"># 或者使用 path.copy()</span>
        res.append(path[:])
        <span class="hljs-keyword">return</span>

    <span class="hljs-keyword">for</span> index <span class="hljs-keyword">in</span> range(begin, size):
        residue = target - candidates[index]
        <span class="hljs-comment"># “剪枝”操作,不必递归到下一层,并且后面的分支也不必执行</span>
        <span class="hljs-keyword">if</span> residue &lt; <span class="hljs-number">0</span>:
            <span class="hljs-keyword">break</span>
        path.append(candidates[index])
        <span class="hljs-comment"># 因为下一层不能比上一层还小,起始索引还从 index 开始</span>
        self.__dfs(candidates, index, size, path, res, residue)
        path.pop()

if name == main:
candidates = [2, 3, 6, 7]
target = 7
solution = Solution()
result = solution.combinationSum(candidates, target)
print(result)


// author:rmokerone
#include <iostream>
#include <vector>

using namespace std;

class Solution {
private:
vector<int> candidates;
vector<vector<int>> res;
vector<int> path;
public:
void DFS(int start, int target) {
if (target == 0) {
res.push_back(path);
return;
}
for (int i = start;
i < candidates.size() && target - candidates[i] >= 0; i++) {
path.push_back(candidates[i]);
DFS(i, target - candidates[i]);
path.pop_back();
}
}

<span class="hljs-function"><span class="hljs-built_in">vector</span>&lt;<span class="hljs-built_in">vector</span>&lt;<span class="hljs-keyword">int</span>&gt;&gt; <span class="hljs-title">combinationSum</span><span class="hljs-params">(<span class="hljs-built_in">vector</span>&lt;<span class="hljs-keyword">int</span>&gt; &amp;candidates, <span class="hljs-keyword">int</span> target)</span> </span>{
    <span class="hljs-built_in">std</span>::sort(candidates.begin(), candidates.end());
    <span class="hljs-keyword">this</span>-&gt;candidates = candidates;
    DFS(<span class="hljs-number">0</span>, target);

    <span class="hljs-keyword">return</span> res;
}

};

附注:这道题我用的是减法,有兴趣的朋友还可以使用加法,加到 target 的时候结算,超过 target 的时候剪枝。

做完这题的朋友,不妨做一下 LeetCode 第 40 题:组合问题 II。

https://www.jianshu.com/p/e721418f89ee


































以上是关于leetcode题解之39. 组合总和的主要内容,如果未能解决你的问题,请参考以下文章

leetcode 39. 组合总和---回溯篇2

LeetCode[39]: 组合总和

算法 ---- LeetCode回溯系列问题题解

算法 ---- LeetCode回溯系列问题题解

算法 ---- LeetCode回溯系列问题题解

算法 ---- LeetCode回溯系列问题题解