Algorithm and data structure interview guide - detailed explanation of dynamic programming algorithm

Dynamic programming problem characteristics

We learned how dynamic programming solves problems by decomposing them into subproblems. In fact, subproblem decomposition is a general algorithmic idea, with different emphasis in divide and conquer, dynamic programming, and backtracking.

  • The divide-and-conquer algorithm recursively divides the original problem into multiple independent sub-problems until the smallest sub-problem is reached, and merges the solutions of the sub-problems in backtracking to finally obtain the solution to the original problem.
  • Dynamic programming also recursively decomposes the problem, but the main difference from the divide-and-conquer algorithm is that the sub-problems in dynamic programming are interdependent, and many overlapping sub-problems will appear during the decomposition process.
  • The backtracking algorithm exhausts all possible solutions in attempts and regressions, and avoids 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 often used to solve optimization problems, which not only contain overlapping subproblems, but also have two other characteristics: optimal substructure and no aftereffects.

optimal substructure

We slightly modify the stair climbing problem to make it more suitable for demonstrating the concept of optimal substructure.

!!! question "The lowest cost of climbing stairs"

给定一个楼梯,你每步可以上 $1$ 阶或者 $2$ 阶,每一阶楼梯上都贴有一个非负整数,表示你在该台阶所需要付出的代价。给定一个非负整数数组 $cost$ ,其中 $cost[i]$ 表示在第 $i$ 个台阶需要付出的代价,$cost[0]$ 为地面起始点。请计算最少需要付出多少代价才能到达顶部?

As shown in the figure below, if 1 11 2 2 2 3 3 The costs of level 3 are respectively1 11 10 10 10 1 1 1 , then climb from the ground to the 3rd3The minimum cost of order 3 is2 22

Insert image description here

d p [ i ] dp[i] d p ​​[ i ] means climbing to theiithThe cumulative price paid at level i , due to the cost at level iiOrder i is only possible fromi − 1 i - 1i1st order ori − 2 i − 2i2nd order comes, sodp[i]dp[i]d p ​​[ i ] can only be equal todp [ i − 1 ] + cost [ i ] dp [i - 1] + cost [i]dp[i1]+cost[i] d p [ i − 2 ] + c o s t [ i ] dp[i - 2] + cost[i] dp[i2]+cos t [ i ] . To minimize the cost, we should choose the smaller of the two:

d p [ i ] = min ⁡ ( d p [ i − 1 ] , d p [ i − 2 ] ) + c o s t [ i ] dp[i] = \min(dp[i-1], dp[i-2]) + cost[i] dp[i]=min(dp[i1],dp[i2])+cost[i]

This can lead to the meaning of optimal substructure: the optimal solution to the original problem is constructed from the optimal solutions to the sub-problems .

This problem obviously has an optimal substructure: we optimally solve the two subproblems dp [ i − 1 ] dp[i-1]dp[i1] d p [ i − 2 ] dp[i-2] dp[i2 ] , and use it to construct the original problemdp[i]dp[i]The optimal solution of d p [ i ] .

So, does the stair climbing problem in the previous section have an optimal substructure? Its goal is to solve the number of solutions. It seems to be a counting problem, but if you ask it another way: "Solve the maximum number of solutions." We unexpectedly found that although the question was equivalent before and after modification, the optimal substructure emerged : No. nnThe maximum number of solutions of order n is equal to n − 1 n-1n1st order and nth− 2 n-2nThe sum of the maximum number of solutions of order 2 . Therefore, the explanation method of optimal substructure is relatively flexible and will have different meanings in different problems.

According to the state transition equation, and the initial state dp [1] = cost [1] dp[1] = cost[1]dp[1]=cost[1] d p [ 2 ] = c o s t [ 2 ] dp[2] = cost[2] dp[2]=cos t [ 2 ] , we can get the dynamic programming code.

=== “Python”

```python title="min_cost_climbing_stairs_dp.py"
[class]{}-[func]{min_cost_climbing_stairs_dp}
```

=== “C++”

