Bài tập tham khảo Quy hoạch động

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 9

Tài liệu ôn thi Kỹ thuật Lập trình CLB Học thuật NES

Tài liệu tham khảo Quy hoạch động


Đây là một bài trong đề thi môn Thực hành Toán tổ hợp của trường mình năm 2021 (môn bắt
buộc của CNTT). Nó có liên quan đến đệ quy và quy hoạch động nên tụi mình muốn gửi để
các bạn tham khảo. Bài giải này là bài giải thật (tức là đã được nộp) do một thành viên K19
của CLB đóng góp. Cuối cùng mình có đính kèm 3 cách giải bài này theo quy hoạch động bằng
C++.

CLB và các cá nhân liên quan không sở hữu đề bài này.

Contents
1 Đề 1

2 Giải 2
2.1 Câu a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.1.1 Mô phỏng ý tưởng ban đầu . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.1.2 Thuật toán cho trường hợp có vũng nước . . . . . . . . . . . . . . . . . . 3
2.2 Câu b . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

3 Code 6

1 Đề
Tìm thuật toán để giải quyết bài toán sau đây, gọi là Bài toán đường đi tránh vũng nước:

Xét góc phần tư thứ 1 của hệ trục tọa độ Oxy với các lưới nguyên (giống như giấy tập có ô),
có một vũng nước S (là một tập hợp các điểm nguyên nằm trong góc phần tư thứ 1).

a. Tìm số đường đi từ O đến A tránh vũng nước S biết mỗi lần đi, chỉ được lên trên 1 đơn vị
hoặc qua phải 1 đơn vị.
b. Có nhận xét gì về số đường đi như vậy nếu vũng nước S không tồn tại?

Trang 1
Tài liệu ôn thi Kỹ thuật Lập trình CLB Học thuật NES

2 Giải
Đây là phần “suy luận” ra công thức truy hồi. Nếu các bạn không muốn đọc có thể nhảy xuống
phần 3 (Code) ở dưới. Lưu ý là trong phần suy luận này mình trình bày theo kiểu bottom-up.

2.1 Câu a
2.1.1 Mô phỏng ý tưởng ban đầu

Em xin mô hình hoá bài toán đã cho thành một bài toán quy hoạch động. Để dễ mô hình hoá,
em xin biểu diễn lại hệ trục toạ độ đã cho bằng một ma trận có kích thước (n + 1) × (m + 1)
với m, n là toạ độ của điểm A(m, n) đã cho trong đề bài. Khi đó bài toán có thể phát biểu lại
như sau:
Cho một lưới hình chữ nhật có kích thước (n + 1) × (m + 1), có bao nhiêu cách để đi từ điểm có
toạ độ (0, 0) đến điểm có toạ độ (n, m) mà chỉ đi lên trên hoặc đi qua phải, mỗi lần đi 1 đơn
vị?

Giả sử ta quy ước ô nằm ở góc dưới cùng bên trái là (0, 0) và trên cùng bên phải là (n, m).
Ta gọi cách để đi từ ô có toạ độ (0, 0) (ô bắt đầu) để đi đến
một ô có toạ độ (i, j) là a(i, j) với i, j lần lượt là chỉ số hàng
(n,m) và chỉ số cột của ô đó.
Vì từ ô bắt đầu ta chỉ có thể đi sang trái hoặc sang phải, nên
ta nhận thấy rằng để đi từ ô bắt đầu tới ô có toạ độ (i, j)
n+1 bất kỳ thì ta chỉ có 2 cách đi: tới từ ô ở ngay dưới bên dưới
(i − 1, j) hoặc từ ô ở ngay bên phải (i, j − 1).
Do đó, cách để đi đến một ô có vị trí (i, j) bất kỳ là:
(0,0)
a(i, j) = a(i − 1, j) + a(i, j − 1)
m+1
Ta nhận thấy rằng để đi từ ô có vị trí (0, 0) đến ô có vị trí (0, 0) thì ta có duy nhất 1 cách đi
là không làm gì cả, nên a(0, 0) = 1.
Để dễ tính toán, ta quy ước không có cách đi nào đến những ô có toạ độ âm, nên a(i, j) = 0
nếu i, j < 0.
Như vậy, ta có thể rút ra thuật toán cho bài toán này là (hiện tại ta chưa xét các “vũng nước”):

