This commit is contained in:
krahets
2025-05-17 17:59:47 +08:00
parent 5817118a68
commit ea4ae128df
43 changed files with 766 additions and 782 deletions

View File

@@ -3621,7 +3621,7 @@
<!-- Page content -->
<h1 id="141-introduction-to-dynamic-programming">14.1 &nbsp; Introduction to dynamic programming<a class="headerlink" href="#141-introduction-to-dynamic-programming" title="Permanent link">&para;</a></h1>
<p><u>Dynamic programming</u> is an important algorithmic paradigm that decomposes a problem into a series of smaller subproblems, and stores the solutions of these subproblems to avoid redundant computations, thereby significantly improving time efficiency.</p>
<p>In this section, we start with a classic problem, first presenting its brute force backtracking solution, observing the overlapping subproblems contained within, and then gradually deriving a more efficient dynamic programming solution.</p>
<p>In this section, we start with a classic problem, first presenting its brute force backtracking solution, identifying the overlapping subproblems, and then gradually deriving a more efficient dynamic programming solution.</p>
<div class="admonition question">
<p class="admonition-title">Climbing stairs</p>
<p>Given a staircase with <span class="arithmatex">\(n\)</span> steps, where you can climb <span class="arithmatex">\(1\)</span> or <span class="arithmatex">\(2\)</span> steps at a time, how many different ways are there to reach the top?</p>
@@ -3630,7 +3630,7 @@
<p><a class="glightbox" href="../intro_to_dynamic_programming.assets/climbing_stairs_example.png" data-type="image" data-width="100%" data-height="auto" data-desc-position="bottom"><img alt="Number of ways to reach the 3rd step" class="animation-figure" src="../intro_to_dynamic_programming.assets/climbing_stairs_example.png" /></a></p>
<p align="center"> Figure 14-1 &nbsp; Number of ways to reach the 3rd step </p>
<p>The goal of this problem is to determine the number of ways, <strong>considering using backtracking to exhaust all possibilities</strong>. Specifically, imagine climbing stairs as a multi-round choice process: starting from the ground, choosing to go up <span class="arithmatex">\(1\)</span> or <span class="arithmatex">\(2\)</span> steps each round, adding one to the count of ways upon reaching the top of the stairs, and pruning the process when exceeding the top. The code is as follows:</p>
<p>This problem aims to calculate the number of ways by <strong>using backtracking to exhaust all possibilities</strong>. Specifically, it considers the problem of climbing stairs as a multi-round choice process: starting from the ground, choosing to move up either <span class="arithmatex">\(1\)</span> or <span class="arithmatex">\(2\)</span> steps each round, incrementing the count of ways upon reaching the top of the stairs, and pruning the process when it exceeds the top. The code is as follows:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:14"><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" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><input id="__tabbed_1_13" name="__tabbed_1" type="radio" /><input id="__tabbed_1_14" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Python</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Java</label><label for="__tabbed_1_4">C#</label><label for="__tabbed_1_5">Go</label><label for="__tabbed_1_6">Swift</label><label for="__tabbed_1_7">JS</label><label for="__tabbed_1_8">TS</label><label for="__tabbed_1_9">Dart</label><label for="__tabbed_1_10">Rust</label><label for="__tabbed_1_11">C</label><label for="__tabbed_1_12">Kotlin</label><label for="__tabbed_1_13">Ruby</label><label for="__tabbed_1_14">Zig</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -3781,12 +3781,12 @@
</div>
</div>
<h2 id="1411-method-1-brute-force-search">14.1.1 &nbsp; Method 1: Brute force search<a class="headerlink" href="#1411-method-1-brute-force-search" title="Permanent link">&para;</a></h2>
<p>Backtracking algorithms do not explicitly decompose the problem but treat solving the problem as a series of decision steps, searching for all possible solutions through exploration and pruning.</p>
<p>We can try to analyze this problem from the perspective of decomposition. Let <span class="arithmatex">\(dp[i]\)</span> be the number of ways to reach the <span class="arithmatex">\(i^{th}\)</span> step, then <span class="arithmatex">\(dp[i]\)</span> is the original problem, and its subproblems include:</p>
<p>Backtracking algorithms do not explicitly decompose the problem into subproblems. Instead, they treat the problem as a sequence of decision steps, exploring all possibilities through trial and pruning.</p>
<p>We can analyze this problem using a decomposition approach. Let <span class="arithmatex">\(dp[i]\)</span> represent the number of ways to reach the <span class="arithmatex">\(i^{th}\)</span> step. In this case, <span class="arithmatex">\(dp[i]\)</span> is the original problem, and its subproblems are:</p>
<div class="arithmatex">\[
dp[i-1], dp[i-2], \dots, dp[2], dp[1]
\]</div>
<p>Since each round can only advance <span class="arithmatex">\(1\)</span> or <span class="arithmatex">\(2\)</span> steps, when we stand on the <span class="arithmatex">\(i^{th}\)</span> step, the previous round must have been either on the <span class="arithmatex">\(i-1^{th}\)</span> or the <span class="arithmatex">\(i-2^{th}\)</span> step. In other words, we can only step from the <span class="arithmatex">\(i-1^{th}\)</span> or the <span class="arithmatex">\(i-2^{th}\)</span> step to the <span class="arithmatex">\(i^{th}\)</span> step.</p>
<p>Since each move can only advance <span class="arithmatex">\(1\)</span> or <span class="arithmatex">\(2\)</span> steps, when we stand on the <span class="arithmatex">\(i^{th}\)</span> step, the previous step must have been either on the <span class="arithmatex">\(i-1^{th}\)</span> or the <span class="arithmatex">\(i-2^{th}\)</span> step. In other words, we can only reach the <span class="arithmatex">\(i^{th}\)</span> from the <span class="arithmatex">\(i-1^{th}\)</span> or <span class="arithmatex">\(i-2^{th}\)</span> step.</p>
<p>This leads to an important conclusion: <strong>the number of ways to reach the <span class="arithmatex">\(i-1^{th}\)</span> step plus the number of ways to reach the <span class="arithmatex">\(i-2^{th}\)</span> step equals the number of ways to reach the <span class="arithmatex">\(i^{th}\)</span> step</strong>. The formula is as follows:</p>
<div class="arithmatex">\[
dp[i] = dp[i-1] + dp[i-2]
@@ -3795,7 +3795,7 @@ dp[i] = dp[i-1] + dp[i-2]
<p><a class="glightbox" href="../intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png" data-type="image" data-width="100%" data-height="auto" data-desc-position="bottom"><img alt="Recursive relationship of solution counts" class="animation-figure" src="../intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png" /></a></p>
<p align="center"> Figure 14-2 &nbsp; Recursive relationship of solution counts </p>
<p>We can obtain the brute force search solution according to the recursive formula. Starting with <span class="arithmatex">\(dp[n]\)</span>, <strong>recursively decompose a larger problem into the sum of two smaller problems</strong>, until reaching the smallest subproblems <span class="arithmatex">\(dp[1]\)</span> and <span class="arithmatex">\(dp[2]\)</span> where the solutions are known, with <span class="arithmatex">\(dp[1] = 1\)</span> and <span class="arithmatex">\(dp[2] = 2\)</span>, representing <span class="arithmatex">\(1\)</span> and <span class="arithmatex">\(2\)</span> ways to climb to the first and second steps, respectively.</p>
<p>We can obtain the brute force search solution according to the recursive formula. Starting with <span class="arithmatex">\(dp[n]\)</span>, <strong>we recursively break a larger problem into the sum of two smaller subproblems</strong>, until reaching the smallest subproblems <span class="arithmatex">\(dp[1]\)</span> and <span class="arithmatex">\(dp[2]\)</span> where the solutions are known, with <span class="arithmatex">\(dp[1] = 1\)</span> and <span class="arithmatex">\(dp[2] = 2\)</span>, representing <span class="arithmatex">\(1\)</span> and <span class="arithmatex">\(2\)</span> ways to climb to the first and second steps, respectively.</p>
<p>Observe the following code, which, like standard backtracking code, belongs to depth-first search but is more concise:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:14"><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" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><input id="__tabbed_2_13" name="__tabbed_2" type="radio" /><input id="__tabbed_2_14" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Python</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Java</label><label for="__tabbed_2_4">C#</label><label for="__tabbed_2_5">Go</label><label for="__tabbed_2_6">Swift</label><label for="__tabbed_2_7">JS</label><label for="__tabbed_2_8">TS</label><label for="__tabbed_2_9">Dart</label><label for="__tabbed_2_10">Rust</label><label for="__tabbed_2_11">C</label><label for="__tabbed_2_12">Kotlin</label><label for="__tabbed_2_13">Ruby</label><label for="__tabbed_2_14">Zig</label></div>
<div class="tabbed-content">
@@ -3916,11 +3916,11 @@ dp[i] = dp[i-1] + dp[i-2]
</div>
</div>
</div>
<p>Figure 14-3 shows the recursive tree formed by brute force search. For the problem <span class="arithmatex">\(dp[n]\)</span>, the depth of its recursive tree is <span class="arithmatex">\(n\)</span>, with a time complexity of <span class="arithmatex">\(O(2^n)\)</span>. Exponential order represents explosive growth, and entering a long wait if a relatively large <span class="arithmatex">\(n\)</span> is input.</p>
<p>Figure 14-3 shows the recursive tree formed by brute force search. For the problem <span class="arithmatex">\(dp[n]\)</span>, the depth of its recursive tree is <span class="arithmatex">\(n\)</span>, with a time complexity of <span class="arithmatex">\(O(2^n)\)</span>. This exponential growth causes the program to run much more slowly when <span class="arithmatex">\(n\)</span> is large, leading to long wait times.</p>
<p><a class="glightbox" href="../intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png" data-type="image" data-width="100%" data-height="auto" data-desc-position="bottom"><img alt="Recursive tree for climbing stairs" class="animation-figure" src="../intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png" /></a></p>
<p align="center"> Figure 14-3 &nbsp; Recursive tree for climbing stairs </p>
<p>Observing Figure 14-3, <strong>the exponential time complexity is caused by 'overlapping subproblems'</strong>. For example, <span class="arithmatex">\(dp[9]\)</span> is decomposed into <span class="arithmatex">\(dp[8]\)</span> and <span class="arithmatex">\(dp[7]\)</span>, <span class="arithmatex">\(dp[8]\)</span> into <span class="arithmatex">\(dp[7]\)</span> and <span class="arithmatex">\(dp[6]\)</span>, both containing the subproblem <span class="arithmatex">\(dp[7]\)</span>.</p>
<p>Observing Figure 14-3, <strong>the exponential time complexity is caused by 'overlapping subproblems'</strong>. For example, <span class="arithmatex">\(dp[9]\)</span> is broken down into <span class="arithmatex">\(dp[8]\)</span> and <span class="arithmatex">\(dp[7]\)</span>, and <span class="arithmatex">\(dp[8]\)</span> is further broken into <span class="arithmatex">\(dp[7]\)</span> and <span class="arithmatex">\(dp[6]\)</span>, both containing the subproblem <span class="arithmatex">\(dp[7]\)</span>.</p>
<p>Thus, subproblems include even smaller overlapping subproblems, endlessly. A vast majority of computational resources are wasted on these overlapping subproblems.</p>
<h2 id="1412-method-2-memoized-search">14.1.2 &nbsp; Method 2: Memoized search<a class="headerlink" href="#1412-method-2-memoized-search" title="Permanent link">&para;</a></h2>
<p>To enhance algorithm efficiency, <strong>we hope that all overlapping subproblems are calculated only once</strong>. For this purpose, we declare an array <code>mem</code> to record the solution of each subproblem, and prune overlapping subproblems during the search process.</p>
@@ -4075,9 +4075,9 @@ dp[i] = dp[i-1] + dp[i-2]
<p align="center"> Figure 14-4 &nbsp; Recursive tree with memoized search </p>
<h2 id="1413-method-3-dynamic-programming">14.1.3 &nbsp; Method 3: Dynamic programming<a class="headerlink" href="#1413-method-3-dynamic-programming" title="Permanent link">&para;</a></h2>
<p><strong>Memoized search is a 'top-down' method</strong>: we start with the original problem (root node), recursively decompose larger subproblems into smaller ones until the solutions to the smallest known subproblems (leaf nodes) are reached. Subsequently, by backtracking, we collect the solutions of the subproblems, constructing the solution to the original problem.</p>
<p>On the contrary, <strong>dynamic programming is a 'bottom-up' method</strong>: starting with the solutions to the smallest subproblems, iteratively construct the solutions to larger subproblems until the original problem is solved.</p>
<p>Since dynamic programming does not include a backtracking process, it only requires looping iteration to implement, without needing recursion. In the following code, we initialize an array <code>dp</code> to store the solutions to the subproblems, serving the same recording function as the array <code>mem</code> in memoized search:</p>
<p><strong>Memoized search is a 'top-down' method</strong>: we start with the original problem (root node), recursively break larger subproblems into smaller ones until the solutions to the smallest known subproblems (leaf nodes) are reached. Subsequently, by backtracking, we collect the solutions of the subproblems, constructing the solution to the original problem.</p>
<p>On the contrary, <strong>dynamic programming is a 'bottom-up' method</strong>: starting with the solutions to the smallest subproblems, it iteratively constructs the solutions to larger subproblems until the original problem is solved.</p>
<p>Since dynamic programming does not involve backtracking, it only requires iteration using loops and does not need recursion. In the following code, we initialize an array <code>dp</code> to store the solutions to subproblems, serving the same recording function as the array <code>mem</code> in memoized search:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:14"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><input id="__tabbed_4_10" name="__tabbed_4" type="radio" /><input id="__tabbed_4_11" name="__tabbed_4" type="radio" /><input id="__tabbed_4_12" name="__tabbed_4" type="radio" /><input id="__tabbed_4_13" name="__tabbed_4" type="radio" /><input id="__tabbed_4_14" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Python</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Java</label><label for="__tabbed_4_4">C#</label><label for="__tabbed_4_5">Go</label><label for="__tabbed_4_6">Swift</label><label for="__tabbed_4_7">JS</label><label for="__tabbed_4_8">TS</label><label for="__tabbed_4_9">Dart</label><label for="__tabbed_4_10">Rust</label><label for="__tabbed_4_11">C</label><label for="__tabbed_4_12">Kotlin</label><label for="__tabbed_4_13">Ruby</label><label for="__tabbed_4_14">Zig</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4280,7 +4280,7 @@ dp[i] = dp[i-1] + dp[i-2]
</div>
</div>
<p>Observing the above code, since the space occupied by the array <code>dp</code> is eliminated, the space complexity is reduced from <span class="arithmatex">\(O(n)\)</span> to <span class="arithmatex">\(O(1)\)</span>.</p>
<p>In dynamic programming problems, the current state is often only related to a limited number of previous states, allowing us to retain only the necessary states and save memory space by "dimension reduction". <strong>This space optimization technique is known as 'rolling variable' or 'rolling array'</strong>.</p>
<p>In many dynamic programming problems, the current state depends only on a limited number of previous states, allowing us to retain only the necessary states and save memory space by "dimension reduction". <strong>This space optimization technique is known as 'rolling variable' or 'rolling array'</strong>.</p>
<!-- Source file information -->