mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-12 15:36:05 +00:00
deploy
This commit is contained in:
@@ -2600,7 +2600,7 @@
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_1" class="md-nav__link">
|
||||
从全排列引出解法
|
||||
参考全排列解法
|
||||
</a>
|
||||
|
||||
</li>
|
||||
@@ -3393,7 +3393,7 @@
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_1" class="md-nav__link">
|
||||
从全排列引出解法
|
||||
参考全排列解法
|
||||
</a>
|
||||
|
||||
</li>
|
||||
@@ -3473,10 +3473,14 @@
|
||||
<p class="admonition-title">Question</p>
|
||||
<p>给定一个正整数数组 <code>nums</code> 和一个目标正整数 <code>target</code> ,请找出所有可能的组合,使得组合中的元素和等于 <code>target</code> 。给定数组无重复元素,每个元素可以被选取多次。请以列表形式返回这些组合,列表中不应包含重复组合。</p>
|
||||
</div>
|
||||
<p>例如,输入集合 <span class="arithmatex">\(\{3, 4, 5\}\)</span> 和目标整数 <span class="arithmatex">\(9\)</span> ,由于集合中的数字可以被重复选取,因此解为 <span class="arithmatex">\(\{3, 3, 3\}, \{4, 5\}\)</span> 。请注意,子集是不区分元素顺序的,例如 <span class="arithmatex">\(\{4, 5\}\)</span> 和 <span class="arithmatex">\(\{5, 4\}\)</span> 是同一个子集。</p>
|
||||
<h3 id="_1">从全排列引出解法<a class="headerlink" href="#_1" title="Permanent link">¶</a></h3>
|
||||
<p>类似于上节全排列问题的解法,我们可以把子集的生成过程想象成一系列选择的结果,并在选择过程中实时更新“元素和”,当元素和等于 <code>target</code> 时,就将子集记录至结果列表。</p>
|
||||
<p>而与全排列问题不同的是,本题允许重复选取同一元素,因此无需借助 <code>selected</code> 布尔列表来记录元素是否已被选择。我们可以对全排列代码进行小幅修改,初步得到解题代码。</p>
|
||||
<p>例如,输入集合 <span class="arithmatex">\(\{3, 4, 5\}\)</span> 和目标整数 <span class="arithmatex">\(9\)</span> ,解为 <span class="arithmatex">\(\{3, 3, 3\}, \{4, 5\}\)</span> 。需要注意两点:</p>
|
||||
<ul>
|
||||
<li>输入集合中的元素可以被无限次重复选取。</li>
|
||||
<li>子集是不区分元素顺序的,比如 <span class="arithmatex">\(\{4, 5\}\)</span> 和 <span class="arithmatex">\(\{5, 4\}\)</span> 是同一个子集。</li>
|
||||
</ul>
|
||||
<h3 id="_1">参考全排列解法<a class="headerlink" href="#_1" title="Permanent link">¶</a></h3>
|
||||
<p>类似于全排列问题,我们可以把子集的生成过程想象成一系列选择的结果,并在选择过程中实时更新“元素和”,当元素和等于 <code>target</code> 时,就将子集记录至结果列表。</p>
|
||||
<p>而与全排列问题不同的是,<strong>本题集合中的元素可以被无限次选取</strong>,因此无需借助 <code>selected</code> 布尔列表来记录元素是否已被选择。我们可以对全排列代码进行小幅修改,初步得到解题代码。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="1:11"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Java</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Python</label><label for="__tabbed_1_4">Go</label><label for="__tabbed_1_5">JavaScript</label><label for="__tabbed_1_6">TypeScript</label><label for="__tabbed_1_7">C</label><label for="__tabbed_1_8">C#</label><label for="__tabbed_1_9">Swift</label><label for="__tabbed_1_10">Zig</label><label for="__tabbed_1_11">Dart</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -3711,29 +3715,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>向以上代码输入数组 <span class="arithmatex">\([3, 4, 5]\)</span> 和目标元素 <span class="arithmatex">\(9\)</span> ,输出结果为 <span class="arithmatex">\([3, 3, 3], [4, 5], [5, 4]\)</span> 。<strong>虽然成功找出了所有和为 <span class="arithmatex">\(9\)</span> 的子集,但其中存在重复的子集 <span class="arithmatex">\([4, 5]\)</span> 和 <span class="arithmatex">\([5, 4]\)</span></strong> 。这是因为搜索过程是区分选择顺序的,如下图所示,先选 <span class="arithmatex">\(4\)</span> 后选 <span class="arithmatex">\(5\)</span> 与先选 <span class="arithmatex">\(5\)</span> 后选 <span class="arithmatex">\(4\)</span> 是两种不同的情况。</p>
|
||||
<p>向以上代码输入数组 <span class="arithmatex">\([3, 4, 5]\)</span> 和目标元素 <span class="arithmatex">\(9\)</span> ,输出结果为 <span class="arithmatex">\([3, 3, 3], [4, 5], [5, 4]\)</span> 。<strong>虽然成功找出了所有和为 <span class="arithmatex">\(9\)</span> 的子集,但其中存在重复的子集 <span class="arithmatex">\([4, 5]\)</span> 和 <span class="arithmatex">\([5, 4]\)</span></strong> 。</p>
|
||||
<p>这是因为搜索过程是区分选择顺序的,然而子集不区分选择顺序。如下图所示,先选 <span class="arithmatex">\(4\)</span> 后选 <span class="arithmatex">\(5\)</span> 与先选 <span class="arithmatex">\(5\)</span> 后选 <span class="arithmatex">\(4\)</span> 是两个不同的分支,但两者对应同一个子集。</p>
|
||||
<p><img alt="子集搜索与越界剪枝" src="../subset_sum_problem.assets/subset_sum_i_naive.png" /></p>
|
||||
<p align="center"> Fig. 子集搜索与越界剪枝 </p>
|
||||
|
||||
<h3 id="_2">重复子集剪枝<a class="headerlink" href="#_2" title="Permanent link">¶</a></h3>
|
||||
<p>为了去除重复子集,<strong>一种直接的思路是对结果列表进行去重</strong>。但这个方法效率很低,因为:</p>
|
||||
<ul>
|
||||
<li>当数组元素较多,尤其是当 <code>target</code> 较大时,搜索过程会产生大量的重复子集。</li>
|
||||
<li>比较子集(数组)的异同是很耗时的,需要先排序数组,再比较数组中每个元素的异同。</li>
|
||||
<li>比较子集(数组)的异同非常耗时,需要先排序数组,再比较数组中每个元素的异同。</li>
|
||||
</ul>
|
||||
<p>为了达到最佳效率,<strong>我们希望在搜索过程中通过剪枝进行去重</strong>。观察下图,重复子集是在以不同顺序选择数组元素时产生的,具体来看:</p>
|
||||
<h3 id="_2">重复子集剪枝<a class="headerlink" href="#_2" title="Permanent link">¶</a></h3>
|
||||
<p><strong>我们考虑在搜索过程中通过剪枝进行去重</strong>。观察下图,重复子集是在以不同顺序选择数组元素时产生的,具体来看:</p>
|
||||
<ol>
|
||||
<li>第一轮和第二轮分别选择 <span class="arithmatex">\(3\)</span> , <span class="arithmatex">\(4\)</span> ,会生成包含这两个元素的所有子集,记为 <span class="arithmatex">\([3, 4, \cdots]\)</span> 。</li>
|
||||
<li>若第一轮选择 <span class="arithmatex">\(4\)</span> ,<strong>则第二轮应该跳过 <span class="arithmatex">\(3\)</span></strong> ,因为该选择产生的子集 <span class="arithmatex">\([4, 3, \cdots]\)</span> 和 <code>1.</code> 中提到的子集完全重复。</li>
|
||||
<li>同理,若第一轮选择 <span class="arithmatex">\(5\)</span> ,<strong>则第二轮应该跳过 <span class="arithmatex">\(3\)</span> 和 <span class="arithmatex">\(4\)</span></strong> ,因为子集 <span class="arithmatex">\([5, 3, \cdots]\)</span> 和子集 <span class="arithmatex">\([5, 4, \cdots]\)</span> 和之前的子集重复。</li>
|
||||
<li>若第一轮选择 <span class="arithmatex">\(4\)</span> ,<strong>则第二轮应该跳过 <span class="arithmatex">\(3\)</span></strong> ,因为该选择产生的子集 <span class="arithmatex">\([4, 3, \cdots]\)</span> 和 <code>1.</code> 中生成的子集完全重复。</li>
|
||||
</ol>
|
||||
<p>分支越靠右,需要排除的分支也越多,例如:</p>
|
||||
<ol>
|
||||
<li>前两轮选择 <span class="arithmatex">\(3\)</span> , <span class="arithmatex">\(5\)</span> ,生成子集 <span class="arithmatex">\([3, 5, \cdots]\)</span> ;</li>
|
||||
<li>前两轮选择 <span class="arithmatex">\(4\)</span> , <span class="arithmatex">\(5\)</span> ,生成子集 <span class="arithmatex">\([4, 5, \cdots]\)</span> ;</li>
|
||||
<li>若第一轮选择 <span class="arithmatex">\(5\)</span> ,<strong>则第二轮应该跳过 <span class="arithmatex">\(3\)</span> 和 <span class="arithmatex">\(4\)</span></strong> ,因为子集 <span class="arithmatex">\([5, 3, \cdots]\)</span> 和子集 <span class="arithmatex">\([5, 4, \cdots]\)</span> 和 <code>1.</code> , <code>2.</code> 中生成的子集完全重复。</li>
|
||||
</ol>
|
||||
<p><img alt="不同选择顺序导致的重复子集" src="../subset_sum_problem.assets/subset_sum_i_pruning.png" /></p>
|
||||
<p align="center"> Fig. 不同选择顺序导致的重复子集 </p>
|
||||
|
||||
<p>总结来看,给定输入数组 <span class="arithmatex">\([x_1, x_2, \cdots, x_n]\)</span> ,设搜索过程中的选择序列为 <span class="arithmatex">\([x_{i_1}, x_{i_2}, \cdots , x_{i_m}]\)</span> ,则该选择序列需要满足 <span class="arithmatex">\(i_1 \leq i_2 \leq \cdots \leq i_m\)</span> 。<strong>不满足该条件的选择序列都是重复子集</strong>。</p>
|
||||
<p>总结来看,给定输入数组 <span class="arithmatex">\([x_1, x_2, \cdots, x_n]\)</span> ,设搜索过程中的选择序列为 <span class="arithmatex">\([x_{i_1}, x_{i_2}, \cdots , x_{i_m}]\)</span> ,则该选择序列需要满足 <span class="arithmatex">\(i_1 \leq i_2 \leq \cdots \leq i_m\)</span> ,<strong>不满足该条件的选择序列都会造成重复,应当剪枝</strong>。</p>
|
||||
<h3 id="_3">代码实现<a class="headerlink" href="#_3" title="Permanent link">¶</a></h3>
|
||||
<p>为实现该剪枝,我们初始化变量 <code>start</code> ,用于指示遍历起点。<strong>当做出选择 <span class="arithmatex">\(x_{i}\)</span> 后,设定下一轮从索引 <span class="arithmatex">\(i\)</span> 开始遍历</strong>,从而完成子集去重。</p>
|
||||
<p>除此之外,我们还对代码进行了两项优化。首先,我们在开启搜索前将数组 <code>nums</code> 排序,在搜索过程中,<strong>当子集和超过 <code>target</code> 时直接结束循环</strong>,因为后边的元素更大,其子集和都一定会超过 <code>target</code> 。其次,<strong>我们通过在 <code>target</code> 上执行减法来统计元素和</strong>,当 <code>target</code> 等于 <span class="arithmatex">\(0\)</span> 时记录解,省去了元素和变量 <code>total</code> 。</p>
|
||||
<p>为实现该剪枝,我们初始化变量 <code>start</code> ,用于指示遍历起点。<strong>当做出选择 <span class="arithmatex">\(x_{i}\)</span> 后,设定下一轮从索引 <span class="arithmatex">\(i\)</span> 开始遍历</strong>。这样做就可以让选择序列满足 <span class="arithmatex">\(i_1 \leq i_2 \leq \cdots \leq i_m\)</span> ,从而保证子集唯一。</p>
|
||||
<p>除此之外,我们还对代码进行了两项优化:</p>
|
||||
<ul>
|
||||
<li>在开启搜索前,先将数组 <code>nums</code> 排序。在遍历所有选择时,<strong>当子集和超过 <code>target</code> 时直接结束循环</strong>,因为后边的元素更大,其子集和都一定会超过 <code>target</code> 。</li>
|
||||
<li>省去元素和变量 <code>total</code>,<strong>通过在 <code>target</code> 上执行减法来统计元素和</strong>,当 <code>target</code> 等于 <span class="arithmatex">\(0\)</span> 时记录解。</li>
|
||||
</ul>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="2:11"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Java</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Python</label><label for="__tabbed_2_4">Go</label><label for="__tabbed_2_5">JavaScript</label><label for="__tabbed_2_6">TypeScript</label><label for="__tabbed_2_7">C</label><label for="__tabbed_2_8">C#</label><label for="__tabbed_2_9">Swift</label><label for="__tabbed_2_10">Zig</label><label for="__tabbed_2_11">Dart</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -3991,13 +4005,14 @@
|
||||
<p class="admonition-title">Question</p>
|
||||
<p>给定一个正整数数组 <code>nums</code> 和一个目标正整数 <code>target</code> ,请找出所有可能的组合,使得组合中的元素和等于 <code>target</code> 。<strong>给定数组可能包含重复元素,每个元素只可被选择一次</strong>。请以列表形式返回这些组合,列表中不应包含重复组合。</p>
|
||||
</div>
|
||||
<p>相比于上题,<strong>本题的输入数组可能包含重复元素</strong>,这引入了新的问题。例如,给定数组 <span class="arithmatex">\([4, \hat{4}, 5]\)</span> 和目标元素 <span class="arithmatex">\(9\)</span> ,则现有代码的输出结果为 <span class="arithmatex">\([4, 5], [\hat{4}, 5]\)</span> ,也出现了重复子集。<strong>造成这种重复的原因是相等元素在某轮中被多次选择</strong>。如下图所示,第一轮共有三个选择,其中两个都为 <span class="arithmatex">\(4\)</span> ,会产生两个重复的搜索分支,从而输出重复子集;同理,第二轮的两个 <span class="arithmatex">\(4\)</span> 也会产生重复子集。</p>
|
||||
<p>相比于上题,<strong>本题的输入数组可能包含重复元素</strong>,这引入了新的问题。例如,给定数组 <span class="arithmatex">\([4, \hat{4}, 5]\)</span> 和目标元素 <span class="arithmatex">\(9\)</span> ,则现有代码的输出结果为 <span class="arithmatex">\([4, 5], [\hat{4}, 5]\)</span> ,出现了重复子集。</p>
|
||||
<p><strong>造成这种重复的原因是相等元素在某轮中被多次选择</strong>。如下图所示,第一轮共有三个选择,其中两个都为 <span class="arithmatex">\(4\)</span> ,会产生两个重复的搜索分支,从而输出重复子集;同理,第二轮的两个 <span class="arithmatex">\(4\)</span> 也会产生重复子集。</p>
|
||||
<p><img alt="相等元素导致的重复子集" src="../subset_sum_problem.assets/subset_sum_ii_repeat.png" /></p>
|
||||
<p align="center"> Fig. 相等元素导致的重复子集 </p>
|
||||
|
||||
<h3 id="_4">相等元素剪枝<a class="headerlink" href="#_4" title="Permanent link">¶</a></h3>
|
||||
<p>为解决此问题,<strong>我们需要限制相等元素在每一轮中只被选择一次</strong>。实现方式比较巧妙:由于数组是已排序的,因此相等元素都是相邻的。利用该特性,在某轮选择中,若当前元素与其左边元素相等,则说明它已经被选择过,因此直接跳过当前元素。</p>
|
||||
<p>与此同时,<strong>本题规定数组元素只能被选择一次</strong>。幸运的是,我们也可以利用变量 <code>start</code> 来满足该约束:当做出选择 <span class="arithmatex">\(x_{i}\)</span> 后,设定下一轮从索引 <span class="arithmatex">\(i + 1\)</span> 开始向后遍历。这样即能去除重复子集,也能避免重复选择相等元素。</p>
|
||||
<p>为解决此问题,<strong>我们需要限制相等元素在每一轮中只被选择一次</strong>。实现方式比较巧妙:由于数组是已排序的,因此相等元素都是相邻的。这意味着在某轮选择中,若当前元素与其左边元素相等,则说明它已经被选择过,因此直接跳过当前元素。</p>
|
||||
<p>与此同时,<strong>本题规定中的每个数组元素只能被选择一次</strong>。幸运的是,我们也可以利用变量 <code>start</code> 来满足该约束:当做出选择 <span class="arithmatex">\(x_{i}\)</span> 后,设定下一轮从索引 <span class="arithmatex">\(i + 1\)</span> 开始向后遍历。这样即能去除重复子集,也能避免重复选择元素。</p>
|
||||
<h3 id="_5">代码实现<a class="headerlink" href="#_5" title="Permanent link">¶</a></h3>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="3:11"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><input id="__tabbed_3_11" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Java</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Python</label><label for="__tabbed_3_4">Go</label><label for="__tabbed_3_5">JavaScript</label><label for="__tabbed_3_6">TypeScript</label><label for="__tabbed_3_7">C</label><label for="__tabbed_3_8">C#</label><label for="__tabbed_3_9">Swift</label><label for="__tabbed_3_10">Zig</label><label for="__tabbed_3_11">Dart</label></div>
|
||||
<div class="tabbed-content">
|
||||
@@ -4276,7 +4291,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>下图展示了数组 <span class="arithmatex">\([4, 4, 5]\)</span> 和目标元素 <span class="arithmatex">\(9\)</span> 的回溯过程,共包含四种剪枝操作。建议你将图示与代码注释相结合,理解整个搜索过程,以及每种剪枝操作是如何工作的。</p>
|
||||
<p>下图展示了数组 <span class="arithmatex">\([4, 4, 5]\)</span> 和目标元素 <span class="arithmatex">\(9\)</span> 的回溯过程,共包含四种剪枝操作。请你将图示与代码注释相结合,理解整个搜索过程,以及每种剪枝操作是如何工作的。</p>
|
||||
<p><img alt="子集和 II 回溯过程" src="../subset_sum_problem.assets/subset_sum_ii.png" /></p>
|
||||
<p align="center"> Fig. 子集和 II 回溯过程 </p>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user