This commit is contained in:
krahets
2025-12-31 19:37:45 +08:00
parent 29ec0c699d
commit 3c9d5689c4
279 changed files with 40895 additions and 16087 deletions

View File

@@ -2,55 +2,55 @@
comments: true
---
# 14.2   Characteristics of dynamic programming problems
# 14.2   Characteristics of Dynamic Programming Problems
In the previous section, we learned how dynamic programming solves the original problem by decomposing it into subproblems. In fact, subproblem decomposition is a general algorithmic approach, with different emphases in divide and conquer, dynamic programming, and backtracking.
- Divide and conquer algorithms recursively divide the original problem into multiple independent subproblems until the smallest subproblems are reached, and combine the solutions of the subproblems during backtracking to ultimately obtain the solution to the original problem.
- Dynamic programming also decomposes the problem recursively, but the main difference from divide and conquer algorithms is that the subproblems in dynamic programming are interdependent, and many overlapping subproblems will appear during the decomposition process.
- Backtracking algorithms exhaust all possible solutions through trial and error and avoid unnecessary search branches by pruning. The solution to the original problem consists of a series of decision steps, and we can consider each sub-sequence before each decision step as a subproblem.
- Divide and conquer algorithms recursively divide the original problem into multiple independent subproblems until the smallest subproblems are reached, and merge the solutions to the subproblems during backtracking to ultimately obtain the solution to the original problem.
- Dynamic programming also recursively decomposes problems, but the main difference from divide and conquer algorithms is that subproblems in dynamic programming are interdependent, and many overlapping subproblems appear during the decomposition process.
- Backtracking algorithms enumerate all possible solutions through trial and error, and avoid unnecessary search branches through pruning. The solution to the original problem consists of a series of decision steps, and we can regard the subsequence before each decision step as a subproblem.
In fact, dynamic programming is commonly used to solve optimization problems, which not only include overlapping subproblems but also have two other major characteristics: optimal substructure and statelessness.
In fact, dynamic programming is commonly used to solve optimization problems, which not only contain overlapping subproblems but also have two other major characteristics: optimal substructure and no aftereffects.
## 14.2.1   Optimal substructure
## 14.2.1   Optimal Substructure
We make a slight modification to the stair climbing problem to make it more suitable to demonstrate the concept of optimal substructure.
We make a slight modification to the stair climbing problem to make it more suitable for demonstrating the concept of optimal substructure.
!!! question "Minimum cost of climbing stairs"
!!! question "Climbing stairs with minimum cost"
Given a staircase, you can step up 1 or 2 steps at a time, and each step on the staircase has a non-negative integer representing the cost you need to pay at that step. Given a non-negative integer array $cost$, where $cost[i]$ represents the cost you need to pay at the $i$-th step, $cost[0]$ is the ground (starting point). What is the minimum cost required to reach the top?
Given a staircase, where you can climb $1$ or $2$ steps at a time, and each step has a non-negative integer representing the cost you need to pay at that step. Given a non-negative integer array $cost$, where $cost[i]$ represents the cost at the $i$-th step, and $cost[0]$ is the ground (starting point). What is the minimum cost required to reach the top?
As shown in Figure 14-6, if the costs of the 1st, 2nd, and 3rd steps are $1$, $10$, and $1$ respectively, then the minimum cost to climb to the 3rd step from the ground is $2$.
As shown in Figure 14-6, if the costs of the $1$st, $2$nd, and $3$rd steps are $1$, $10$, and $1$ respectively, then climbing from the ground to the $3$rd step requires a minimum cost of $2$.
![Minimum cost to climb to the 3rd step](dp_problem_features.assets/min_cost_cs_example.png){ class="animation-figure" }
<p align="center"> Figure 14-6 &nbsp; Minimum cost to climb to the 3rd step </p>
Let $dp[i]$ be the cumulative cost of climbing to the $i$-th step. Since the $i$-th step can only come from the $i-1$ or $i-2$ step, $dp[i]$ can only be either $dp[i-1] + cost[i]$ or $dp[i-2] + cost[i]$. To minimize the cost, we should choose the smaller of the two:
Let $dp[i]$ be the accumulated cost of climbing to the $i$-th step. Since the $i$-th step can only come from the $i-1$-th or $i-2$-th step, $dp[i]$ can only equal $dp[i-1] + cost[i]$ or $dp[i-2] + cost[i]$. To minimize the cost, we should choose the smaller of the two:
$$
dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
$$
This leads us to the meaning of optimal substructure: **The optimal solution to the original problem is constructed from the optimal solutions of subproblems**.
This leads us to the meaning of optimal substructure: **the optimal solution to the original problem is constructed from the optimal solutions to the subproblems**.
This problem obviously has optimal substructure: we select the better one from the optimal solutions of the two subproblems, $dp[i-1]$ and $dp[i-2]$, and use it to construct the optimal solution for the original problem $dp[i]$.
This problem clearly has optimal substructure: we select the better one from the optimal solutions to the two subproblems $dp[i-1]$ and $dp[i-2]$, and use it to construct the optimal solution to the original problem $dp[i]$.
So, does the stair climbing problem from the previous section have optimal substructure? Its goal is to solve for the number of solutions, which seems to be a counting problem, but if we ask in another way: "Solve for the maximum number of solutions". We surprisingly find that **although the problem has changed, the optimal substructure has emerged**: the maximum number of solutions at the $n$-th step equals the sum of the maximum number of solutions at the $n-1$ and $n-2$ steps. Thus, the interpretation of optimal substructure is quite flexible and will have different meanings in different problems.
So, does the stair climbing problem from the previous section have optimal substructure? Its goal is to find the number of ways, which seems to be a counting problem, but if we change the question: "Find the maximum number of ways". We surprisingly discover that **although the problem before and after modification are equivalent, the optimal substructure has emerged**: the maximum number of ways for the $n$-th step equals the sum of the maximum number of ways for the $n-1$-th and $n-2$-th steps. Therefore, the interpretation of optimal substructure is quite flexible and will have different meanings in different problems.
According to the state transition equation, and the initial states $dp[1] = cost[1]$ and $dp[2] = cost[2]$, we can obtain the dynamic programming code:
According to the state transition equation and the initial states $dp[1] = cost[1]$ and $dp[2] = cost[2]$, we can obtain the dynamic programming code:
=== "Python"
```python title="min_cost_climbing_stairs_dp.py"
def min_cost_climbing_stairs_dp(cost: list[int]) -> int:
"""Climbing stairs with minimum cost: Dynamic programming"""
"""Minimum cost climbing stairs: Dynamic programming"""
n = len(cost) - 1
if n == 1 or n == 2:
return cost[n]
# Initialize dp table, used to store subproblem solutions
# Initialize dp table, used to store solutions to subproblems
dp = [0] * (n + 1)
# Initial state: preset the smallest subproblem solution
# Initial state: preset the solution to the smallest subproblem
dp[1], dp[2] = cost[1], cost[2]
# State transition: gradually solve larger subproblems from smaller ones
for i in range(3, n + 1):
@@ -61,14 +61,14 @@ According to the state transition equation, and the initial states $dp[1] = cost
=== "C++"
```cpp title="min_cost_climbing_stairs_dp.cpp"
/* Climbing stairs with minimum cost: Dynamic programming */
/* Minimum cost climbing stairs: Dynamic programming */
int minCostClimbingStairsDP(vector<int> &cost) {
int n = cost.size() - 1;
if (n == 1 || n == 2)
return cost[n];
// Initialize dp table, used to store subproblem solutions
// Initialize dp table, used to store solutions to subproblems
vector<int> dp(n + 1);
// Initial state: preset the smallest subproblem solution
// Initial state: preset the solution to the smallest subproblem
dp[1] = cost[1];
dp[2] = cost[2];
// State transition: gradually solve larger subproblems from smaller ones
@@ -82,14 +82,14 @@ According to the state transition equation, and the initial states $dp[1] = cost
=== "Java"
```java title="min_cost_climbing_stairs_dp.java"
/* Climbing stairs with minimum cost: Dynamic programming */
/* Minimum cost climbing stairs: Dynamic programming */
int minCostClimbingStairsDP(int[] cost) {
int n = cost.length - 1;
if (n == 1 || n == 2)
return cost[n];
// Initialize dp table, used to store subproblem solutions
// Initialize dp table, used to store solutions to subproblems
int[] dp = new int[n + 1];
// Initial state: preset the smallest subproblem solution
// Initial state: preset the solution to the smallest subproblem
dp[1] = cost[1];
dp[2] = cost[2];
// State transition: gradually solve larger subproblems from smaller ones
@@ -103,82 +103,234 @@ According to the state transition equation, and the initial states $dp[1] = cost
=== "C#"
```csharp title="min_cost_climbing_stairs_dp.cs"
[class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDP}
/* Minimum cost climbing stairs: Dynamic programming */
int MinCostClimbingStairsDP(int[] cost) {
int n = cost.Length - 1;
if (n == 1 || n == 2)
return cost[n];
// Initialize dp table, used to store solutions to subproblems
int[] dp = new int[n + 1];
// Initial state: preset the solution to the smallest subproblem
dp[1] = cost[1];
dp[2] = cost[2];
// State transition: gradually solve larger subproblems from smaller ones
for (int i = 3; i <= n; i++) {
dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "Go"
```go title="min_cost_climbing_stairs_dp.go"
[class]{}-[func]{minCostClimbingStairsDP}
/* Minimum cost climbing stairs: Dynamic programming */
func minCostClimbingStairsDP(cost []int) int {
n := len(cost) - 1
if n == 1 || n == 2 {
return cost[n]
}
min := func(a, b int) int {
if a < b {
return a
}
return b
}
// Initialize dp table, used to store solutions to subproblems
dp := make([]int, n+1)
// Initial state: preset the solution to the smallest subproblem
dp[1] = cost[1]
dp[2] = cost[2]
// State transition: gradually solve larger subproblems from smaller ones
for i := 3; i <= n; i++ {
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
}
return dp[n]
}
```
=== "Swift"
```swift title="min_cost_climbing_stairs_dp.swift"
[class]{}-[func]{minCostClimbingStairsDP}
/* Minimum cost climbing stairs: Dynamic programming */
func minCostClimbingStairsDP(cost: [Int]) -> Int {
let n = cost.count - 1
if n == 1 || n == 2 {
return cost[n]
}
// Initialize dp table, used to store solutions to subproblems
var dp = Array(repeating: 0, count: n + 1)
// Initial state: preset the solution to the smallest subproblem
dp[1] = cost[1]
dp[2] = cost[2]
// State transition: gradually solve larger subproblems from smaller ones
for i in 3 ... n {
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
}
return dp[n]
}
```
=== "JS"
```javascript title="min_cost_climbing_stairs_dp.js"
[class]{}-[func]{minCostClimbingStairsDP}
/* Minimum cost climbing stairs: Dynamic programming */
function minCostClimbingStairsDP(cost) {
const n = cost.length - 1;
if (n === 1 || n === 2) {
return cost[n];
}
// Initialize dp table, used to store solutions to subproblems
const dp = new Array(n + 1);
// Initial state: preset the solution to the smallest subproblem
dp[1] = cost[1];
dp[2] = cost[2];
// State transition: gradually solve larger subproblems from smaller ones
for (let i = 3; i <= n; i++) {
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "TS"
```typescript title="min_cost_climbing_stairs_dp.ts"
[class]{}-[func]{minCostClimbingStairsDP}
/* Minimum cost climbing stairs: Dynamic programming */
function minCostClimbingStairsDP(cost: Array<number>): number {
const n = cost.length - 1;
if (n === 1 || n === 2) {
return cost[n];
}
// Initialize dp table, used to store solutions to subproblems
const dp = new Array(n + 1);
// Initial state: preset the solution to the smallest subproblem
dp[1] = cost[1];
dp[2] = cost[2];
// State transition: gradually solve larger subproblems from smaller ones
for (let i = 3; i <= n; i++) {
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "Dart"
```dart title="min_cost_climbing_stairs_dp.dart"
[class]{}-[func]{minCostClimbingStairsDP}
/* Minimum cost climbing stairs: Dynamic programming */
int minCostClimbingStairsDP(List<int> cost) {
int n = cost.length - 1;
if (n == 1 || n == 2) return cost[n];
// Initialize dp table, used to store solutions to subproblems
List<int> dp = List.filled(n + 1, 0);
// Initial state: preset the solution to the smallest subproblem
dp[1] = cost[1];
dp[2] = cost[2];
// State transition: gradually solve larger subproblems from smaller ones
for (int i = 3; i <= n; i++) {
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "Rust"
```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp}
/* Minimum cost climbing stairs: Dynamic programming */
fn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 {
let n = cost.len() - 1;
if n == 1 || n == 2 {
return cost[n];
}
// Initialize dp table, used to store solutions to subproblems
let mut dp = vec![-1; n + 1];
// Initial state: preset the solution to the smallest subproblem
dp[1] = cost[1];
dp[2] = cost[2];
// State transition: gradually solve larger subproblems from smaller ones
for i in 3..=n {
dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];
}
dp[n]
}
```
=== "C"
```c title="min_cost_climbing_stairs_dp.c"
[class]{}-[func]{minCostClimbingStairsDP}
/* Minimum cost climbing stairs: Dynamic programming */
int minCostClimbingStairsDP(int cost[], int costSize) {
int n = costSize - 1;
if (n == 1 || n == 2)
return cost[n];
// Initialize dp table, used to store solutions to subproblems
int *dp = calloc(n + 1, sizeof(int));
// Initial state: preset the solution to the smallest subproblem
dp[1] = cost[1];
dp[2] = cost[2];
// State transition: gradually solve larger subproblems from smaller ones
for (int i = 3; i <= n; i++) {
dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];
}
int res = dp[n];
// Free memory
free(dp);
return res;
}
```
=== "Kotlin"
```kotlin title="min_cost_climbing_stairs_dp.kt"
[class]{}-[func]{minCostClimbingStairsDP}
/* Minimum cost climbing stairs: Dynamic programming */
fun minCostClimbingStairsDP(cost: IntArray): Int {
val n = cost.size - 1
if (n == 1 || n == 2) return cost[n]
// Initialize dp table, used to store solutions to subproblems
val dp = IntArray(n + 1)
// Initial state: preset the solution to the smallest subproblem
dp[1] = cost[1]
dp[2] = cost[2]
// State transition: gradually solve larger subproblems from smaller ones
for (i in 3..n) {
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
}
return dp[n]
}
```
=== "Ruby"
```ruby title="min_cost_climbing_stairs_dp.rb"
[class]{}-[func]{min_cost_climbing_stairs_dp}
```
=== "Zig"
```zig title="min_cost_climbing_stairs_dp.zig"
[class]{}-[func]{minCostClimbingStairsDP}
### Minimum cost climbing stairs: DP ###
def min_cost_climbing_stairs_dp(cost)
n = cost.length - 1
return cost[n] if n == 1 || n == 2
# Initialize dp table, used to store solutions to subproblems
dp = Array.new(n + 1, 0)
# Initial state: preset the solution to the smallest subproblem
dp[1], dp[2] = cost[1], cost[2]
# State transition: gradually solve larger subproblems from smaller ones
(3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }
dp[n]
end
```
Figure 14-7 shows the dynamic programming process for the above code.
![Dynamic programming process for minimum cost of climbing stairs](dp_problem_features.assets/min_cost_cs_dp.png){ class="animation-figure" }
![Dynamic programming process for climbing stairs with minimum cost](dp_problem_features.assets/min_cost_cs_dp.png){ class="animation-figure" }
<p align="center"> Figure 14-7 &nbsp; Dynamic programming process for minimum cost of climbing stairs </p>
<p align="center"> Figure 14-7 &nbsp; Dynamic programming process for climbing stairs with minimum cost </p>
This problem can also be space-optimized, compressing one dimension to zero, reducing the space complexity from $O(n)$ to $O(1)$:
This problem can also be space-optimized, compressing from one dimension to zero, reducing the space complexity from $O(n)$ to $O(1)$:
=== "Python"
```python title="min_cost_climbing_stairs_dp.py"
def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:
"""Climbing stairs with minimum cost: Space-optimized dynamic programming"""
"""Minimum cost climbing stairs: Space-optimized dynamic programming"""
n = len(cost) - 1
if n == 1 or n == 2:
return cost[n]
@@ -191,7 +343,7 @@ This problem can also be space-optimized, compressing one dimension to zero, red
=== "C++"
```cpp title="min_cost_climbing_stairs_dp.cpp"
/* Climbing stairs with minimum cost: Space-optimized dynamic programming */
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
int minCostClimbingStairsDPComp(vector<int> &cost) {
int n = cost.size() - 1;
if (n == 1 || n == 2)
@@ -209,7 +361,7 @@ This problem can also be space-optimized, compressing one dimension to zero, red
=== "Java"
```java title="min_cost_climbing_stairs_dp.java"
/* Climbing stairs with minimum cost: Space-optimized dynamic programming */
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
int minCostClimbingStairsDPComp(int[] cost) {
int n = cost.length - 1;
if (n == 1 || n == 2)
@@ -227,97 +379,231 @@ This problem can also be space-optimized, compressing one dimension to zero, red
=== "C#"
```csharp title="min_cost_climbing_stairs_dp.cs"
[class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDPComp}
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
int MinCostClimbingStairsDPComp(int[] cost) {
int n = cost.Length - 1;
if (n == 1 || n == 2)
return cost[n];
int a = cost[1], b = cost[2];
for (int i = 3; i <= n; i++) {
int tmp = b;
b = Math.Min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "Go"
```go title="min_cost_climbing_stairs_dp.go"
[class]{}-[func]{minCostClimbingStairsDPComp}
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
func minCostClimbingStairsDPComp(cost []int) int {
n := len(cost) - 1
if n == 1 || n == 2 {
return cost[n]
}
min := func(a, b int) int {
if a < b {
return a
}
return b
}
// Initial state: preset the solution to the smallest subproblem
a, b := cost[1], cost[2]
// State transition: gradually solve larger subproblems from smaller ones
for i := 3; i <= n; i++ {
tmp := b
b = min(a, tmp) + cost[i]
a = tmp
}
return b
}
```
=== "Swift"
```swift title="min_cost_climbing_stairs_dp.swift"
[class]{}-[func]{minCostClimbingStairsDPComp}
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
func minCostClimbingStairsDPComp(cost: [Int]) -> Int {
let n = cost.count - 1
if n == 1 || n == 2 {
return cost[n]
}
var (a, b) = (cost[1], cost[2])
for i in 3 ... n {
(a, b) = (b, min(a, b) + cost[i])
}
return b
}
```
=== "JS"
```javascript title="min_cost_climbing_stairs_dp.js"
[class]{}-[func]{minCostClimbingStairsDPComp}
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
function minCostClimbingStairsDPComp(cost) {
const n = cost.length - 1;
if (n === 1 || n === 2) {
return cost[n];
}
let a = cost[1],
b = cost[2];
for (let i = 3; i <= n; i++) {
const tmp = b;
b = Math.min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "TS"
```typescript title="min_cost_climbing_stairs_dp.ts"
[class]{}-[func]{minCostClimbingStairsDPComp}
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
function minCostClimbingStairsDPComp(cost: Array<number>): number {
const n = cost.length - 1;
if (n === 1 || n === 2) {
return cost[n];
}
let a = cost[1],
b = cost[2];
for (let i = 3; i <= n; i++) {
const tmp = b;
b = Math.min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "Dart"
```dart title="min_cost_climbing_stairs_dp.dart"
[class]{}-[func]{minCostClimbingStairsDPComp}
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
int minCostClimbingStairsDPComp(List<int> cost) {
int n = cost.length - 1;
if (n == 1 || n == 2) return cost[n];
int a = cost[1], b = cost[2];
for (int i = 3; i <= n; i++) {
int tmp = b;
b = min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "Rust"
```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
fn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 {
let n = cost.len() - 1;
if n == 1 || n == 2 {
return cost[n];
};
let (mut a, mut b) = (cost[1], cost[2]);
for i in 3..=n {
let tmp = b;
b = cmp::min(a, tmp) + cost[i];
a = tmp;
}
b
}
```
=== "C"
```c title="min_cost_climbing_stairs_dp.c"
[class]{}-[func]{minCostClimbingStairsDPComp}
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
int minCostClimbingStairsDPComp(int cost[], int costSize) {
int n = costSize - 1;
if (n == 1 || n == 2)
return cost[n];
int a = cost[1], b = cost[2];
for (int i = 3; i <= n; i++) {
int tmp = b;
b = myMin(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "Kotlin"
```kotlin title="min_cost_climbing_stairs_dp.kt"
[class]{}-[func]{minCostClimbingStairsDPComp}
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
fun minCostClimbingStairsDPComp(cost: IntArray): Int {
val n = cost.size - 1
if (n == 1 || n == 2) return cost[n]
var a = cost[1]
var b = cost[2]
for (i in 3..n) {
val tmp = b
b = min(a, tmp) + cost[i]
a = tmp
}
return b
}
```
=== "Ruby"
```ruby title="min_cost_climbing_stairs_dp.rb"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
### Minimum cost climbing stairs: DP ###
def min_cost_climbing_stairs_dp(cost)
n = cost.length - 1
return cost[n] if n == 1 || n == 2
# Initialize dp table, used to store solutions to subproblems
dp = Array.new(n + 1, 0)
# Initial state: preset the solution to the smallest subproblem
dp[1], dp[2] = cost[1], cost[2]
# State transition: gradually solve larger subproblems from smaller ones
(3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }
dp[n]
end
# Minimum cost climbing stairs: Space-optimized dynamic programming
def min_cost_climbing_stairs_dp_comp(cost)
n = cost.length - 1
return cost[n] if n == 1 || n == 2
a, b = cost[1], cost[2]
(3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] }
b
end
```
=== "Zig"
## 14.2.2 &nbsp; No Aftereffects
```zig title="min_cost_climbing_stairs_dp.zig"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
No aftereffects is one of the important characteristics that enable dynamic programming to solve problems effectively. Its definition is: **given a certain state, its future development is only related to the current state and has nothing to do with all past states**.
## 14.2.2 &nbsp; Statelessness
Statelessness is one of the important characteristics that make dynamic programming effective in solving problems. Its definition is: **Given a certain state, its future development is only related to the current state and unrelated to all past states experienced**.
Taking the stair climbing problem as an example, given state $i$, it will develop into states $i+1$ and $i+2$, corresponding to jumping 1 step and 2 steps respectively. When making these two choices, we do not need to consider the states before state $i$, as they do not affect the future of state $i$.
Taking the stair climbing problem as an example, given state $i$, it will develop into states $i+1$ and $i+2$, corresponding to jumping $1$ step and jumping $2$ steps, respectively. When making these two choices, we do not need to consider the states before state $i$, as they have no effect on the future of state $i$.
However, if we add a constraint to the stair climbing problem, the situation changes.
!!! question "Stair climbing with constraints"
!!! question "Climbing stairs with constraint"
Given a staircase with $n$ steps, you can go up 1 or 2 steps each time, **but you cannot jump 1 step twice in a row**. How many ways are there to climb to the top?
Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time, **but you cannot jump $1$ step in two consecutive rounds**. How many ways are there to climb to the top?
As shown in Figure 14-8, there are only 2 feasible options for climbing to the 3rd step, among which the option of jumping 1 step three times in a row does not meet the constraint condition and is therefore discarded.
As shown in Figure 14-8, there are only $2$ feasible ways to climb to the $3$rd step. The way of jumping $1$ step three consecutive times does not satisfy the constraint and is therefore discarded.
![Number of feasible options for climbing to the 3rd step with constraints](dp_problem_features.assets/climbing_stairs_constraint_example.png){ class="animation-figure" }
![Number of ways to climb to the 3rd step with constraint](dp_problem_features.assets/climbing_stairs_constraint_example.png){ class="animation-figure" }
<p align="center"> Figure 14-8 &nbsp; Number of feasible options for climbing to the 3rd step with constraints </p>
<p align="center"> Figure 14-8 &nbsp; Number of ways to climb to the 3rd step with constraint </p>
In this problem, if the last round was a jump of 1 step, then the next round must be a jump of 2 steps. This means that **the next step choice cannot be independently determined by the current state (current stair step), but also depends on the previous state (last round's stair step)**.
In this problem, if the previous round was a jump of $1$ step, then the next round must jump $2$ steps. This means that **the next choice cannot be determined solely by the current state (current stair step number), but also depends on the previous state (the stair step number from the previous round)**.
It is not difficult to find that this problem no longer satisfies statelessness, and the state transition equation $dp[i] = dp[i-1] + dp[i-2]$ also fails, because $dp[i-1]$ represents this round's jump of 1 step, but it includes many "last round was a jump of 1 step" options, which, to meet the constraint, cannot be directly included in $dp[i]$.
It is not difficult to see that this problem no longer satisfies no aftereffects, and the state transition equation $dp[i] = dp[i-1] + dp[i-2]$ also fails, because $dp[i-1]$ represents jumping $1$ step in this round, but it includes many solutions where "the previous round was a jump of $1$ step", which cannot be directly counted in $dp[i]$ to satisfy the constraint.
For this, we need to expand the state definition: **State $[i, j]$ represents being on the $i$-th step and the last round was a jump of $j$ steps**, where $j \in \{1, 2\}$. This state definition effectively distinguishes whether the last round was a jump of 1 step or 2 steps, and we can judge accordingly where the current state came from.
For this reason, we need to expand the state definition: **state $[i, j]$ represents being on the $i$-th step with the previous round having jumped $j$ steps**, where $j \in \{1, 2\}$. This state definition effectively distinguishes whether the previous round was a jump of $1$ step or $2$ steps, allowing us to determine where the current state came from.
- When the last round was a jump of 1 step, the round before last could only choose to jump 2 steps, that is, $dp[i, 1]$ can only be transferred from $dp[i-1, 2]$.
- When the last round was a jump of 2 steps, the round before last could choose to jump 1 step or 2 steps, that is, $dp[i, 2]$ can be transferred from $dp[i-2, 1]$ or $dp[i-2, 2]$.
- When the previous round jumped $1$ step, the round before that could only choose to jump $2$ steps, i.e., $dp[i, 1]$ can only be transferred from $dp[i-1, 2]$.
- When the previous round jumped $2$ steps, the round before that could choose to jump $1$ step or $2$ steps, i.e., $dp[i, 2]$ can be transferred from $dp[i-2, 1]$ or $dp[i-2, 2]$.
As shown in Figure 14-9, $dp[i, j]$ represents the number of solutions for state $[i, j]$. At this point, the state transition equation is:
As shown in Figure 14-9, under this definition, $dp[i, j]$ represents the number of ways for state $[i, j]$. The state transition equation is then:
$$
\begin{cases}
@@ -326,22 +612,22 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
\end{cases}
$$
![Recursive relationship considering constraints](dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png){ class="animation-figure" }
![Recurrence relation considering constraints](dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png){ class="animation-figure" }
<p align="center"> Figure 14-9 &nbsp; Recursive relationship considering constraints </p>
<p align="center"> Figure 14-9 &nbsp; Recurrence relation considering constraints </p>
In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two representing the total number of solutions for climbing to the $n$-th step:
Finally, return $dp[n, 1] + dp[n, 2]$, where the sum of the two represents the total number of ways to climb to the $n$-th step:
=== "Python"
```python title="climbing_stairs_constraint_dp.py"
def climbing_stairs_constraint_dp(n: int) -> int:
"""Constrained climbing stairs: Dynamic programming"""
"""Climbing stairs with constraint: Dynamic programming"""
if n == 1 or n == 2:
return 1
# Initialize dp table, used to store subproblem solutions
# Initialize dp table, used to store solutions to subproblems
dp = [[0] * 3 for _ in range(n + 1)]
# Initial state: preset the smallest subproblem solution
# Initial state: preset the solution to the smallest subproblem
dp[1][1], dp[1][2] = 1, 0
dp[2][1], dp[2][2] = 0, 1
# State transition: gradually solve larger subproblems from smaller ones
@@ -354,14 +640,14 @@ In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two represen
=== "C++"
```cpp title="climbing_stairs_constraint_dp.cpp"
/* Constrained climbing stairs: Dynamic programming */
/* Climbing stairs with constraint: Dynamic programming */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// Initialize dp table, used to store subproblem solutions
// Initialize dp table, used to store solutions to subproblems
vector<vector<int>> dp(n + 1, vector<int>(3, 0));
// Initial state: preset the smallest subproblem solution
// Initial state: preset the solution to the smallest subproblem
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
@@ -378,14 +664,14 @@ In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two represen
=== "Java"
```java title="climbing_stairs_constraint_dp.java"
/* Constrained climbing stairs: Dynamic programming */
/* Climbing stairs with constraint: Dynamic programming */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// Initialize dp table, used to store subproblem solutions
// Initialize dp table, used to store solutions to subproblems
int[][] dp = new int[n + 1][3];
// Initial state: preset the smallest subproblem solution
// Initial state: preset the solution to the smallest subproblem
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
@@ -402,75 +688,256 @@ In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two represen
=== "C#"
```csharp title="climbing_stairs_constraint_dp.cs"
[class]{climbing_stairs_constraint_dp}-[func]{ClimbingStairsConstraintDP}
/* Climbing stairs with constraint: Dynamic programming */
int ClimbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// Initialize dp table, used to store solutions to subproblems
int[,] dp = new int[n + 1, 3];
// Initial state: preset the solution to the smallest subproblem
dp[1, 1] = 1;
dp[1, 2] = 0;
dp[2, 1] = 0;
dp[2, 2] = 1;
// State transition: gradually solve larger subproblems from smaller ones
for (int i = 3; i <= n; i++) {
dp[i, 1] = dp[i - 1, 2];
dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];
}
return dp[n, 1] + dp[n, 2];
}
```
=== "Go"
```go title="climbing_stairs_constraint_dp.go"
[class]{}-[func]{climbingStairsConstraintDP}
/* Climbing stairs with constraint: Dynamic programming */
func climbingStairsConstraintDP(n int) int {
if n == 1 || n == 2 {
return 1
}
// Initialize dp table, used to store solutions to subproblems
dp := make([][3]int, n+1)
// Initial state: preset the solution to the smallest subproblem
dp[1][1] = 1
dp[1][2] = 0
dp[2][1] = 0
dp[2][2] = 1
// State transition: gradually solve larger subproblems from smaller ones
for i := 3; i <= n; i++ {
dp[i][1] = dp[i-1][2]
dp[i][2] = dp[i-2][1] + dp[i-2][2]
}
return dp[n][1] + dp[n][2]
}
```
=== "Swift"
```swift title="climbing_stairs_constraint_dp.swift"
[class]{}-[func]{climbingStairsConstraintDP}
/* Climbing stairs with constraint: Dynamic programming */
func climbingStairsConstraintDP(n: Int) -> Int {
if n == 1 || n == 2 {
return 1
}
// Initialize dp table, used to store solutions to subproblems
var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)
// Initial state: preset the solution to the smallest subproblem
dp[1][1] = 1
dp[1][2] = 0
dp[2][1] = 0
dp[2][2] = 1
// State transition: gradually solve larger subproblems from smaller ones
for i in 3 ... n {
dp[i][1] = dp[i - 1][2]
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
}
return dp[n][1] + dp[n][2]
}
```
=== "JS"
```javascript title="climbing_stairs_constraint_dp.js"
[class]{}-[func]{climbingStairsConstraintDP}
/* Climbing stairs with constraint: Dynamic programming */
function climbingStairsConstraintDP(n) {
if (n === 1 || n === 2) {
return 1;
}
// Initialize dp table, used to store solutions to subproblems
const dp = Array.from(new Array(n + 1), () => new Array(3));
// Initial state: preset the solution to the smallest subproblem
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// State transition: gradually solve larger subproblems from smaller ones
for (let i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
=== "TS"
```typescript title="climbing_stairs_constraint_dp.ts"
[class]{}-[func]{climbingStairsConstraintDP}
/* Climbing stairs with constraint: Dynamic programming */
function climbingStairsConstraintDP(n: number): number {
if (n === 1 || n === 2) {
return 1;
}
// Initialize dp table, used to store solutions to subproblems
const dp = Array.from({ length: n + 1 }, () => new Array(3));
// Initial state: preset the solution to the smallest subproblem
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// State transition: gradually solve larger subproblems from smaller ones
for (let i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
=== "Dart"
```dart title="climbing_stairs_constraint_dp.dart"
[class]{}-[func]{climbingStairsConstraintDP}
/* Climbing stairs with constraint: Dynamic programming */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// Initialize dp table, used to store solutions to subproblems
List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));
// Initial state: preset the solution to the smallest subproblem
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// State transition: gradually solve larger subproblems from smaller ones
for (int i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
=== "Rust"
```rust title="climbing_stairs_constraint_dp.rs"
[class]{}-[func]{climbing_stairs_constraint_dp}
/* Climbing stairs with constraint: Dynamic programming */
fn climbing_stairs_constraint_dp(n: usize) -> i32 {
if n == 1 || n == 2 {
return 1;
};
// Initialize dp table, used to store solutions to subproblems
let mut dp = vec![vec![-1; 3]; n + 1];
// Initial state: preset the solution to the smallest subproblem
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// State transition: gradually solve larger subproblems from smaller ones
for i in 3..=n {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
dp[n][1] + dp[n][2]
}
```
=== "C"
```c title="climbing_stairs_constraint_dp.c"
[class]{}-[func]{climbingStairsConstraintDP}
/* Climbing stairs with constraint: Dynamic programming */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// Initialize dp table, used to store solutions to subproblems
int **dp = malloc((n + 1) * sizeof(int *));
for (int i = 0; i <= n; i++) {
dp[i] = calloc(3, sizeof(int));
}
// Initial state: preset the solution to the smallest subproblem
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// State transition: gradually solve larger subproblems from smaller ones
for (int i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
int res = dp[n][1] + dp[n][2];
// Free memory
for (int i = 0; i <= n; i++) {
free(dp[i]);
}
free(dp);
return res;
}
```
=== "Kotlin"
```kotlin title="climbing_stairs_constraint_dp.kt"
[class]{}-[func]{climbingStairsConstraintDP}
/* Climbing stairs with constraint: Dynamic programming */
fun climbingStairsConstraintDP(n: Int): Int {
if (n == 1 || n == 2) {
return 1
}
// Initialize dp table, used to store solutions to subproblems
val dp = Array(n + 1) { IntArray(3) }
// Initial state: preset the solution to the smallest subproblem
dp[1][1] = 1
dp[1][2] = 0
dp[2][1] = 0
dp[2][2] = 1
// State transition: gradually solve larger subproblems from smaller ones
for (i in 3..n) {
dp[i][1] = dp[i - 1][2]
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
}
return dp[n][1] + dp[n][2]
}
```
=== "Ruby"
```ruby title="climbing_stairs_constraint_dp.rb"
[class]{}-[func]{climbing_stairs_constraint_dp}
### Climbing stairs with constraint: DP ###
def climbing_stairs_constraint_dp(n)
return 1 if n == 1 || n == 2
# Initialize dp table, used to store solutions to subproblems
dp = Array.new(n + 1) { Array.new(3, 0) }
# Initial state: preset the solution to the smallest subproblem
dp[1][1], dp[1][2] = 1, 0
dp[2][1], dp[2][2] = 0, 1
# State transition: gradually solve larger subproblems from smaller ones
for i in 3...(n + 1)
dp[i][1] = dp[i - 1][2]
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
end
dp[n][1] + dp[n][2]
end
```
=== "Zig"
In the above case, since we only need to consider one more preceding state, we can still make the problem satisfy no aftereffects by expanding the state definition. However, some problems have very severe "aftereffects".
```zig title="climbing_stairs_constraint_dp.zig"
[class]{}-[func]{climbingStairsConstraintDP}
```
!!! question "Climbing stairs with obstacle generation"
In the above cases, since we only need to consider the previous state, we can still meet the statelessness by expanding the state definition. However, some problems have very serious "state effects".
Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time. **It is stipulated that when climbing to the $i$-th step, the system will automatically place an obstacle on the $2i$-th step, and thereafter no round is allowed to jump to the $2i$-th step**. For example, if the first two rounds jump to the $2$nd and $3$rd steps, then afterwards you cannot jump to the $4$th and $6$th steps. How many ways are there to climb to the top?
!!! question "Stair climbing with obstacle generation"
In this problem, the next jump depends on all past states, because each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming is often difficult to solve.
Given a staircase with $n$ steps, you can go up 1 or 2 steps each time. **It is stipulated that when climbing to the $i$-th step, the system automatically places an obstacle on the $2i$-th step, and thereafter all rounds are not allowed to jump to the $2i$-th step**. For example, if the first two rounds jump to the 2nd and 3rd steps, then later you cannot jump to the 4th and 6th steps. How many ways are there to climb to the top?
In this problem, the next jump depends on all past states, as each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming often struggles to solve.
In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy statelessness. For these kinds of problems, we usually choose to use other methods, such as heuristic search, genetic algorithms, reinforcement learning, etc., to obtain usable local optimal solutions within a limited time.
In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy no aftereffects. For such problems, we usually choose to use other methods, such as heuristic search, genetic algorithms, reinforcement learning, etc., to obtain usable local optimal solutions within a limited time.

File diff suppressed because it is too large Load Diff

View File

@@ -2,80 +2,80 @@
comments: true
---
# 14.6 &nbsp; Edit distance problem
# 14.6 &nbsp; Edit Distance Problem
Edit distance, also known as Levenshtein distance, refers to the minimum number of modifications required to transform one string into another, commonly used in information retrieval and natural language processing to measure the similarity between two sequences.
Edit distance, also known as Levenshtein distance, refers to the minimum number of edits required to transform one string into another, commonly used in information retrieval and natural language processing to measure the similarity between two sequences.
!!! question
Given two strings $s$ and $t$, return the minimum number of edits required to transform $s$ into $t$.
You can perform three types of edits on a string: insert a character, delete a character, or replace a character with any other character.
You can perform three types of edit operations on a string: insert a character, delete a character, or replace a character with any other character.
As shown in Figure 14-27, transforming `kitten` into `sitting` requires 3 edits, including 2 replacements and 1 insertion; transforming `hello` into `algo` requires 3 steps, including 2 replacements and 1 deletion.
![Example data of edit distance](edit_distance_problem.assets/edit_distance_example.png){ class="animation-figure" }
![Example data for edit distance](edit_distance_problem.assets/edit_distance_example.png){ class="animation-figure" }
<p align="center"> Figure 14-27 &nbsp; Example data of edit distance </p>
<p align="center"> Figure 14-27 &nbsp; Example data for edit distance </p>
**The edit distance problem can naturally be explained with a decision tree model**. Strings correspond to tree nodes, and a round of decision (an edit operation) corresponds to an edge of the tree.
**The edit distance problem can be naturally explained using the decision tree model**. Strings correspond to tree nodes, and a round of decision (one edit operation) corresponds to an edge of the tree.
As shown in Figure 14-28, with unrestricted operations, each node can derive many edges, each corresponding to one operation, meaning there are many possible paths to transform `hello` into `algo`.
As shown in Figure 14-28, without restricting operations, each node can branch into many edges, with each edge corresponding to one operation, meaning there are many possible paths to transform `hello` into `algo`.
From the perspective of the decision tree, the goal of this problem is to find the shortest path between the node `hello` and the node `algo`.
From the perspective of the decision tree, the goal of this problem is to find the shortest path between node `hello` and node `algo`.
![Edit distance problem represented based on decision tree model](edit_distance_problem.assets/edit_distance_decision_tree.png){ class="animation-figure" }
![Representing edit distance problem based on decision tree model](edit_distance_problem.assets/edit_distance_decision_tree.png){ class="animation-figure" }
<p align="center"> Figure 14-28 &nbsp; Edit distance problem represented based on decision tree model </p>
<p align="center"> Figure 14-28 &nbsp; Representing edit distance problem based on decision tree model </p>
### 1. &nbsp; Dynamic programming approach
### 1. &nbsp; Dynamic Programming Approach
**Step one: Think about each round of decision, define the state, thus obtaining the $dp$ table**
**Step 1: Think about the decisions in each round, define the state, and thus obtain the $dp$ table**
Each round of decision involves performing one edit operation on string $s$.
We aim to gradually reduce the problem size during the edit process, which enables us to construct subproblems. Let the lengths of strings $s$ and $t$ be $n$ and $m$, respectively. We first consider the tail characters of both strings $s[n-1]$ and $t[m-1]$.
We want the problem scale to gradually decrease during the editing process, which allows us to construct subproblems. Let the lengths of strings $s$ and $t$ be $n$ and $m$ respectively. We first consider the tail characters of the two strings, $s[n-1]$ and $t[m-1]$.
- If $s[n-1]$ and $t[m-1]$ are the same, we can skip them and directly consider $s[n-2]$ and $t[m-2]$.
- If $s[n-1]$ and $t[m-1]$ are different, we need to perform one edit on $s$ (insert, delete, replace) so that the tail characters of the two strings match, allowing us to skip them and consider a smaller-scale problem.
- If $s[n-1]$ and $t[m-1]$ are different, we need to perform one edit on $s$ (insert, delete, or replace) to make the tail characters of the two strings the same, allowing us to skip them and consider a smaller-scale problem.
Thus, each round of decision (edit operation) in string $s$ changes the remaining characters in $s$ and $t$ to be matched. Therefore, the state is the $i$-th and $j$-th characters currently considered in $s$ and $t$, denoted as $[i, j]$.
In other words, each round of decision (edit operation) we make on string $s$ will change the remaining characters to be matched in $s$ and $t$. Therefore, the state is the $i$-th and $j$-th characters currently being considered in $s$ and $t$, denoted as $[i, j]$.
State $[i, j]$ corresponds to the subproblem: **The minimum number of edits required to change the first $i$ characters of $s$ into the first $j$ characters of $t$**.
State $[i, j]$ corresponds to the subproblem: **the minimum number of edits required to change the first $i$ characters of $s$ into the first $j$ characters of $t$**.
From this, we obtain a two-dimensional $dp$ table of size $(i+1) \times (j+1)$.
**Step two: Identify the optimal substructure and then derive the state transition equation**
**Step 2: Identify the optimal substructure, and then derive the state transition equation**
Consider the subproblem $dp[i, j]$, whose corresponding tail characters of the two strings are $s[i-1]$ and $t[j-1]$, which can be divided into three scenarios as shown in Figure 14-29.
Consider subproblem $dp[i, j]$, where the tail characters of the corresponding two strings are $s[i-1]$ and $t[j-1]$, which can be divided into the three cases shown in Figure 14-29 based on different edit operations.
1. Add $t[j-1]$ after $s[i-1]$, then the remaining subproblem is $dp[i, j-1]$.
1. Insert $t[j-1]$ after $s[i-1]$, then the remaining subproblem is $dp[i, j-1]$.
2. Delete $s[i-1]$, then the remaining subproblem is $dp[i-1, j]$.
3. Replace $s[i-1]$ with $t[j-1]$, then the remaining subproblem is $dp[i-1, j-1]$.
![State transition of edit distance](edit_distance_problem.assets/edit_distance_state_transfer.png){ class="animation-figure" }
![State transition for edit distance](edit_distance_problem.assets/edit_distance_state_transfer.png){ class="animation-figure" }
<p align="center"> Figure 14-29 &nbsp; State transition of edit distance </p>
<p align="center"> Figure 14-29 &nbsp; State transition for edit distance </p>
Based on the analysis above, we can determine the optimal substructure: The minimum number of edits for $dp[i, j]$ is the minimum among $dp[i, j-1]$, $dp[i-1, j]$, and $dp[i-1, j-1]$, plus the edit step $1$. The corresponding state transition equation is:
Based on the above analysis, the optimal substructure can be obtained: the minimum number of edits for $dp[i, j]$ equals the minimum among the minimum edit steps of $dp[i, j-1]$, $dp[i-1, j]$, and $dp[i-1, j-1]$, plus the edit step $1$ for this time. The corresponding state transition equation is:
$$
dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1
$$
Please note, **when $s[i-1]$ and $t[j-1]$ are the same, no edit is required for the current character**, in which case the state transition equation is:
Please note that **when $s[i-1]$ and $t[j-1]$ are the same, no edit is required for the current character**, in which case the state transition equation is:
$$
dp[i, j] = dp[i-1, j-1]
$$
**Step three: Determine the boundary conditions and the order of state transitions**
**Step 3: Determine boundary conditions and state transition order**
When both strings are empty, the number of edits is $0$, i.e., $dp[0, 0] = 0$. When $s$ is empty but $t$ is not, the minimum number of edits equals the length of $t$, that is, the first row $dp[0, j] = j$. When $s$ is not empty but $t$ is, the minimum number of edits equals the length of $s$, that is, the first column $dp[i, 0] = i$.
When both strings are empty, the number of edit steps is $0$, i.e., $dp[0, 0] = 0$. When $s$ is empty but $t$ is not, the minimum number of edit steps equals the length of $t$, i.e., the first row $dp[0, j] = j$. When $s$ is not empty but $t$ is empty, the minimum number of edit steps equals the length of $s$, i.e., the first column $dp[i, 0] = i$.
Observing the state transition equation, solving $dp[i, j]$ depends on the solutions to the left, above, and upper left, so a double loop can be used to traverse the entire $dp$ table in the correct order.
Observing the state transition equation, the solution $dp[i, j]$ depends on solutions to the left, above, and upper-left, so the entire $dp$ table can be traversed in order through two nested loops.
### 2. &nbsp; Code implementation
### 2. &nbsp; Code Implementation
=== "Python"
@@ -89,14 +89,14 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut
dp[i][0] = i
for j in range(1, m + 1):
dp[0][j] = j
# State transition: the rest of the rows and columns
# State transition: rest of the rows and columns
for i in range(1, n + 1):
for j in range(1, m + 1):
if s[i - 1] == t[j - 1]:
# If the two characters are equal, skip these two characters
# If two characters are equal, skip both characters
dp[i][j] = dp[i - 1][j - 1]
else:
# The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
# Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1
return dp[n][m]
```
@@ -115,14 +115,14 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// State transition: the rest of the rows and columns
// State transition: rest of the rows and columns
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
// If the two characters are equal, skip these two characters
// If two characters are equal, skip both characters
dp[i][j] = dp[i - 1][j - 1];
} else {
// The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
@@ -145,14 +145,14 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// State transition: the rest of the rows and columns
// State transition: rest of the rows and columns
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s.charAt(i - 1) == t.charAt(j - 1)) {
// If the two characters are equal, skip these two characters
// If two characters are equal, skip both characters
dp[i][j] = dp[i - 1][j - 1];
} else {
// The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
@@ -164,73 +164,323 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut
=== "C#"
```csharp title="edit_distance.cs"
[class]{edit_distance}-[func]{EditDistanceDP}
/* Edit distance: Dynamic programming */
int EditDistanceDP(string s, string t) {
int n = s.Length, m = t.Length;
int[,] dp = new int[n + 1, m + 1];
// State transition: first row and first column
for (int i = 1; i <= n; i++) {
dp[i, 0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0, j] = j;
}
// State transition: rest of the rows and columns
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
// If two characters are equal, skip both characters
dp[i, j] = dp[i - 1, j - 1];
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;
}
}
}
return dp[n, m];
}
```
=== "Go"
```go title="edit_distance.go"
[class]{}-[func]{editDistanceDP}
/* Edit distance: Dynamic programming */
func editDistanceDP(s string, t string) int {
n := len(s)
m := len(t)
dp := make([][]int, n+1)
for i := 0; i <= n; i++ {
dp[i] = make([]int, m+1)
}
// State transition: first row and first column
for i := 1; i <= n; i++ {
dp[i][0] = i
}
for j := 1; j <= m; j++ {
dp[0][j] = j
}
// State transition: rest of the rows and columns
for i := 1; i <= n; i++ {
for j := 1; j <= m; j++ {
if s[i-1] == t[j-1] {
// If two characters are equal, skip both characters
dp[i][j] = dp[i-1][j-1]
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1
}
}
}
return dp[n][m]
}
```
=== "Swift"
```swift title="edit_distance.swift"
[class]{}-[func]{editDistanceDP}
/* Edit distance: Dynamic programming */
func editDistanceDP(s: String, t: String) -> Int {
let n = s.utf8CString.count
let m = t.utf8CString.count
var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)
// State transition: first row and first column
for i in 1 ... n {
dp[i][0] = i
}
for j in 1 ... m {
dp[0][j] = j
}
// State transition: rest of the rows and columns
for i in 1 ... n {
for j in 1 ... m {
if s.utf8CString[i - 1] == t.utf8CString[j - 1] {
// If two characters are equal, skip both characters
dp[i][j] = dp[i - 1][j - 1]
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1
}
}
}
return dp[n][m]
}
```
=== "JS"
```javascript title="edit_distance.js"
[class]{}-[func]{editDistanceDP}
/* Edit distance: Dynamic programming */
function editDistanceDP(s, t) {
const n = s.length,
m = t.length;
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
// State transition: first row and first column
for (let i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (let j = 1; j <= m; j++) {
dp[0][j] = j;
}
// State transition: rest of the rows and columns
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= m; j++) {
if (s.charAt(i - 1) === t.charAt(j - 1)) {
// If two characters are equal, skip both characters
dp[i][j] = dp[i - 1][j - 1];
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] =
Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "TS"
```typescript title="edit_distance.ts"
[class]{}-[func]{editDistanceDP}
/* Edit distance: Dynamic programming */
function editDistanceDP(s: string, t: string): number {
const n = s.length,
m = t.length;
const dp = Array.from({ length: n + 1 }, () =>
Array.from({ length: m + 1 }, () => 0)
);
// State transition: first row and first column
for (let i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (let j = 1; j <= m; j++) {
dp[0][j] = j;
}
// State transition: rest of the rows and columns
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= m; j++) {
if (s.charAt(i - 1) === t.charAt(j - 1)) {
// If two characters are equal, skip both characters
dp[i][j] = dp[i - 1][j - 1];
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] =
Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "Dart"
```dart title="edit_distance.dart"
[class]{}-[func]{editDistanceDP}
/* Edit distance: Dynamic programming */
int editDistanceDP(String s, String t) {
int n = s.length, m = t.length;
List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));
// State transition: first row and first column
for (int i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// State transition: rest of the rows and columns
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
// If two characters are equal, skip both characters
dp[i][j] = dp[i - 1][j - 1];
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "Rust"
```rust title="edit_distance.rs"
[class]{}-[func]{edit_distance_dp}
/* Edit distance: Dynamic programming */
fn edit_distance_dp(s: &str, t: &str) -> i32 {
let (n, m) = (s.len(), t.len());
let mut dp = vec![vec![0; m + 1]; n + 1];
// State transition: first row and first column
for i in 1..=n {
dp[i][0] = i as i32;
}
for j in 1..m {
dp[0][j] = j as i32;
}
// State transition: rest of the rows and columns
for i in 1..=n {
for j in 1..=m {
if s.chars().nth(i - 1) == t.chars().nth(j - 1) {
// If two characters are equal, skip both characters
dp[i][j] = dp[i - 1][j - 1];
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] =
std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
dp[n][m]
}
```
=== "C"
```c title="edit_distance.c"
[class]{}-[func]{editDistanceDP}
/* Edit distance: Dynamic programming */
int editDistanceDP(char *s, char *t, int n, int m) {
int **dp = malloc((n + 1) * sizeof(int *));
for (int i = 0; i <= n; i++) {
dp[i] = calloc(m + 1, sizeof(int));
}
// State transition: first row and first column
for (int i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// State transition: rest of the rows and columns
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
// If two characters are equal, skip both characters
dp[i][j] = dp[i - 1][j - 1];
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
int res = dp[n][m];
// Free memory
for (int i = 0; i <= n; i++) {
free(dp[i]);
}
return res;
}
```
=== "Kotlin"
```kotlin title="edit_distance.kt"
[class]{}-[func]{editDistanceDP}
/* Edit distance: Dynamic programming */
fun editDistanceDP(s: String, t: String): Int {
val n = s.length
val m = t.length
val dp = Array(n + 1) { IntArray(m + 1) }
// State transition: first row and first column
for (i in 1..n) {
dp[i][0] = i
}
for (j in 1..m) {
dp[0][j] = j
}
// State transition: rest of the rows and columns
for (i in 1..n) {
for (j in 1..m) {
if (s[i - 1] == t[j - 1]) {
// If two characters are equal, skip both characters
dp[i][j] = dp[i - 1][j - 1]
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1
}
}
}
return dp[n][m]
}
```
=== "Ruby"
```ruby title="edit_distance.rb"
[class]{}-[func]{edit_distance_dp}
### Edit distance: dynamic programming ###
def edit_distance_dp(s, t)
n, m = s.length, t.length
dp = Array.new(n + 1) { Array.new(m + 1, 0) }
# State transition: first row and first column
(1...(n + 1)).each { |i| dp[i][0] = i }
(1...(m + 1)).each { |j| dp[0][j] = j }
# State transition: rest of the rows and columns
for i in 1...(n + 1)
for j in 1...(m +1)
if s[i - 1] == t[j - 1]
# If two characters are equal, skip both characters
dp[i][j] = dp[i - 1][j - 1]
else
# Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1
end
end
end
dp[n][m]
end
```
=== "Zig"
```zig title="edit_distance.zig"
[class]{}-[func]{editDistanceDP}
```
As shown in Figure 14-30, the process of state transition in the edit distance problem is very similar to that in the knapsack problem, which can be seen as filling a two-dimensional grid.
As shown in Figure 14-30, the state transition process for the edit distance problem is very similar to the knapsack problem and can both be viewed as the process of filling a two-dimensional grid.
=== "<1>"
![Dynamic programming process of edit distance](edit_distance_problem.assets/edit_distance_dp_step1.png){ class="animation-figure" }
![Dynamic programming process for edit distance](edit_distance_problem.assets/edit_distance_dp_step1.png){ class="animation-figure" }
=== "<2>"
![edit_distance_dp_step2](edit_distance_problem.assets/edit_distance_dp_step2.png){ class="animation-figure" }
@@ -274,13 +524,13 @@ As shown in Figure 14-30, the process of state transition in the edit distance p
=== "<15>"
![edit_distance_dp_step15](edit_distance_problem.assets/edit_distance_dp_step15.png){ class="animation-figure" }
<p align="center"> Figure 14-30 &nbsp; Dynamic programming process of edit distance </p>
<p align="center"> Figure 14-30 &nbsp; Dynamic programming process for edit distance </p>
### 3. &nbsp; Space optimization
### 3. &nbsp; Space Optimization
Since $dp[i, j]$ is derived from the solutions above $dp[i-1, j]$, to the left $dp[i, j-1]$, and to the upper left $dp[i-1, j-1]$, and direct traversal will lose the upper left solution $dp[i-1, j-1]$, and reverse traversal cannot build $dp[i, j-1]$ in advance, therefore, both traversal orders are not feasible.
Since $dp[i, j]$ is transferred from the solutions above $dp[i-1, j]$, to the left $dp[i, j-1]$, and to the upper-left $dp[i-1, j-1]$, forward traversal will lose the upper-left solution $dp[i-1, j-1]$, and reverse traversal cannot build $dp[i, j-1]$ in advance, so neither traversal order is feasible.
For this reason, we can use a variable `leftup` to temporarily store the solution from the upper left $dp[i-1, j-1]$, thus only needing to consider the solutions to the left and above. This situation is similar to the unbounded knapsack problem, allowing for direct traversal. The code is as follows:
For this reason, we can use a variable `leftup` to temporarily store the upper-left solution $dp[i-1, j-1]$, so we only need to consider the solutions to the left and above. This situation is the same as the unbounded knapsack problem, allowing for forward traversal. The code is as follows:
=== "Python"
@@ -292,21 +542,21 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio
# State transition: first row
for j in range(1, m + 1):
dp[j] = j
# State transition: the rest of the rows
# State transition: rest of the rows
for i in range(1, n + 1):
# State transition: first column
leftup = dp[0] # Temporarily store dp[i-1, j-1]
dp[0] += 1
# State transition: the rest of the columns
# State transition: rest of the columns
for j in range(1, m + 1):
temp = dp[j]
if s[i - 1] == t[j - 1]:
# If the two characters are equal, skip these two characters
# If two characters are equal, skip both characters
dp[j] = leftup
else:
# The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
# Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = min(dp[j - 1], dp[j], leftup) + 1
leftup = temp # Update for the next round of dp[i-1, j-1]
leftup = temp # Update for next round's dp[i-1, j-1]
return dp[m]
```
@@ -321,22 +571,22 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// State transition: the rest of the rows
// State transition: rest of the rows
for (int i = 1; i <= n; i++) {
// State transition: first column
int leftup = dp[0]; // Temporarily store dp[i-1, j-1]
dp[0] = i;
// State transition: the rest of the columns
// State transition: rest of the columns
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s[i - 1] == t[j - 1]) {
// If the two characters are equal, skip these two characters
// If two characters are equal, skip both characters
dp[j] = leftup;
} else {
// The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // Update for the next round of dp[i-1, j-1]
leftup = temp; // Update for next round's dp[i-1, j-1]
}
}
return dp[m];
@@ -354,22 +604,22 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// State transition: the rest of the rows
// State transition: rest of the rows
for (int i = 1; i <= n; i++) {
// State transition: first column
int leftup = dp[0]; // Temporarily store dp[i-1, j-1]
dp[0] = i;
// State transition: the rest of the columns
// State transition: rest of the columns
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s.charAt(i - 1) == t.charAt(j - 1)) {
// If the two characters are equal, skip these two characters
// If two characters are equal, skip both characters
dp[j] = leftup;
} else {
// The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // Update for the next round of dp[i-1, j-1]
leftup = temp; // Update for next round's dp[i-1, j-1]
}
}
return dp[m];
@@ -379,65 +629,334 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio
=== "C#"
```csharp title="edit_distance.cs"
[class]{edit_distance}-[func]{EditDistanceDPComp}
/* Edit distance: Space-optimized dynamic programming */
int EditDistanceDPComp(string s, string t) {
int n = s.Length, m = t.Length;
int[] dp = new int[m + 1];
// State transition: first row
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// State transition: rest of the rows
for (int i = 1; i <= n; i++) {
// State transition: first column
int leftup = dp[0]; // Temporarily store dp[i-1, j-1]
dp[0] = i;
// State transition: rest of the columns
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s[i - 1] == t[j - 1]) {
// If two characters are equal, skip both characters
dp[j] = leftup;
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // Update for next round's dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "Go"
```go title="edit_distance.go"
[class]{}-[func]{editDistanceDPComp}
/* Edit distance: Space-optimized dynamic programming */
func editDistanceDPComp(s string, t string) int {
n := len(s)
m := len(t)
dp := make([]int, m+1)
// State transition: first row
for j := 1; j <= m; j++ {
dp[j] = j
}
// State transition: rest of the rows
for i := 1; i <= n; i++ {
// State transition: first column
leftUp := dp[0] // Temporarily store dp[i-1, j-1]
dp[0] = i
// State transition: rest of the columns
for j := 1; j <= m; j++ {
temp := dp[j]
if s[i-1] == t[j-1] {
// If two characters are equal, skip both characters
dp[j] = leftUp
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1
}
leftUp = temp // Update for next round's dp[i-1, j-1]
}
}
return dp[m]
}
```
=== "Swift"
```swift title="edit_distance.swift"
[class]{}-[func]{editDistanceDPComp}
/* Edit distance: Space-optimized dynamic programming */
func editDistanceDPComp(s: String, t: String) -> Int {
let n = s.utf8CString.count
let m = t.utf8CString.count
var dp = Array(repeating: 0, count: m + 1)
// State transition: first row
for j in 1 ... m {
dp[j] = j
}
// State transition: rest of the rows
for i in 1 ... n {
// State transition: first column
var leftup = dp[0] // Temporarily store dp[i-1, j-1]
dp[0] = i
// State transition: rest of the columns
for j in 1 ... m {
let temp = dp[j]
if s.utf8CString[i - 1] == t.utf8CString[j - 1] {
// If two characters are equal, skip both characters
dp[j] = leftup
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1
}
leftup = temp // Update for next round's dp[i-1, j-1]
}
}
return dp[m]
}
```
=== "JS"
```javascript title="edit_distance.js"
[class]{}-[func]{editDistanceDPComp}
/* Edit distance: Space-optimized dynamic programming */
function editDistanceDPComp(s, t) {
const n = s.length,
m = t.length;
const dp = new Array(m + 1).fill(0);
// State transition: first row
for (let j = 1; j <= m; j++) {
dp[j] = j;
}
// State transition: rest of the rows
for (let i = 1; i <= n; i++) {
// State transition: first column
let leftup = dp[0]; // Temporarily store dp[i-1, j-1]
dp[0] = i;
// State transition: rest of the columns
for (let j = 1; j <= m; j++) {
const temp = dp[j];
if (s.charAt(i - 1) === t.charAt(j - 1)) {
// If two characters are equal, skip both characters
dp[j] = leftup;
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;
}
leftup = temp; // Update for next round's dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "TS"
```typescript title="edit_distance.ts"
[class]{}-[func]{editDistanceDPComp}
/* Edit distance: Space-optimized dynamic programming */
function editDistanceDPComp(s: string, t: string): number {
const n = s.length,
m = t.length;
const dp = new Array(m + 1).fill(0);
// State transition: first row
for (let j = 1; j <= m; j++) {
dp[j] = j;
}
// State transition: rest of the rows
for (let i = 1; i <= n; i++) {
// State transition: first column
let leftup = dp[0]; // Temporarily store dp[i-1, j-1]
dp[0] = i;
// State transition: rest of the columns
for (let j = 1; j <= m; j++) {
const temp = dp[j];
if (s.charAt(i - 1) === t.charAt(j - 1)) {
// If two characters are equal, skip both characters
dp[j] = leftup;
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;
}
leftup = temp; // Update for next round's dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "Dart"
```dart title="edit_distance.dart"
[class]{}-[func]{editDistanceDPComp}
/* Edit distance: Space-optimized dynamic programming */
int editDistanceDPComp(String s, String t) {
int n = s.length, m = t.length;
List<int> dp = List.filled(m + 1, 0);
// State transition: first row
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// State transition: rest of the rows
for (int i = 1; i <= n; i++) {
// State transition: first column
int leftup = dp[0]; // Temporarily store dp[i-1, j-1]
dp[0] = i;
// State transition: rest of the columns
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s[i - 1] == t[j - 1]) {
// If two characters are equal, skip both characters
dp[j] = leftup;
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // Update for next round's dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "Rust"
```rust title="edit_distance.rs"
[class]{}-[func]{edit_distance_dp_comp}
/* Edit distance: Space-optimized dynamic programming */
fn edit_distance_dp_comp(s: &str, t: &str) -> i32 {
let (n, m) = (s.len(), t.len());
let mut dp = vec![0; m + 1];
// State transition: first row
for j in 1..m {
dp[j] = j as i32;
}
// State transition: rest of the rows
for i in 1..=n {
// State transition: first column
let mut leftup = dp[0]; // Temporarily store dp[i-1, j-1]
dp[0] = i as i32;
// State transition: rest of the columns
for j in 1..=m {
let temp = dp[j];
if s.chars().nth(i - 1) == t.chars().nth(j - 1) {
// If two characters are equal, skip both characters
dp[j] = leftup;
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // Update for next round's dp[i-1, j-1]
}
}
dp[m]
}
```
=== "C"
```c title="edit_distance.c"
[class]{}-[func]{editDistanceDPComp}
/* Edit distance: Space-optimized dynamic programming */
int editDistanceDPComp(char *s, char *t, int n, int m) {
int *dp = calloc(m + 1, sizeof(int));
// State transition: first row
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// State transition: rest of the rows
for (int i = 1; i <= n; i++) {
// State transition: first column
int leftup = dp[0]; // Temporarily store dp[i-1, j-1]
dp[0] = i;
// State transition: rest of the columns
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s[i - 1] == t[j - 1]) {
// If two characters are equal, skip both characters
dp[j] = leftup;
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // Update for next round's dp[i-1, j-1]
}
}
int res = dp[m];
// Free memory
free(dp);
return res;
}
```
=== "Kotlin"
```kotlin title="edit_distance.kt"
[class]{}-[func]{editDistanceDPComp}
/* Edit distance: Space-optimized dynamic programming */
fun editDistanceDPComp(s: String, t: String): Int {
val n = s.length
val m = t.length
val dp = IntArray(m + 1)
// State transition: first row
for (j in 1..m) {
dp[j] = j
}
// State transition: rest of the rows
for (i in 1..n) {
// State transition: first column
var leftup = dp[0] // Temporarily store dp[i-1, j-1]
dp[0] = i
// State transition: rest of the columns
for (j in 1..m) {
val temp = dp[j]
if (s[i - 1] == t[j - 1]) {
// If two characters are equal, skip both characters
dp[j] = leftup
} else {
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1
}
leftup = temp // Update for next round's dp[i-1, j-1]
}
}
return dp[m]
}
```
=== "Ruby"
```ruby title="edit_distance.rb"
[class]{}-[func]{edit_distance_dp_comp}
```
=== "Zig"
```zig title="edit_distance.zig"
[class]{}-[func]{editDistanceDPComp}
### Edit distance: space-optimized DP ###
def edit_distance_dp_comp(s, t)
n, m = s.length, t.length
dp = Array.new(m + 1, 0)
# State transition: first row
(1...(m + 1)).each { |j| dp[j] = j }
# State transition: rest of the rows
for i in 1...(n + 1)
# State transition: first column
leftup = dp.first # Temporarily store dp[i-1, j-1]
dp[0] += 1
# State transition: rest of the columns
for j in 1...(m + 1)
temp = dp[j]
if s[i - 1] == t[j - 1]
# If two characters are equal, skip both characters
dp[j] = leftup
else
# Minimum edit steps = minimum edit steps of insert, delete, replace + 1
dp[j] = [dp[j - 1], dp[j], leftup].min + 1
end
leftup = temp # Update for next round's dp[i-1, j-1]
end
end
dp[m]
end
```

View File

@@ -3,22 +3,22 @@ comments: true
icon: material/table-pivot
---
# Chapter 14. &nbsp; Dynamic programming
# Chapter 14. &nbsp; Dynamic Programming
![Dynamic programming](../assets/covers/chapter_dynamic_programming.jpg){ class="cover-image" }
!!! abstract
Streams merge into rivers, and rivers merge into the sea.
Dynamic programming weaves smaller problems solutions into larger ones, guiding us step by step toward the far shore—where the ultimate answer awaits.
Streams converge into rivers, rivers converge into the sea.
Dynamic programming gathers solutions to small problems into answers to large problems, step by step guiding us to the shore of problem-solving.
## Chapter contents
- [14.1 &nbsp; Introduction to dynamic programming](intro_to_dynamic_programming.md)
- [14.2 &nbsp; Characteristics of DP problems](dp_problem_features.md)
- [14.3 &nbsp; DP problem-solving approach](dp_solution_pipeline.md)
- [14.4 &nbsp; 0-1 Knapsack problem](knapsack_problem.md)
- [14.5 &nbsp; Unbounded knapsack problem](unbounded_knapsack_problem.md)
- [14.6 &nbsp; Edit distance problem](edit_distance_problem.md)
- [14.1 &nbsp; Introduction to Dynamic Programming](intro_to_dynamic_programming.md)
- [14.2 &nbsp; Characteristics of Dynamic Programming Problems](dp_problem_features.md)
- [14.3 &nbsp; Dynamic Programming Problem-Solving Approach](dp_solution_pipeline.md)
- [14.4 &nbsp; 0-1 Knapsack Problem](knapsack_problem.md)
- [14.5 &nbsp; Unbounded Knapsack Problem](unbounded_knapsack_problem.md)
- [14.6 &nbsp; Edit Distance Problem](edit_distance_problem.md)
- [14.7 &nbsp; Summary](summary.md)

File diff suppressed because it is too large Load Diff

View File

@@ -4,24 +4,26 @@ comments: true
# 14.7 &nbsp; Summary
- Dynamic programming decomposes problems and improves computational efficiency by avoiding redundant computations through storing solutions of subproblems.
- Without considering time, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree has many overlapping subproblems, resulting in very low efficiency. By introducing a memorization list, it's possible to store solutions of all computed subproblems, ensuring that overlapping subproblems are only computed once.
- Memorization search is a top-down recursive solution, whereas dynamic programming corresponds to a bottom-up iterative approach, akin to "filling out a table." Since the current state only depends on certain local states, we can eliminate one dimension of the dp table to reduce space complexity.
- Decomposition of subproblems is a universal algorithmic approach, differing in characteristics among divide and conquer, dynamic programming, and backtracking.
- Dynamic programming problems have three main characteristics: overlapping subproblems, optimal substructure, and no aftereffects.
- If the optimal solution of the original problem can be constructed from the optimal solutions of its subproblems, it has an optimal substructure.
- No aftereffects mean that the future development of a state depends only on the current state and not on all past states experienced. Many combinatorial optimization problems do not have this property and cannot be quickly solved using dynamic programming.
### 1. &nbsp; Key Review
- Dynamic programming decomposes problems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving computational efficiency.
- Without considering time constraints, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree contains a large number of overlapping subproblems, resulting in extremely low efficiency. By introducing a memo list, we can store the solutions to all computed subproblems, ensuring that overlapping subproblems are only computed once.
- Memoization is a top-down recursive solution, while the corresponding dynamic programming is a bottom-up iterative solution, similar to "filling in a table". Since the current state only depends on certain local states, we can eliminate one dimension of the $dp$ table to reduce space complexity.
- Subproblem decomposition is a general algorithmic approach, with different properties in divide and conquer, dynamic programming, and backtracking.
- Dynamic programming problems have three major characteristics: overlapping subproblems, optimal substructure, and no aftereffects.
- If the optimal solution to the original problem can be constructed from the optimal solutions to the subproblems, then it has optimal substructure.
- No aftereffects means that for a given state, its future development is only related to that state and has nothing to do with all past states. Many combinatorial optimization problems do not have no aftereffects and cannot be quickly solved using dynamic programming.
**Knapsack problem**
- The knapsack problem is one of the most typical dynamic programming problems, with variants including the 0-1 knapsack, unbounded knapsack, and multiple knapsacks.
- The state definition of the 0-1 knapsack is the maximum value in a knapsack of capacity $c$ with the first $i$ items. Based on decisions not to include or to include an item in the knapsack, optimal substructures can be identified and state transition equations constructed. In space optimization, since each state depends on the state directly above and to the upper left, the list should be traversed in reverse order to avoid overwriting the upper left state.
- In the unbounded knapsack problem, there is no limit on the number of each kind of item that can be chosen, thus the state transition for including items differs from the 0-1 knapsack. Since the state depends on the state directly above and to the left, space optimization should involve forward traversal.
- The coin change problem is a variant of the unbounded knapsack problem, shifting from seeking the maximum value to seeking the minimum number of coins, thus the state transition equation should change $\max()$ to $\min()$. From pursuing not exceeding the capacity of the knapsack to seeking exactly the target amount, thus use $amt + 1$ to represent the invalid solution of unable to make up the target amount.
- Coin Change Problem II shifts from seeking the minimum number of coins to seeking the number of coin combinations,” changing the state transition equation accordingly from $\min()$ to summation operator.
- The knapsack problem is one of the most typical dynamic programming problems, with variants such as the 0-1 knapsack, unbounded knapsack, and multiple knapsack.
- The state definition for the 0-1 knapsack is the maximum value among the first $i$ items in a knapsack of capacity $c$. Based on the two decisions of not putting an item in the knapsack and putting it in, the optimal substructure can be identified and the state transition equation constructed. In space optimization, since each state depends on the state directly above and to the upper-left, the list needs to be traversed in reverse order to avoid overwriting the upper-left state.
- The unbounded knapsack problem has no limit on the selection quantity of each type of item, so the state transition for choosing to put in an item differs from the 0-1 knapsack problem. Since the state depends on the state directly above and directly to the left, space optimization should use forward traversal.
- The coin change problem is a variant of the unbounded knapsack problem. It changes from seeking the "maximum" value to seeking the "minimum" number of coins, so $\max()$ in the state transition equation should be changed to $\min()$. It changes from seeking "not exceeding" the knapsack capacity to seeking "exactly" making up the target amount, so $amt + 1$ is used to represent the invalid solution of "unable to make up the target amount".
- Coin change problem II changes from seeking the "minimum number of coins" to seeking the "number of coin combinations", so the state transition equation correspondingly changes from $\min()$ to a summation operator.
**Edit distance problem**
- Edit distance (Levenshtein distance) measures the similarity between two strings, defined as the minimum number of editing steps needed to change one string into another, with editing operations including adding, deleting, or replacing.
- The state definition for the edit distance problem is the minimum number of editing steps needed to change the first $i$ characters of $s$ into the first $j$ characters of $t$. When $s[i] \ne t[j]$, there are three decisions: add, delete, replace, each with their corresponding residual subproblems. From this, optimal substructures can be identified, and state transition equations built. When $s[i] = t[j]$, no editing of the current character is necessary.
- In edit distance, the state depends on the state directly above, to the left, and to the upper left. Therefore, after space optimization, neither forward nor reverse traversal can correctly perform state transitions. To address this, we use a variable to temporarily store the upper left state, making it equivalent to the situation in the unbounded knapsack problem, allowing for forward traversal after space optimization.
- Edit distance (Levenshtein distance) is used to measure the similarity between two strings, defined as the minimum number of edit steps from one string to another, with edit operations including insert, delete, and replace.
- The state definition for the edit distance problem is the minimum number of edit steps required to change the first $i$ characters of $s$ into the first $j$ characters of $t$. When $s[i] \ne t[j]$, there are three decisions: insert, delete, replace, each with corresponding remaining subproblems. From this, the optimal substructure can be identified and the state transition equation constructed. When $s[i] = t[j]$, no edit is required for the current character.
- In edit distance, the state depends on the state directly above, directly to the left, and to the upper-left, so after space optimization, neither forward nor reverse traversal can correctly perform state transitions. For this reason, we use a variable to temporarily store the upper-left state, thus transforming to a situation equivalent to the unbounded knapsack problem, allowing for forward traversal after space optimization.