• Bắt đầu tại ô (0, 0). Lần lượt đi từ trái qua phải, từ dưới lên trên và cập nhật giá trị cho
các ô đã đi qua theo công thức:

1
 , (i, j) = (0, 0)
a(i, j) = 0 ,i < 0 ∨ j < 0

a(i, j − 1) + a(i − 1, j) , các TH còn lại

• Lặp lại bước trên và thoát khỏi thuật toán sau khi gán xong giá trị cho ô (n, m).

• Kết quả của bài toán (số đường đi từ O tới A) là giá trị tại ô (n, m).

Giả sử khi m = 3, n = 4 thì ma trận thu được sẽ là:

Trang 2
Tài liệu ôn thi Kỹ thuật Lập trình CLB Học thuật NES

1 5 15 35
1 4 10 20
1 3 6 10
1 2 3 4
1 1 1 1

⇒ Có 35 cách đi từ O tới A(3, 4). Ta có thể kiểm chứng bằng công thức chứng minh được ở
3
câu b: C3+4 = 35.

2.1.2 Thuật toán cho trường hợp có vũng nước

Gọi S là tập hợp những điểm trên hệ trục toạ độ bị “chìm” trong vũng nước.
Ta dễ dàng nhận thấy ở những ô chìm trong vũng nước thì không có cách nào để đi đến đó,
nên a(i, j) = 0 nếu (i, j) ∈ S.
Thuật toán cho trường hợp này tương tự trường hợp trên, chỉ có điều lần này ta sẽ xét thêm
các ô đó có nằm trong vũng nước hay không. Cụ thể như sau:

• Bắt đầu tại ô (0, 0). Lần lượt đi từ trái qua phải, từ dưới lên trên và cập nhật giá trị cho
các ô đã đi qua theo công thức:

1
 , (i, j) = (0, 0)
a(i, j) = 0 , i < 0 ∨ j < 0 ∨ (i, j) ∈ S

a(i, j − 1) + a(i − 1, j) , các TH còn lại

• Lặp lại bước trên và thoát khỏi thuật toán sau khi gán xong giá trị cho ô (n, m).

• Kết quả của bài toán là giá trị tại ô (n, m).

Ví dụ: xét điểm A(5, 6) và vũng nước như hình bên.


Ta nhận thấy rằng các điểm trên hệ trục toạ độ bị
“chìm” trong vũng nước là
{(2, 2), (2, 3), (2, 4), (3, 3), (3, 4)}.
Vậy, S = {(2, 2), (3, 2), (4, 2), (3, 3), (4, 3)} (do ký hiệu
toạ độ trong hệ trục Oxy và ma trận bị ngược nhau).
Khi thực hiện thuật toán, các ô mang toạ độ này sẽ
chứa giá trị 0.
Vậy ta có thể thành lập một ma trận 7 × 6 như
sau:.

Trang 3
Tài liệu ôn thi Kỹ thuật Lập trình CLB Học thuật NES

1 7 13 19 34 82

1 6 6 6 15 48

1 5 0 0 9 33

1 4 0 0 9 24

1 3 0 4 9 15

1 2 3 4 5 6

1 1 1 1 1 1

Vậy đáp số của ví dụ này là 82.

(còn nữa ở trang sau)

Trang 4
Tài liệu ôn thi Kỹ thuật Lập trình CLB Học thuật NES

