Nguồn: Topcoder.
Có rất nhiều bài toán được áp dụng quy hoạch động (QHĐ) (Dynamic Programming). QHĐ là một trong những kĩ thuật quan trọng. Bài viết này sẽ giúp bạn hiểu được QHĐ thông qua các ví dụ cụ thể.
Note: Trong bài này có thể có nhiều phần bạn đã biết, bạn hoàn toàn có thể chuyển qua đọc phần khác.
QHĐ là kĩ thuật được được dùng khi có một công thức và một (hoặc một vài) trạng thái bắt đầu. Một bài toán được tính bởi các bài toán nhỏ hơn đã tìm ra trước đó. QHĐ có độ phức tạp đa thức nên sẽ chạy nhanh hơn quay lui và duyệt trâu.
Để hiểu rõ hơn hãy xem ví dụ sau:
Cho đồng xu và giá tiền của mỗi đồng (), và số . Tìm số đồng xu nhỏ nhất để tổng giá trị của chúng bằng (số lượng đồng xu không giới hạn).
Bây giờ chúng ta sẽ xây dựng thuật giải:
Đầu tiên, cần tìm một trạng thái của bài toán.
Trạng thái là một trường hợp, một bài toán con của bài toán lớn.
Ví dụ, trạng thái trong bài này là số lượng xu nhỏ nhất để tổng bằng , với . Để tìm ra trạng thái , cần phải tìm tất cả các trạng thái mà . Một khi đã tìm ra trạng thái , ta có thể dễ dàng tìm ra trạng thái của .
Với mỗi , , tìm số đồng xu nhỏ nhất để tổng bằng . Giả sử nó bằng . Nếu nhỏ hơn số lượng đồng xu hiện tại cho tổng thì ta cập nhập nó bằng .
Sau đây là ví dụ:
Cho các đồng xu với giá tiền 1, 3 và 5.
Và = 11.
Đầu tiên, ta bắt đầu từ trạng thái 0, chúng ta có .
Xét đến tổng 1. Có duy nhất đồng xu 1 nhỏ hơn hoặc bằng tổng 1, nên ta có .
Xét đến tổng 2. Cũng giống như tổng trước, chỉ có 1 đổng xu 2, có
Đến tổng 3. Lần này có 2 đồng xu 3 là 1 và 3.
- Nếu ta chọn đồng 1, ta có
- Nếu ta chọn đồng 3, ta có
Rõ ràng là 1 3 nên ta chọn đồng 3 và
Xét tiếp đến tổng 4, rồi đến 11 bằng cách như trên.
Mã giả:
Gán Min[i] bằng dương vô cùng với mọi i
Min[0]=0
For i = 1 to S
For j = 0 to N - 1
If (Vj<=i AND Min[i-Vj]+1<Min[i])
Then Min[i]=Min[i-Vj]+1
Output Min[S]
Đây là lời giải cho tất cả các tổng:
Tổng | Lượng xu nhỏ nhất | Xu được chọn (tổng còn lại) |
0 | 0 | - |
1 | 1 | 1 (0) |
2 | 2 | 1 (1) |
3 | 1 | 3 (0) |
4 | 2 | 1 (3) |
5 | 1 | 5 (0) |
6 | 2 | 3 (3) |
7 | 3 | 1 (6) |
8 | 2 | 3 (5) |
9 | 3 | 1 (8) |
10 | 2 | 5 (5) |
11 | 3 | 1 (10) |
Vậy là chúng ta đã tìm được lời giải cho 3 đồng xu tổng bằng 11.
Dựa vào bảng trên, ta có thể truy vết lại được những đồng xu nào được chọn để tối ưu bài toán.
Bài QHĐ trên còn có một cách tiếp cận khác nữa. Lần này, ta sẽ không tính liên tiếp các tổng. Bắt đầu từ trạng thái 0. Thử nhét đồng xu thứ 1 vào các tổng đã tính. Nếu như tổng có số đồng xu ít hơn số đồng xu hiện tại thì tiến hành cập nhật. Rồi tiếp tục thử với đồng thứ 2, 3 cho đến khi thử hết các đồng. Ví dụ, nhét đồng 1 (giá trị 1) vào tổng 0 ta có tổng 1. Vì ta chưa tính tổng 1 nên . Nhét đồng 1 vào tổng 1 ta có . Tiếp tục làm như vậy với các tổng còn lại. Sau đồng 1, ta nhét đồng 2(giá trị 3) vào tổng 0 ta được 1, mà , ta cập nhật . Tiếp tục nhét đồng 2 vào các tổng còn lại, cũng nhứ thử nhét các đồng xu khác.
#Elementary
Bây giờ, chúng ta cùng đến một khái niệm mới, công thức truy hồi (recurrent relation), mối liên hệ giữa những trạng thái.
Ví dụ:
Cho một dãy N số - . Tìm dãy con không giảm dài nhất.
Ta quy định trạng thái là dãy con không giảm dài nhất kết thúc tại . Với và , tính được khi tồn tại (vì đây là dãy không giảm). Khi đó . Tiếp tục tính như vậy cho đến khi đến được trạng thái .
Hãy xem bảng sau với dãy: 5, 3, 4, 8, 6, 7:
I | Độ dài dãy con không giảm dài nhất của i số đầu tiên |
Vị trí của kí tự cuối trong dãy |
1 | 1 | 1 |
2 | 1 | 2 |
3 | 2 | 2 |
4 | 3 | 3 |
5 | 3 | 3 |
6 | 4 | 5 |
Bài luyện tập:
Cho đồ thị vô hướng có đỉnh () và các cạnh có trọng số dương. Tìm đường đi ngắn nhất từ đỉnh 1 đến đỉnh hoặc thông báo không tồn tại đường đi.
Gợi ý: Tại mỗi bước, chọn ra trong số các đỉnh chưa thăm mà có đường đi từ 1, chọn ra đỉnh có đường đi ngắn nhất.
Các bài ví dụ khác:
Tới đây bạn sẽ được làm quen với QHĐ 2 chiều.
Bài toán:
Cho một bảng , mỗi ô có một lượng táo. Bắt đầu từ ô trái trên, mỗi bước có thể đi sang phải hoặc xuống dưới. Bạn có thể ăn được nhiều nhất bao nhiêu quả táo ?
Cách giải bài này cũng tương tự như những bài trước.
Đầu tiên là phải xác định trạng thái là gì. Ở mỗi ô có nhiều nhất 2 cách có thể tới được ô đó, từ ô bên trái và ô phía trên. Do vậy, để tìm trạng thái hiện tại, ta phải tính trước các ô có thể đến được nó.
Ta có công thức truy hồi sau:
(trong đó, là hàng, là cột, là số táo ở ô )
có thể được tính từ trái sang phải, từ trên xuống dưới, hoặc từ trên xuống, từ trái sang.
Mã giả:
For i = 0 to N - 1
For j = 0 to M - 1
S[i][j] = A[i][j] +
max(S[i][j-1], if j>0 ; S[i-1][j], if i>0 ; 0)
Output S[n-1][m-1]
Ví dụ khác:
#Upper-Intermediate
Phần này sẽ giới thiệu với bạn những bài toán cùng với một số điều kiện.
Đây là một ví dụ cụ thể:
Cho đồ thị vô hướng có trọng số dương và đỉnh.
Ban đầu bạn có số tiền là . Để đi qua đỉnh , bạn phải trả số tiền là . Và đương nhiên, nếu không đủ tiền thì bạn không đi được. Tìm đường đi ngắn nhất từ 1 tới thỏa mãn tiêu chí trên. Nếu có nhiều đường ngắn nhất, in ra đường với chi phí nhỏ nhất. Giới hạn: ; ; .
Có thể dễ dàng thấy đây là một bài Dijkstra cơ bản, tuy nhiên chỉ khác ở chỗ nó có thêm một điều kiện. Trong bài toán Dijkstra cơ bản ta có , là độ dài đường đi ngắn nhất từ 1 tới . Còn ở đây, chúng ta cần phải quan tâm đến số tiền còn lại. Do đó chúng ta có thể mở rộng mảng này thành , là độ dài đường đi ngắn nhất tới , và còn lại số tiền là . Bằng cách này bài toán đã được đưa về bài toán Dijkstra quen thuộc. Tại mỗi bước ta tìm trạng thái có quãng đường ngắn nhất, đánh dấu là đã thăm rồi update cho các trạng thái cạnh nó. Đáp án sẽ là có giá trị nhỏ nhất (và lớn nhất trong số các có cùng giá trị).
Mã giả:
Gán mọi(i,j) là chưa thăm
Gán Min[i][j] bằng dương vô cùng với mọi (i,j)
Min[0][M]=0
While(TRUE)
Trong số những trạng thái chưa thăm (i,j) tìm cái có Min[i][j]
nhỏ nhất. Giải sử nó là (k,l).
Nếu không tìm được (k,l) nào mà Min[k][l] nhỏ hơn dương vô cùng - thoát vòng lặp.
Đánh dấu (k,l) đã thăm
For All Neighbors p of Vertex k.
If (l-S[p]>=0 AND
Min[p][l-S[p]]>Min[k][l]+Dist[k][p])
Then Min[p][l-S[p]]=Min[k][l]+Dist[k][p]
i.e.
Nếu tại (i,j) có đủ tiền để đi qua p (l-S[p] là số tiền còn lại sau khi đi qua p), và đường đi ngắn nhất của (p,l-S[p]) lớn hơn [đường đi ngắn nhất tới (k,l)] + [khoảng cách từ k tới p)],
thì gán (i,j) bằn tổng này.
End For
End While
Tìm số nhỏ nhất trong các Min[N-1][j] (for all j, 0<=j<=M);
Nếu có nhiều hơn một trạng thái, lấy trạng thái nào có j lớn nhất. Nếu không có (N-1,j) nào nhỏ hơn dương vô cùng - không tồn tại đường đi.
Các bài luyện tập:
Những bài sau đây sẽ cần một chút kĩ năng phân tích để có thể tối ưu chúng thành bài QHĐ.
Problem StarAdventure – SRM 208 Div 1:
Cho ma trận M hàng, N cột (). Mỗi ô có một lượng táo.
Bạn đang ở ô góc trái trên. Bạn chỉ có thể đi xuống hoặc sang phải. Bạn cần tới ô góc phải dưới. Rồi quay lại ô trái trên bằng cách lên hoặc sang trái. Cuối cùng, bạn quay lại ô phải dưới.
Tìm số táo nhiều nhất mà bạn có thể ăn được.
Khi đi qua một ô, toàn bộ táo của ô đấy sẽ bị ăn hết.
Giới hạn: mỗi ô có từ 0 đến 1000 quả táo.
Đọc đến đây, hẳn bạn sẽ thấy cái đề này quen quen, nó chính là bài mở rộng của bài toán phần Intermediate. Ta có thể thử đưa bài toán này về thành bài toán trên. Để ý thấy đường đi từ ô góc phải dưới lên trái trên cũng có thể coi là một đường đi từ góc trái trên xuống. Như vậy, chúng ta phải xử lý bài toán với 3 đường đi từ trái trên xuống. Gọi 3 đường này là trái, giữa và phải. Khi 2 đường giao nhau (như hình dưới):
thì nó cũng tương đương với hình sau:
Bằng cách này, chúng ta đã có một cái nhìn khác về bài toán. Các đường này sẽ không giao nhau (trừ ô góc trái trên và phải dưới). Với mỗi hàng y (không phải hàng đầu và cuối), tọa độ x ở mỗi đường sẽ là ( , và ) : . Ta xét hàng thứ y. Giả sử, ta xét , and và số táo hiện giờ thu được là nhiều nhất. Từ đó ta có thể tối ưu cho hàng . Chúng ta cần tìm cách chuyển trạng thái. Gọi là lượng táo nhiều nhất thu được đến hàng với 3 đường đang dừng ở cột , , và . Với hàng , thêm vào số lượng táo ở các ô , and . Vì chúng ta đang đi xuống. Sau đó, chúng ta xét đến những đường có thể sang phải. Để tránh việc giao nhau, ta xét lần lượt các bước ở trái, phải rồi giữa.
Bài luyện tập thêm:
Note:
Khi gặp một bài toán, hãy để ý xem nó có được giải trong thời gian đa thức không. Nếu có, thử xác định trạng thái của nó, cách chuyển trạng thái, và nếu không chuyển được trạng thái, hãy thử tối ưu nó về một bài QHĐ (như ví dụ ở trên).
Những bài đã đề cập ở trên: