Dr thấy cái tài liệu này cũng hay hay, và rất có ích. Mời các bạn tham khảo. Sẽ có phiên bản tiếng Việt, nhưng các bạn đọc trước phiên bản tiếng Anh này đã nhé!
Dynamic programming is a common and useful technique in writing programs. In this context, "programming" does not refer to coding, it refers to planning in a tabular format. The very hallmark of dynamic programming is that it stores intermediate results during the computation in one or more separate arrays. However, in doing so, sometimes it can result in a faster algorithm. Usually, dynamic programming is used to solve optimization problems, such as to determine the shortest, fastest, most profitable, least cost, etc. way of doing a task.
We shall consider the following problem. Suppose you are in a city with m horizontal roads and n vertical roads. The horizontal roads are numbered from 1 to m and the vertical roads are numbered from 1 to n as shown in the diagram below.

All the horizontal roads intersect with the vertical roads at the junctions. Each junction is labelled (i, j), where i is the number of the horizontal road and j is the number of the vertical road.
You are driving a car and is at the junction (1, 1), and your car is low in petrol, so you need to drive your car to the petrol station located at (m, n). As you have to meet an important client after filling up the petrol, so you need to reach the petrol station in the shortest possible time. However, the amount of petrol in your car is low, so you have to drive to the petrol using the shortest distance route. That is, you want to drive only rightward or downward, you do not want to waste petrol by driving leftward or upward. Also, the amount of time need to travel from a junction to any of its adjacent junctions is known, the amount of time varies from place to place because the amount of congestion at different places is not the same. You can assume that no time is needed to clear a junction.
What is the fastest route you should take to drive to the petrol station, and how much time is required?
We shall formalize the problem as follows. Let h(i, j) be the amount of time needed to drive from (i, j) to (i, j + 1), and v(i, j) be the amount of time needed to drive from (i, j) to (i + 1, j). Both h(i, j) and v(i, j) are known for all junctions (i, j). We shall answer the second question on the time required first.
Using the dynamic programming approach, we will need to define another array t of size m by n, where t(i, j) is the minimum amount of time needed to drive from (i, j) to (m, n).
Let us define the basis first:- t(m, n) = 0.
- t(m, j) = t(m, j + 1) + h(m, j), for j = 1 to n - 1.
- t(i, n) = t(i + 1, n) + v(i, n), for i = 1 to m - 1.
The first is simple: if you are already at (m, n), you don't need to spend any more time travelling. The second base case arise because if you are already at horizontal road m, then you can only move right. Similarly, if you are already at vertical road n, then you can only drive downwards, so this is how the third base case is derived. With the above three conditions, we can fill up the last row and column of the array t backwards from t(m, n).
Next, we define the recurrence:- t(i, j) = min{t(i, j + 1) + h(i, j), t(i + 1, j) + v(i, j)}.
This recurrence relation should be self-explanatory: suppose we already computed t(i, j + 1) and t(i + 1, j), then we can find the fastest way to reach (m, n) from (i, j) using the recurrence relation. The recurrence is the one which allows us to fill up the remaining entries of the table t.
Thus, we can derive an algorithm to compute the entries of the table t:
Code:
// Basis
t(m, n) = 0
for j = n - 1 downto 1
t(m, j) = t(m, j + 1) + h(m, j)
for i = m - 1 downto 1
t(i, n) = t(i + 1, n) + v(i, n)
// Recurrence
for i = m - 1 downto 1
for j = n - 1 downto 1
if t(i, j + 1) + h(i, j) < t(i + 1, j) + v(i, j)
t(i, j) = t(i, j + 1) + h(i, j)
else
t(i, j) = t(i + 1, j) + v(i, j)
Notice that the correctness of this algorithm is from the fact that the array t is built backwards from bottom up as well as right to left. The algorithm terminates when we have computed t(1, 1), which is the minimum time required to drive from (1, 1) to (m, n).
So we have solved half the problem. How do we find out the route which takes the shortest time?
To do this, we will augment the above algorithm with another array r of size m by n, where r(i, j) is either the value RIGHT or DOWN, indicating driving rightward or downward will lead to the fastest route from (i, j) to (m, n). Again we define the basis and recurrence for r:- r(m, j) = RIGHT, for j = 1 to n - 1.
- r(i, n) = DOWN, for i = 1 to m - 1.
- r(i, j) = RIGHT, if t(i, j + 1) + h(i, j) < t(i + 1, j) + v(i, j), for j = 1 to n - 1 and i = 1 to m - 1.
- r(i, j) = DOWN, if t(i, j + 1) + h(i, j) >= t(i + 1, j) + v(i, j), for j = 1 to n - 1 and i = 1 to m - 1.
Note that r(m, n) is undefined, but that is not important. We will use the array r to determine the route from (1, 1) to (m, n).
We modify the algorithm thus:
Code:
// Basis
t(m, n) = 0
for j = n - 1 downto 1
t(m, j) = t(m, j + 1) + h(m, j)
r(m, j) = RIGHT
for i = m - 1 downto 1
t(i, n) = t(i + 1, n) + v(i, n)
r(i, n) = DOWN
// Recurrence
for i = m - 1 downto 1
for j = n - 1 downto 1
if t(i, j + 1) + h(i, j) < t(i + 1, j) + v(i, j)
t(i, j) = t(i, j + 1) + h(i, j)
r(i, j) = RIGHT
else
t(i, j) = t(i + 1, j) + v(i, j)
r(i, j) = DOWN
Finally, we can determine the shortest time route by backtracking using the array r:
Code:
// Backtracking
i = 1
j = 1
while i < m and j < n
if r(i, j) == RIGHT
print "Drive rightward from (i, j) to (i, j + 1)"
j = j + 1
else // r(i, j) == DOWN
print "Drive downward from (i, j) to (i + 1, j)"
i = i + 1
Although the algorithm seems a little bit complicated, it actually achieves the best possible time complexity. Since both t and r are of size m by n, filling up both tables in the basis and recurrence steps take O(mn) time. For the backtracking step, we will move rightwards a total of n - 1 times and downwards a total of m - 1 times, so the time complexity is O(m + n). Hence, the total time complexity is still O(mn). Intuitively, this is the best possible time complexity since there are mn junctions in the problem, so the time complexity is actually linear with respect to the size of the problem.
We shall conclude this miniature with another exercise in dynamic programming known as the subset sum problem.
Formally, we define the subset sum problem as follows. You are given:- A set of n positive integers, S.
- A positive number, m.
Is there a subset of S whose elements sum up to m? For example, if S = {2, 3, 5, 7, 11} and m = 14, then the answer is yes because 2 + 5 + 7 = 14; and if S = {2, 3, 5, 7, 11} and m = 6, then the answer is no.
Give an algorithm that uses dynamic programming to solve the subset sum problem, and analyze the time complexity of your algorithm in terms of n and m.
Chúc mọi thành viên cộng đồng C Việt thành công và học tập tốt!