This commit is contained in:
krahets
2023-07-11 19:21:38 +08:00
parent 4c792892a1
commit 2c96e433e2
117 changed files with 4713 additions and 132 deletions

View File

@ -1870,14 +1870,14 @@
<li class="md-nav__item">
<a href="#1215" class="md-nav__link">
12.1.5. &nbsp; 典型例题
12.1.5. &nbsp; 优势与局限性
</a>
</li>
<li class="md-nav__item">
<a href="#1216" class="md-nav__link">
12.1.6. &nbsp; 优势与局限性
12.1.6. &nbsp; 典型例题
</a>
</li>
@ -1982,6 +1982,8 @@
@ -2060,6 +2062,20 @@
<li class="md-nav__item">
<a href="../../chapter_dynamic_programming/unbounded_knapsack_problem/" class="md-nav__link">
13.5. &nbsp; 完全背包问题New
</a>
</li>
</ul>
</nav>
</li>
@ -2236,14 +2252,14 @@
<li class="md-nav__item">
<a href="#1215" class="md-nav__link">
12.1.5. &nbsp; 典型例题
12.1.5. &nbsp; 优势与局限性
</a>
</li>
<li class="md-nav__item">
<a href="#1216" class="md-nav__link">
12.1.6. &nbsp; 优势与局限性
12.1.6. &nbsp; 典型例题
</a>
</li>
@ -2273,7 +2289,7 @@
<h1 id="121">12.1. &nbsp; 回溯算法<a class="headerlink" href="#121" title="Permanent link">&para;</a></h1>
<p>「回溯算法 Backtracking Algorithm」是一种通过穷举来解决问题的方法它的核心思想是从一个初始状态出发暴力搜索所有可能的解决方案当遇到正确的解则将其记录直到找到解或者尝试了所有可能的选择都无法找到解为止。</p>
<p>回溯算法通常采用「深度优先搜索」来遍历解空间。在二叉树章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。下面,我们将从前序遍历入手,逐步了解回溯算法的工作原理。</p>
<p>回溯算法通常采用「深度优先搜索」来遍历解空间。在二叉树章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来我们先用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。</p>
<div class="admonition question">
<p class="admonition-title">例题一</p>
<p>给定一个二叉树,搜索并记录所有值为 <span class="arithmatex">\(7\)</span> 的节点,返回节点列表。</p>
@ -3542,7 +3558,19 @@
</div>
</div>
<p>相较于基于前序遍历的实现代码,基于回溯算法框架的实现代码虽然显得啰嗦,但通用性更好。实际上,<strong>所有回溯问题都可以在该框架下解决</strong>。我们需要根据具体问题来定义 <code>state</code><code>choices</code> ,并实现框架中的各个方法。</p>
<h2 id="1215">12.1.5. &nbsp; 典型例题<a class="headerlink" href="#1215" title="Permanent link">&para;</a></h2>
<h2 id="1215">12.1.5. &nbsp; 优势与局限性<a class="headerlink" href="#1215" title="Permanent link">&para;</a></h2>
<p>回溯算法本质上是一种深度优先搜索算法,它尝试所有可能的解决方案直到找到满足条件的解。这种方法的优势在于它能够找到所有可能的解决方案,而且在合理的剪枝操作下,具有很高的效率。</p>
<p>然而,在处理大规模或者复杂问题时,<strong>回溯算法的运行效率可能难以接受</strong></p>
<ul>
<li>在最坏的情况下,回溯算法需要遍历解空间的所有可能解,所需时间很长。例如,求解 <span class="arithmatex">\(n\)</span> 皇后问题的时间复杂度可以达到 <span class="arithmatex">\(O(n!)\)</span></li>
<li>在每一次递归调用时,都需要保存当前的状态(例如选择路径、用于剪枝的辅助变量等),对于深度很大的递归,空间需求可能会变得非常大。</li>
</ul>
<p>即便如此,<strong>回溯算法仍然是某些搜索问题和约束满足问题的最佳解决方案</strong>。对于这些问题,由于无法预测哪些选择可生成有效的解,因此我们必须对所有可能的选择进行遍历。在这种情况下,<strong>关键是如何进行效率优化</strong></p>
<ul>
<li>上文介绍过的剪枝是一种常用的优化方法。它可以避免搜索那些肯定不会产生有效解的路径,从而节省时间和空间。</li>
<li>另一个常用的优化方法是加入「启发式搜索 Heuristic Search」策略它在搜索过程中引入一些策略或者估计值从而优先搜索最有可能产生有效解的路径。</li>
</ul>
<h2 id="1216">12.1.6. &nbsp; 典型例题<a class="headerlink" href="#1216" title="Permanent link">&para;</a></h2>
<p><strong>搜索问题</strong>:这类问题的目标是找到满足特定条件的解决方案。</p>
<ul>
<li>全排列问题:给定一个集合,求出其所有可能的排列组合。</li>
@ -3562,14 +3590,6 @@
<li>最大团问题:给定一个无向图,找到最大的完全子图,即子图中的任意两个顶点之间都有边相连。</li>
</ul>
<p>请注意回溯算法通常不是解决组合优化问题的最优方法。0-1 背包问题通常使用动态规划解决;旅行商是一个 NP-Hard 问题,常用解决方法有遗传算法和蚁群算法等;最大团问题是图轮中的一个经典 NP-Hard 问题,通常用贪心算法等启发式算法来解决。</p>
<h2 id="1216">12.1.6. &nbsp; 优势与局限性<a class="headerlink" href="#1216" title="Permanent link">&para;</a></h2>
<p>回溯算法本质上是一种深度优先搜索算法,它尝试所有可能的解决方案直到找到满足条件的解。这种方法的优势在于它能够找到所有可能的解决方案,而且在合理的剪枝操作下,具有很高的效率。</p>
<p>然而,在处理大规模或者复杂问题时,<strong>回溯算法的运行效率可能难以接受</strong>。这是因为在最坏的情况下,回溯算法需要遍历解空间的所有可能解。例如,求解 <span class="arithmatex">\(n\)</span> 皇后问题的时间复杂度可以达到 <span class="arithmatex">\(O(n!)\)</span> 。回溯算法的空间复杂度也可能较高。因为在每一次递归调用时,都需要保存当前的状态(例如选择路径、用于剪枝的辅助变量等),对于深度很大的递归,空间需求可能会变得非常大。</p>
<p>即便如此,<strong>回溯算法仍然是某些搜索问题和约束满足问题的最佳解决方案</strong>。对于这些问题,由于我们无法预测哪些选择可生成有效的解,因此我们必须对所有可能的选择进行遍历。在这种情况下,<strong>关键是如何进行效率优化</strong></p>
<ul>
<li>上文介绍过的剪枝是一种常用的优化方法。它可以避免搜索那些肯定不会产生有效解的路径,从而节省时间和空间。</li>
<li>另一个常用的优化方法是加入「启发式搜索 Heuristic Search」策略它在搜索过程中引入一些策略或者估计值从而优先搜索最有可能产生有效解的路径。</li>
</ul>