```cpp title="min_cost_climbing_stairs_dp.cpp"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “Java”

```java title="min_cost_climbing_stairs_dp.java"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDP}
```

=== “C#”

```csharp title="min_cost_climbing_stairs_dp.cs"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDP}
```

=== “Go”

```go title="min_cost_climbing_stairs_dp.go"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “Swift”

```swift title="min_cost_climbing_stairs_dp.swift"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “JS”

```javascript title="min_cost_climbing_stairs_dp.js"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “TS”

```typescript title="min_cost_climbing_stairs_dp.ts"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “Dart”

```dart title="min_cost_climbing_stairs_dp.dart"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “Rust”

```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp}
```

=== “C”

```c title="min_cost_climbing_stairs_dp.c"
[class]{}-[func]{minCostClimbingStairsDP}
```

=== “Zig”

```zig title="min_cost_climbing_stairs_dp.zig"
[class]{}-[func]{minCostClimbingStairsDP}
```

The figure below shows the dynamic programming process of the above code.

Insert image description here

This problem can also be space optimized, compressing one dimension to zero dimension, so that the space complexity changes from O ( n ) O (n)O ( n ) DescendanceO ( 1 ) O(1)O(1)

=== “Python”

```python title="min_cost_climbing_stairs_dp.py"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
```

=== “C++”

```cpp title="min_cost_climbing_stairs_dp.cpp"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “Java”

```java title="min_cost_climbing_stairs_dp.java"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDPComp}
```

=== “C#”

```csharp title="min_cost_climbing_stairs_dp.cs"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDPComp}
```

=== “Go”

```go title="min_cost_climbing_stairs_dp.go"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “Swift”

```swift title="min_cost_climbing_stairs_dp.swift"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “JS”

```javascript title="min_cost_climbing_stairs_dp.js"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “TS”

```typescript title="min_cost_climbing_stairs_dp.ts"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “Dart”

```dart title="min_cost_climbing_stairs_dp.dart"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “Rust”

```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
```

=== “C”

```c title="min_cost_climbing_stairs_dp.c"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

=== “Zig”

```zig title="min_cost_climbing_stairs_dp.zig"
[class]{}-[func]{minCostClimbingStairsDPComp}
```

no aftereffects

No aftereffects is one of the important characteristics of dynamic programming that can effectively solve problems. It is defined as: given a certain state, its future development is only related to the current state, and has nothing to do with all the states that the current state has experienced in the past .

Taking the stair climbing problem as an example, given state iii , it will develop the statei + 1 i+1i+1 and statei + 2 i+2i+2 , corresponding to jump1 11 step and jump2 22 steps. In making these two choices, we need not consider stateiiThe state before i , their contribution to state iii 's future has no impact.

However, if we add a constraint to the stair climbing problem, the situation is different.

!!! question “Climbing stairs with restraints”

给定一个共有 $n$ 阶的楼梯,你每步可以上 $1$ 阶或者 $2$ 阶,**但不能连续两轮跳 $1$ 阶**,请问有多少种方案可以爬到楼顶。

For example, in the picture below, climb to the 3rd 3rd placeThere are only 2 2left in level 32 possible solutions, including three consecutive jumps1 1The first- order solution does not satisfy the constraints and is therefore discarded.

Insert image description here

In this problem, if the previous round was jump 1 1If you come up to level 1 , you must jump2 2Level 2 . This means thatthe next step choice cannot be determined independently by the current state (the current number of stairs), but is also related to the previous state (the number of stairs in the previous round).

It is not difficult to find that this problem no longer satisfies no aftereffects. The state transition equation dp [ i ] = dp [ i − 1 ] + dp [ i − 2 ] dp[i] = dp[i-1] + dp[i -2]dp[i]=dp[i1]+dp[i2 ] also fails becausedp [ i − 1 ] dp[i-1]dp[i1 ] represents jumping1 11 level, but it contains many "previous round jumps1 11 order up" scheme, and in order to satisfy the constraints, we cannot changedp [ i − 1 ] dp[i-1]dp[i1 ] Directly included indp[i]dp[i]dp[i] 中。

To do this, we need to extend the state definition: state [i, j] [i, j][i,j ] indicates that it is in theiithLevel i , and jumpedjjj order, wherej ∈ { 1 , 2 } j \in \{1, 2\}j{ 1,2 } . This state definition effectively distinguishes the previous round of jumps1 1Level 1 orlevel 2 2Level 2 , we can use this to decide how to jump next.

  • When jjj equals1 11 , that is, the last round jumped1 1At level 1 , you can only choose to jump 2 2this round.Level 2 .
  • When jjj equals2 22 , that is, the previous round jumped2 2At level 2 , you can choose to jump1 1Level 1 or jump2 2Level 2 .

As shown in the figure below, under this definition, dp [ i , j ] dp[i, j]dp[i,j ] represents the state[i, j] [i, j][i,j ] corresponding to the number of plans. At this time, the state transition equation is:

{ d p [ i , 1 ] = d p [ i − 1 , 2 ] d p [ i , 2 ] = d p [ i − 2 , 1 ] + d p [ i − 2 , 2 ] \begin{cases} dp[i, 1] = dp[i-1, 2] \\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \end{cases} { dp[i,1]=dp[i1,2]dp[i,2]=dp[i2,1]+dp[i2,2]

Insert image description here

Finally, return dp [ n , 1 ] + dp [ n , 2 ] dp[n, 1] + dp[n, 2]dp[n,1]+dp[n,2 ] , the sum of the two represents climbing to thennthThe total number of n -order solutions.

=== “Python”

```python title="climbing_stairs_constraint_dp.py"
[class]{}-[func]{climbing_stairs_constraint_dp}
```

=== “C++”

```cpp title="climbing_stairs_constraint_dp.cpp"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “Java”