2.2 Câu b
Nếu không tồn tại vũng nước, ta nhận thấy rằng để đi
từ gốc toạ độ O đến A(m, n) luôn luôn tốn m + n bước.
Ví dụ như xét điểm A(5, 6) như hình bên, ta luôn luôn
tốn 5 + 6 = 11 bước để đi từ O đến A.
Nếu ta ký hiệu một bước đi lên là ký tự U và một bước
sang phải là ký tự R, một đường đi sẽ tương ứng với
một chuỗi các kí tự U, R. Ta nhận thấy nếu ta đổi chỗ
các kí tự U hoặc các kí tự R cho nhau thì chuỗi trên
vẫn không thay đổi, nên bài toán sẽ trở thành: có bao
nhiêu cách chọn m + n kí tự từ m kí tự R và n kí tự U?

Ta có thể dễ dàng suy ra số cách chọn từ kiến thức về


tổ hợp:    
m+n m+n
=
m n
Đường đi ứng với chuỗi URRURUUURRU

Lưu ý về ký hiệu:  
n
= Cnk
k

(còn nữa ở trang sau)

Trang 5
Tài liệu ôn thi Kỹ thuật Lập trình CLB Học thuật NES

3 Code
Các bạn có thể copy nội dung của các đoạn code trong phần này trong thư mục answer-sources
ở link sau: Github: hungngocphat01/nes-ktlt-2021.

1 #include <iostream>
2 using namespace std;
3

4 // Cấu trúc tọa độ trong không gian 2 chiều


5 struct Point2D {
6 int x;
7 int y;
8 };
9

10 // 2 hàm tiện ích để cấp phát/giải phóng mảng 2 chiều


11 // init: giá trị khởi tạo cho các phần tử trong mảng
12 int** create2DArray(int row, int col, int init) {
13 int **arr = (int**)malloc(sizeof(int*) * row);
14 for (int i = 0; i < row; i++) {
15 arr[i] = (int*)malloc(sizeof(int) * col);
16 for (int j = 0; j < col; j++) {
17 arr[i][j] = init;
18 }
19 }
20 return arr;
21 }
22

23 void delete2DArray(int**& arr, int row) {


24 for (int i = 0; i < row; i++) {
25 free(arr[i]);
26 }
27 free(arr);
28 arr = NULL;
29 }
30

31 // Hàm kiểm tra xem (i, j) có nằm trong vũng nước không
32 // S: vũng nước (danh sách các tọa độ); z: kích thước mảng S
33 bool inPuddle(int i, int j, Point2D S[], int z) {
34 for (int k = 0; k < z; k++) {
35 if (S[k].x == i && S[k].y == j) {
36 return true;
37 }
38 }
39 return false;
40 }
41

42 // Hàm đếm số bước đi từ gốc tọa độ (0, 0) đến điểm (m, n) có vũng nước là S
43 // Hàm này chưa sử dụng quy hoạch động

Trang 6
Tài liệu ôn thi Kỹ thuật Lập trình CLB Học thuật NES

44 int countSteps(int m, int n, Point2D S[], int z) {


45 // Trường hợp (m, n) trùng gốc tọa độ (0, 0)
46 if (m == 0 && n == 0) {
47 return 1;
48 }
49 // Nếu m hoặc n âm hoặc (m, n) trong vũng nước
50 else if (m < 0 || n < 0 || inPuddle(m, n, S, z)) {
51 return 0;
52 }
53 // Các trg hợp còn lại
54 return countSteps(m - 1, n, S, z) + countSteps(m, n - 1, S, z);
55 }
56

57 // Hàm tương tự như trên nhưng sử dụng quy hoạch động theo top-down
58 // answer: bảng tra lời giải
59 int QHD_TopDown(int m, int n, Point2D S[], int z, int **answer) {
60 // Kiểm tra xem lời giải đã có trong bảng chưa. Nếu có trả về luôn
61 // Ta phải có đk m*n > 0 để tránh trường hợp bị chỉ số âm
62 if (m * n > 0 && answer[m][n] != -1) {
63 return answer[m][n];
64 }
65

66 // Nếu chưa có lời giải, giair lại y hệt hàm đệ quy ở trên
67 if (m == 0 && n == 0) {
68 return 1;
69 }
70 else if (m < 0 || n < 0 || inPuddle(m, n, S, z)) {
71 return 0;
72 }
73 int result = QHD_TopDown(m - 1, n, S, z, answer) + QHD_TopDown(m, n - 1,
,→ S, z, answer);
74 // Lưu lại giá trị trên vào bangr tra
75 answer[m][n] = result;
76 return result;
77 }
78