View File

@ -1906,6 +1906,8 @@
@ -1984,6 +1986,20 @@
<li class="md-nav__item">
<a href="../chapter_dynamic_programming/unbounded_knapsack_problem/" class="md-nav__link">
13.5. &nbsp; 完全背包问题New
</a>
</li>
</ul>
</nav>
</li>

View File

@ -1947,6 +1947,8 @@
@ -2025,6 +2027,20 @@
<li class="md-nav__item">
<a href="../../chapter_dynamic_programming/unbounded_knapsack_problem/" class="md-nav__link">
13.5. &nbsp; 完全背包问题New
</a>
</li>
</ul>
</nav>
</li>

View File

@ -1961,6 +1961,8 @@
@ -2039,6 +2041,20 @@
<li class="md-nav__item">
<a href="../../chapter_dynamic_programming/unbounded_knapsack_problem/" class="md-nav__link">
13.5. &nbsp; 完全背包问题New
</a>
</li>
</ul>
</nav>
</li>
@ -2231,7 +2247,7 @@
<h1 id="122">12.2. &nbsp; 全排列问题<a class="headerlink" href="#122" title="Permanent link">&para;</a></h1>
<p>全排列问题是回溯算法的一个典型应用。它的定义是在给定一个集合(如一个数组或字符串)的情况下,找出这个集合中元素的所有可能的排列。</p>
<p>下表所示,列举了几个示例数组和对应的所有排列。</p>
<p>下表列举了几个示例数据,包括输入数组和对应的所有排列。</p>
<div class="center-table">
<table>
<thead>
@ -2559,7 +2575,7 @@
</div>
</div>
<p>需要重点关注的是,我们引入了一个布尔型数组 <code>selected</code> ,它的长度与输入数组长度相等,其中 <code>selected[i]</code> 表示 <code>choices[i]</code> 是否已被选择。我们利用 <code>selected</code> 避免某个元素被重复选择,从而实现剪枝。</p>
<p>如下图所示,假设我们第一轮选择 1 ,第二轮选择 3 ,第三轮选择 2 ,则需要在第二轮剪掉元素 1 的分支,在第三轮剪掉元素 1, 3 的分支。<strong>从本质上理解,此剪枝操作可将搜索空间大小从 <span class="arithmatex">\(O(n^n)\)</span> 降低至 <span class="arithmatex">\(O(n!)\)</span></strong></p>
<p>如下图所示,假设我们第一轮选择 1 ,第二轮选择 3 ,第三轮选择 2 ,则需要在第二轮剪掉元素 1 的分支,在第三轮剪掉元素 1, 3 的分支。<strong>此剪枝操作可将搜索空间大小从 <span class="arithmatex">\(O(n^n)\)</span> 降低至 <span class="arithmatex">\(O(n!)\)</span></strong></p>
<p><img alt="全排列剪枝示例" src="../permutations_problem.assets/permutations_i_pruning.png" /></p>
<p align="center"> Fig. 全排列剪枝示例 </p>
@ -2572,7 +2588,7 @@
<p><img alt="重复排列" src="../permutations_problem.assets/permutations_ii.png" /></p>
<p align="center"> Fig. 重复排列 </p>
<p>那么,如何去除重复的排列呢?最直接地,我们可以借助一个哈希表,直接对排列结果进行去重。然而这样做不够优雅,因为生成重复排列的搜索分支是没有必要的,应当被提前识别并剪枝,这样可以提升算法效率。</p>
<p>那么,如何去除重复的排列呢?最直接地,我们可以借助一个哈希表,直接对排列结果进行去重。然而这样做不够优雅,<strong>因为生成重复排列的搜索分支是没有必要的,应当被提前识别并剪枝</strong>,这样可以进一步提升算法效率。</p>
<p>观察发现,在第一轮中,选择 <span class="arithmatex">\(1\)</span> 或选择 <span class="arithmatex">\(\hat{1}\)</span> 是等价的,因为在这两个选择之下生成的所有排列都是重复的。因此,我们应该把 <span class="arithmatex">\(\hat{1}\)</span> 剪枝掉。同理,在第一轮选择 <span class="arithmatex">\(2\)</span> 后,第二轮选择中的 <span class="arithmatex">\(1\)</span><span class="arithmatex">\(\hat{1}\)</span> 也会产生重复分支,因此也需要将第二轮的 <span class="arithmatex">\(\hat{1}\)</span> 剪枝。</p>
<p><img alt="重复排列剪枝" src="../permutations_problem.assets/permutations_ii_pruning.png" /></p>
<p align="center"> Fig. 重复排列剪枝 </p>

View File

@ -1961,6 +1961,8 @@
@ -2039,6 +2041,20 @@
<li class="md-nav__item">
<a href="../../chapter_dynamic_programming/unbounded_knapsack_problem/" class="md-nav__link">
13.5. &nbsp; 完全背包问题New
</a>
</li>
</ul>
</nav>
</li>

View File

@ -1916,6 +1916,8 @@
@ -1994,6 +1996,20 @@
<li class="md-nav__item">
<a href="../../chapter_dynamic_programming/unbounded_knapsack_problem/" class="md-nav__link">
13.5. &nbsp; 完全背包问题New
</a>
</li>
</ul>
</nav>
</li>