```java title="climbing_stairs_constraint_dp.java"
[class]{climbing_stairs_constraint_dp}-[func]{climbingStairsConstraintDP}
```

=== “C#”

```csharp title="climbing_stairs_constraint_dp.cs"
[class]{climbing_stairs_constraint_dp}-[func]{climbingStairsConstraintDP}
```

=== “Go”

```go title="climbing_stairs_constraint_dp.go"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “Swift”

```swift title="climbing_stairs_constraint_dp.swift"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “JS”

```javascript title="climbing_stairs_constraint_dp.js"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “TS”

```typescript title="climbing_stairs_constraint_dp.ts"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “Dart”

```dart title="climbing_stairs_constraint_dp.dart"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “Rust”

```rust title="climbing_stairs_constraint_dp.rs"
[class]{}-[func]{climbing_stairs_constraint_dp}
```

=== “C”

```c title="climbing_stairs_constraint_dp.c"
[class]{}-[func]{climbingStairsConstraintDP}
```

=== “Zig”

```zig title="climbing_stairs_constraint_dp.zig"
[class]{}-[func]{climbingStairsConstraintDP}
```

In the above case, since we only need to consider the previous state, we can still make the problem satisfy no aftereffects again by extending the state definition. However, some problems have very serious "repercussions."

!!! question "Stair climbing and obstacle generation"

给定一个共有 $n$ 阶的楼梯,你每步可以上 $1$ 阶或者 $2$ 阶。**规定当爬到第 $i$ 阶时,系统自动会给第 $2i$ 阶上放上障碍物,之后所有轮都不允许跳到第 $2i$ 阶上**。例如,前两轮分别跳到了第 $2$、$3$ 阶上,则之后就不能跳到第 $4$、$6$ 阶上。请问有多少种方案可以爬到楼顶。

In this problem, the next jump depends on all past states, because each jump creates obstacles on a higher ladder and affects future jumps. For this type of problem, dynamic programming is often difficult to solve.

In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy the aftereffect-free property. For this type of problem, we usually choose to use other methods, such as heuristic search, genetic algorithm, reinforcement learning, etc., to obtain available local optimal solutions in a limited time.

Guess you like

Origin blog.csdn.net/zy_dreamer/article/details/132923057