79 int QHD_BottomUp(int m, int n, Point2D S[], int z) {


80 // Bảng tra lời giải có (m+1) hàng * (n+1) cột
81 int **answer = create2DArray(m+1, n+1, 0);
82

83 for (int i = 0; i < m+1; i++) {


84 for (int j = 0; j < n+1; j++) {
85 // Nếu (i, j) == (0, 0) hoặc trong vũng nước thì "trả về" 0
86 if ((i == 0 && j == 0) || (inPuddle(i, j, S, z))) {
87 answer[i][j] = 0;
88 }
89 /** Nếu (i, j) đi dọc theo đường biên của bảng thì trả về 1 (các
,→ bạn có thể quan sát hình minh họa ở phần trên).

Trang 7
Tài liệu ôn thi Kỹ thuật Lập trình CLB Học thuật NES

90

91 Phải làm như thế này để tránh trường hợp i < 0 hay j < 0 do khi
,→ ta sử dụng mảng 2 chiều thì chỉ số ko thể âm. **/
92 else if ((i == 0 && j != 0) || (i != 0 && j == 0)) {
93 answer[i][j] = 1;
94 }
95 // Các trg hợp còn lại
96 else {
97 answer[i][j] = answer[i - 1][j] + answer[i][j - 1];
98 }
99 }
100 }
101

102 int a = answer[m][n];


103 // Giải phóng
104 delete2DArray(answer, m+1);
105 return a;
106 }
107

108 int main() {


109 // Các tọa độ vũng nước
110 Point2D S[] = {
111 {1, 2},
112 {3, 4},
113 {5, 6}
114 };
115 int z = 3; // kích thước mảng S
116 int m = 16, n = 16; // tọa độ ô cần test
117

118 // GIẢI THEO TOP-DOWN


119 // Tạo bảng tra lời giải, ta khởi tạo -1 cho những chỗ chưa có lời giải
120 int** answer = create2DArray(m+1, n+1, -1);
121 cout << "Ket qua top-down: " << endl;
122 int c = QHD_TopDown(m, n, S, z, answer);
123 cout << c << endl;
124 delete2DArray(answer, m+1);
125

126 // GIẢI THEO BOTTOM-UP


127 cout << "Ket qua bottom-up: " << endl;
128 c = QHD_BottomUp(m, n, S, z);
129 cout << c << endl;
130

131 // GIẢI THEO ĐỆ QUY THƯỜNG (KHÔNG CÓ QUY HOẠCH ĐỘNG)


132 cout << "Ket qua de quy thong thuong: " << endl;
133 c = countSteps(m, n, S, z);
134 cout << c << endl;
135

136 return 0;

Trang 8
Tài liệu ôn thi Kỹ thuật Lập trình CLB Học thuật NES

137 }

Sau khi chạy, các bạn sẽ thu được kết quả như sau:

Trong đó 2 cái đầu tiên có quy hoạch động chạy rất nhanh (giải ra ngay lập tức). Cái cuối
cùng giải rất lâu nên mình không chụp được kết quả. Trong một số trường hợp có thể không
giải ra được.

Như các bạn thấy thì việc code 1 bài như trên đòi hỏi phải sử dụng cấp phát động và định
nghĩa thêm kiểu struct khá phiền phức. Đó là lý do tại sao bạn nên sử dụng Python (hay
các ngôn ngữ bậc cao khác) để học thuật toán. Các bạn có thể đọc code của bài đó giải bằng
Python ở link: QHD_ThamKhao.py @ hungngocphat01/LaTeX-NES-KTLT-2021.

Trang 9

You might also like