Dynamic programming (algorithm that exchanges space for time)-examples and detailed usage explanations
This blog is based on the dynamic programming algorithm in Chapter 15 of " Introduction to Algorithms ". It quotes a lot of content and examples in the book, and gives python code reproduction based on the pseudocode in the book , explaining in detail the core logic and implementation process of the algorithm .
Dynamic programming (DP) thinking
The core idea of the dynamic programming (Dynamic Programming) algorithm is: a processing algorithm that divides large problems into overlapping sub-problems to solve, thereby obtaining the optimal solution step by step.
Dynamic programming is similar to the divide-and-conquer method in that it solves the original problem by combining solutions to sub-problems ("programming" here refers to a table method, not writing a computer program). But the divide-and-conquer method divides the problem into disjoint sub-problems, solves the sub-problems recursively, and then combines their solutions to find the solution to the original problem. In contrast, dynamic programming is applied when sub-problems overlap , that is, different sub-problems have common sub-problems (the solution of the sub-problems is performed recursively, dividing it into smaller sub-sub-problems).
In this case, the divide-and-conquer algorithm does a lot of unnecessary work by repeatedly solving common sub-subproblems. The dynamic programming algorithm only solves each sub-subproblem once and saves its solution in a table, thereby eliminating the need to recalculate each time a sub-subproblem is solved, avoiding this unnecessary calculation work.
Examples
Dynamic programming methods are often used to solve optimization problems. Two examples are given below to illustrate: the first is to cut the steel bars into short steel bars so that the total value is the highest; the second is to solve how to use the least scalar multiplication operation Complete a matrix chain multiplication operation
Steel bar cutting problem
Here is the background to the steel bar cutting problem:
For example, the figure below shows all possible cutting solutions for a 4-inch steel bar:
the number above the steel bar is the price corresponding to each section of steel bar. Different cutting solutions correspond to different prices. We found that cutting a steel bar with a length of 4 inches into two steel bars with a length of 2 inches each will produce P 2 P_2P2+ P 2 P_2 P2The income of =5+5=10 is the optimal solution, which corresponds to plan C in the figure above.
Next, we gradually analyze the recursive formula of the optimal revenue of the steel bar cutting problem:
a more general revenue formula, the maximum revenue after cutting a steel bar of length n r_nrnExpressed as follows:
A simpler recursive method: where, pi p_i
in (15.2)piRefers to the price corresponding to the steel bar with length i, which can be obtained directly from the table without cutting.
If solved with the traditional recursive method , once the size of n becomes slightly larger, the running time of the program will become quite long, because this method repeatedly calls itself recursively with the same parameter values, that is, it repeatedly solves the same sub-problem . Time increases exponentially . The figure below shows the calling process of the recursive tree when n=4:
the running time of the dynamic programming method is polynomial order.
There are two equivalent implementation methods of dynamic programming: The code implementation process of the bottom-up method
is given
: In the above pseudocode, the input is two variables, the price list array and the length n; the output is a steel bar of length n the optimal benefit obtained.
Calculate the maximum profit from steel bar cutting when the length is 9:
import numpy as np
def Bottom_Up_Cut_Rod(p, n):
r = []
r.insert(0, 0)
for j in range(1, n + 1):
q = float('-inf')
for i in range(1, j + 1):
q = max(q, p[i - 1] + r[j - i])
r.insert(j, q)
return r[n]
if __name__ == '__main__':
# 出售长度为i(i=1,2,3,,,10)的钢条所对应的价格样表
p = [1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
n = 9
result = Bottom_Up_Cut_Rod(p, n)
print("长度为{}时,".format(n))
print("对应的最优收益值为{}。".format(result))
The picture below shows the corresponding optimal cutting plan and profit for lengths 1, 2, 3....
Idea: In order to solve the original problem of size n, we first solve the sub-problem with exactly the same form but smaller size. That is, after the first cutting is completed, we regard the two sections of steel bars as two independent instances of the steel bar cutting problem. We form the optimal solution to the original problem by combining the optimal solutions of two related sub-problems and selecting the one with the greatest combined benefit among all possible two-section cutting solutions.
We say that the steel bar cutting problem satisfies the optimal substructure property: the optimal solution of the problem is composed of the optimal solutions of related sub-problems, and these sub-problems can be solved independently.
Matrix chain multiplication problem
The matrix chain multiplication problem is the problem of multiplying multiple matrices to find the fastest calculation order.
The way you add parentheses to the matrix chain will have a huge impact on the cost of the product operation. The following example illustrates:
Therefore, if A is a matrix of pXq and B is a matrix of qXr, then the product C is a matrix of pXr. The time required to calculate C is determined by the number of scalar multiplications, i.e. pqr. Below we will express the computational cost in terms of the number of scalar multiplications .
Traditional approach: Exhausting all possible bracketing solutions will not be an efficient algorithm because its running time will still increase exponentially .
The calculation cost formula of the matrix chain problem is defined below. Let m[i,j] represent the calculation matrix A i , . . . , j A_{i,...,j}Ai,...,jMinimum number of scalar multiplications required.
Assuming that the optimal split point k is unknown, the recursive solution formula becomes:
The following is an example to illustrate:
The picture above is an example written by me to help understanding. Among them, A 1 A_1A1:2 × \times × 3, meansA 1 A_1A1The dimensions of are 2 rows and 3 columns. The P list stores the dimensions of four matrices, where the number of columns of the previous matrix is equal to the number of rows of the latter matrix. Ask for A 1 A_1A1 A 2 A_2 A2 A 3 A_3 A3 A 4 A_4 A4The calculation cost of((A1A2)(A3A4)) . Among them,( A 1 A 2 ) (A_1A_2)(A1A2The calculation cost of ) corresponds to the matrix C in the above figure,( A 3 A 4 ) (A_3A_4)(A3A4) corresponds to the matrix D in the figure above. Finally, multiply C and D, and the final calculation cost (total number of times) is 48.
The process MATRIX-CHAIN-ORDER given below implements the bottom-up table method:
The meaning of each input variable is as follows:
The output variables are the optimal cost matrix m and the optimal split point matrix k.
The calculation process of the code is given below to help understand. Assume n=4, p = [ p 0 , p 1 , p 2 , p 3 , p 4 ] p = [p_0,p_1,p_2,p_3,p_4]p=[p0,p1,p2,p3,p4],求 A 1 A 2 A 3 A 4 A_1A_2A_3A_4 A1A2A3A4The optimal cost is to find m[1,4]. The calculation process is as follows:
The left side is the optimal cost matrix , and the right side is the optimal split point matrix .
Taking the data in Figure 15-5 as an example, the python code is as follows:
import numpy as np
def MATRIX_CHAIN_ORDER(p):
n = len(p) - 1
# 定义计算代价矩阵m
m = np.zeros((n, n))
# 定义最优分割点矩阵s
s = np.zeros((n, n))
for l in range(2, n + 1):
for i in range(1, n - l + 2):
j = i + l - 1
m[i - 1, j - 1] = float('inf')
for k in range(i, j):
q = m[i - 1, k - 1] + m[k, j - 1] + p[i - 1] * p[k] * p[j]
if q < m[i - 1, j - 1]:
m[i - 1, j - 1] = q
s[i - 1, j - 1] = k
print(m) # 每个元素对应长度为j-i+1的矩阵所需要最小计算代价
print(s) # 对应长度为j-i+1的矩阵最优分割点
return m, s
if __name__ == '__main__':
p = [30, 35, 15, 5, 10, 20, 25]
MATRIX_CHAIN_ORDER(p)
The running results are as follows:
It can be seen that the results are exactly the same as the two matrices in the book. (It’s just that the matrix is rotated along the main diagonal in the book)
Idea: Any solution to a non-trivial matrix chain multiplication problem instance requires chain partitioning, and any optimal solution is composed of optimal solutions to sub-problem instances. Therefore, in order to construct an optimal solution to an instance of the matrix chain multiplication problem, we can divide the problem into two subproblems ( A i A i + 1 ⋅ ⋅ ⋅ A k A_iA_{i+1}···A_kAiAi+1⋅⋅⋅AkSum A k + 1 ⋅ ⋅ ⋅ A j A_{k+1}···A_jAk+1⋅⋅⋅Aj) the optimal solution to the sub-problem instance, and then combine the optimal solutions to the sub-problems. We must ensure that when determining the dividing point, we have examined all possible dividing points, so as to ensure that the optimal solution will not be missed.
Conditions and scenarios that the application meets
Optimization problems solved by dynamic programming methods should have two elements: optimal substructure and subproblem overlap .
Dynamic programming algorithms can be used to solve optimization problems. In addition to the two examples given in this article, common scenarios include solving Fibonacci sequences, solving the longest common subsequence, 01 knapsack problem and optimal binary search trees, etc. . If you want to have an in-depth understanding of the principles and application scenarios of this algorithm, you can read " Introduction to Algorithms ". I have an electronic version. Brothers and sisters who need it can send me a private message to get it.
To sum up, problems that satisfy the overlapping nature of the optimal substructure and subproblems can be modeled using dynamic programming ideas. This method will open up memory, store previous calculation results, and call them directly the next time they encounter them without repeating the calculations. , thus greatly saving time, it is a classic algorithm that exchanges space for time. In most cases, projects tend to have stricter time requirements, which are much looser than memory usage.