Quy hoạch động cơ bản (Phần 2)
Bài viết được sưu tầm và bổ sung từ bài viết "Một số bài toán quy hoạch động kinh điển" của thầy Nguyễn Thanh Tùng, Tạp chí Tin học và Nhà trường số
Người viết: Nguyễn Anh Bảo - Đại học Bách Khoa Hà Nội
Reviewer: * Hồ Ngọc Vĩnh Phát - Đại học Khoa học Tự nhiên, ĐHQG-HCM * Ngô Nhật Quang - Trường THPT chuyên Khoa học Tự Nhiên, ĐHQGHN
Giới thiệu¶
Ở phần trước, chúng ta đã được làm quen với khái niệm Quy hoạch động đồng thời xem xét một số bài toán điển hình. Bài viết này nhằm giới thiệu thêm một số bài toán quy hoạch động thường gặp khác.
1. Biến đổi xâu¶
Để thuận tiện cho việc trình bày và tính toán, trong mục
1.1. Mô hình¶
Cho hai xâu
, có độ dài lần lượt là và , bắt đầu từ chỉ số . Ta muốn biến đổi xâu về xâu qua một số phép biến đổi thuộc các loại sau: * Chèn kí tự vào sau kí tự thứ : I i C
,i
có thể bằng* Thay thế kí tự ở vị trí thứ bằng kí tự : R i C
* Xoá kí tự ở vị trí thứ: D i
Hãy tìm số ít nhất các phép biến đổi để biến xâu
thành xâu . Điều kiện: .
Ví dụ 1: ILoveVNOI
ILoveVNOIMore
Lượt biến đổi | Phép biến đổi | A |
---|---|---|
I 9 e |
ILoveVNOIe |
|
I 9 r |
ILoveVNOIre |
|
I 9 o |
ILoveVNOIore |
|
I 9 M |
ILoveVNOIMore |
Ví dụ 2: asrefghuar
regular
Lượt biến đổi | Phép biến đổi | A |
---|---|---|
R 8 l |
asrefghlar |
|
R 7 u |
asrefgular |
|
D 5 |
asregular |
|
D 1 |
sregular |
|
D 1 |
regular |
Ví dụ 3: D9M12Y2022
D1M1Y2023
Lượt biến đổi | Phép biến đổi | A |
---|---|---|
R 2 1 |
D1M12Y2022 |
|
R 10 3 |
D1M12Y2023 |
|
D 5 |
D1M1Y2023 |
1.2. Lời giải¶
Chú ý: Chỉ số bắt đầu của
Đặt
Ý tưởng bài toán này là quy hoạch động mảng
Dễ thấy
Ta đi tìm công thức truy hồi:
Có 2 trường hợp xảy ra:
- Nếu
: Cần biến xâu thành xâu . Để biến đổi tối ưu, cần biến đổi xâu thành xâu sau ít phép biến đổi nhất, giữ nguyên kí tự . Số phép biến đổi cần thiết là . - Nếu
: Để biến đổi thành , ta có thể dùng một trong các cách sau:- Xoá kí tự
: Sau khi xóa, cần biến thành . Số phép biến đổi ít nhất là . Do đó số phép biến đổi phải dùng là . - Thay thế
bởi : Sau đó, cần biến thành . Số phép biến đổi ít nhất là . - Chèn
vào sau : Sau đó, cần biến đổi thành . Số phép biến đổi ít nhất là . - Các cách biến đổi khác đều phải chứa phép biến đổi
hoặc . Do đó, ta có thể đảo thứ tự các phép biến đổi để đưa về lần biến đổi đầu tiên là hoặc .
- Xoá kí tự
Tổng kết lại, ta có công thức QHĐ sau:
, với mọi . , với mọi . nếu . nếu .
Bài này ta có thể tiết kiệm biến hơn bằng cách dùng 2 mảng 1 chiều tính lẫn nhau và một mảng đánh dấu 2 chiều để truy vết.
1.3. Code tham khảo¶
Cần lưu ý thứ tự tính. Để tính
## include <iostream>
## include <string>
## include <vector>
using namespace std;
int m, n;
string a, b;
vector<vector<int>> L;
int main()
{
cin >> a >> b;
m = a.length();
n = b.length();
L.resize(m + 1);
for (auto& i : L)
i.resize(n + 1);
/* Vì a và b bắt đầu từ chỉ số 1 nên
* chèn thêm 1 kí tự vào đầu 2 xâu */
a = "_" + a;
b = "_" + b;
for (int i = 0; i <= m; i++)
L[i][0] = i;
for (int j = 0; j <= n; j++)
L[0][j] = j;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
{
if (a[i] == b[j])
L[i][j] = L[i - 1][j - 1];
else
L[i][j] = 1 + min(L[i - 1][j - 1],
min(L[i - 1][j], L[i][j - 1]));
}
cout << L[m][n];
}
1.4. Một số bài toán khác¶
1.4.1. Xâu con chung dài nhất¶
Link nộp bài: VNOJ-LCS
Cho
xâu A và B. Tìm xâu con chung dài nhất của và . Xâu là xâu con của xâu khi và chỉ khi có thể thu được bằng cách xóa một số kí tự của (có thể tất cả hoặc không kí tự nào). Điều kiện: . Input: xâu và . Output: Độ dài xâu con chung dài nhất.
Lời giải:
Gọi
- Nếu
thì ta chỉ cần chọn xâu con dài nhất của và , do đó độ dài xâu con dài nhất của và là . - Nếu
thì xâu con chung dài nhất sẽ là xâu con của và hoặc và .
Từ đó có công thức quy hoạch động như sau:
nếu nếu .
Cài đặt:
Lưu ý do std::vector
trong c++
).
Nếu đề bài yêu cầu phải in ra xâu con dài nhất thì phải thực hiện truy vết. Dưới đây là một cách cài đặt tham khảo:
## include <iostream>
## include <string>
## include <vector>
using namespace std;
// Struct dùng để truy vết
struct Trace
{
// Vị trí của kí tự trước đó trong A và B
int i;
int j;
// Kí tự cần thêm vào xâu kết quả
// (có thể là kí tự NULL)
char c;
Trace(int ii = 0, int jj = 0, char cc = '\0')
: i(ii), j(jj), c(cc)
{ };
};
int m, n;
string a, b;
vector<vector<int>> L;
vector<vector<Trace>> Tr;
int main()
{
cin >> a >> b;
m = a.length();
n = b.length();
L.resize(m + 1);
Tr.resize(m + 1);
for (auto& i : L)
i.resize(n + 1);
for (auto& i : Tr)
i.resize(n + 1);
// Vì a và b bắt đầu từ chỉ số 1 nên
// chèn thêm 1 kí tự vào đầu 2 xâu
a = "_" + a;
b = "_" + b;
for (int i = 0; i <= m; i++)
L[i][0] = 0;
for (int j = 0; j <= n; j++)
L[0][j] = 0;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
{
if (a[i] == b[j])
{
L[i][j] = L[i - 1][j - 1] + 1;
Tr[i][j] = Trace(i - 1, j - 1, a[i]);
}
else if (L[i - 1][j] > L[i][j - 1])
{
L[i][j] = L[i - 1][j];
Tr[i][j] = Trace(i - 1, j);
}
else
{
L[i][j] = L[i][j - 1];
Tr[i][j] = Trace(i, j - 1);
}
}
// Truy vết xâu con chung dài nhất từ Tr[m][n]
Trace t = Tr[m][n];
string ans = "";
while (true)
{
if (t.c != '\0')
ans = t.c + ans;
if (t.i == 0 && t.j == 0)
break;
else
t = Tr[t.i][t.j];
}
cout << ans;
}
Như vậy độ phức tạp bộ nhớ của bài toán là
Có một phương pháp cài đặt tốt hơn, chỉ với độ phức tạp bộ nhớ
Để tính
, ta chỉ cần 3 ô , và . Tức là để tính hàng
thì chỉ cần hàng và . Do đó ta chỉ cần 2 mảng 1 chiều để lưu hàng vừa tính và hàng đang tính . Cách cài đặt mới như sau:
vector<int> P(n + 1), L(n + 1);
for (int i = 1; i <= m; i++)
{
L[0] = 0;
for (int j = 1; j <= n; j++)
if (a[i] == b[j])
L[j] = P[j - 1] + 1;
else
L[j] = max(L[j - 1], P[j]);
P = L;
}
1.4.2. Bắc cầu¶
Hai nước Alpha và Beta nằm ở hai bên bờ sông Omega, Alpha nằm ở bờ bắc và có
thành phố được đánh số từ đến , Beta nằm ở bờ nam và có thành phố được đánh số từ đến (theo vị trí từ tây sang đông). Mỗi thành phố của nước này thường có quan hệ kết nghĩa với một số thành phố của nước kia. Để tăng cường tình hữu nghị, hai nước muốn xây các cây cầu bắc qua sông, mỗi cây cầu sẽ là nhịp cầu nối 2 thành phố kết nghĩa. Với yêu cầu là các cây cầu không được cắt nhau và mỗi thành phố chỉ là đầu cầu cho nhiều nhất là một cây cầu, hãy đếm số cây cầu nhiều nhất có thể xây dựng. Điều kiện: .
Lời giải:
Bài toán có thể được phát biểu lại như sau: Cho đồ thị hai phía
Ý tưởng tương tự như các bài trên, ta xét mảng
Khi đó công thức QHĐ sẽ là:
*
Nhận xét: Không mất tổng quát, giả sử thứ tự xây các cây cầu là tăng dần theo mút ở thành phố
1.4.3. Palindrome (IOI 2000)¶
Link nộp bài: SPOJ - IOIPALIN
Cho một xâu
. Ở mỗi bước, bạn An có thể chèn kí tự tùy ý vào bất kì vị trí nào trong xâu . Hãy tính số bước ít nhất cần thực hiện để biến thành xâu đối xứng. Xâu được gọi là xâu đối xứng nếu , với mọi . Điều kiện: .
Lời giải:
Cách 1: Công thức QHĐ của bài này như sau:
Gọi
Tóm lại, công thức QHĐ là:
*
Đáp số của bài toán sẽ là
Cài đặt: Ta có thể cài đặt trực tiếp thuật toán trên mà không cần quan tâm đến thứ tự tính như sau:
## include <iostream>
## include <string>
using namespace std;
const int N = 5010;
int n, d[N][N];
string s;
int calc(int i, int j)
{
// Nếu L[i, j] chưa được tính thì lưu giá trị vào d[i][j]
if (d[i][j] == -1)
{
if (i >= j)
d[i][j] = 0;
else
{
if (s[i] == s[j])
d[i][j] = calc(i + 1, j - 1);
else
d[i][j] = 1 + min(calc(i, j - 1), calc(i + 1, j));
}
}
return d[i][j];
}
int main()
{
cin >> s;
n = s.length();
s = "_" + s;
for (int i = 0; i <= n; i++)
for (int j = 0; j <= n; j++)
d[i][j] = -1;
cout << calc(1, n) << '\n';
}
Nhận xét: Đây là phương pháp đệ quy có nhớ (memoization). Độ phức tạp bộ nhớ của thuật toán là
Để ý để tính được mảng
Cách 2: Từ ý tưởng của bài xâu con chung dài nhất, ta có thuật toán sau:
- Gọi
là xâu đảo của và là xâu con chung dài nhất của và . Khi đó các kí tự của không thuộc cũng là các kí tự cần thêm vào để trở thành đối xứng. Đáp số của bài toán sẽ là , với là độ dài của .
Ví dụ: e
và c
vào
2. Xếp vali không giới hạn (Unbounded Knapsack)¶
2.1. Mô hình¶
Có
đồ vật, vật thứ có trọng lượng và giá trị . Hãy chọn ra một số các đồ vật để xếp vào vali có trọng lượng tối đa sao cho tổng giá trị của vali là lớn nhất (Chú ý mỗi vật có thể chọn nhiều lần). Điều kiện: .
Chú ý: Bài toán này khác với bài toán Xếp Vali ở phần trước ở chỗ mỗi vật không phải là duy nhất và có thể được chọn vào vali nhiều lần.
2.2. Lời giải¶
Trạng thái của bài toán phụ thuộc vào hai yếu tố: số vật đang được chọn và tổng khối lượng của chúng. Ta có thể gọi
Ta đi tìm công thức truy hồi của
Trong đó,
2.3. Code tham khảo¶
Tương tự như các ví dụ trước, ta có thể dùng mảng hai chiều
Để giảm độ phức tạp bộ nhớ xuống
## include <iostream>
## include <vector>
using namespace std;
long long n, w;
vector<long long> a, b, L, P;
int main()
{
cin >> n >> w;
a.resize(n + 1);
b.resize(n + 1);
for (int i = 1; i <= n; i++)
cin >> a[i] >> b[i];
P = L = vector<long long>(w + 1);
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= w; j++)
{
if (a[i] > j)
L[j] = P[j];
else
L[j] = max(P[j], L[j - a[i]] + b[i]);
}
P = L;
}
cout << L[w];
}
Lưu ý rằng đoạn chương trình trên mới chỉ cài đặt y nguyên công thức QHĐ chứ chưa tối ưu. Ví dụ với các
2.4. Một số bài toán khác¶
2.4.1. Đổi tiền¶
Ở đất nước Omega người ta chỉ tiêu tiền xu. Có
loại tiền xu, loại thứ có mệnh giá là đồng. Một người khách du lịch đến Omega du lịch với số tiền đồng. Ông ta muốn đổi số tiền đó ra tiền xu Omega để tiện tiêu dùng. Ông ta cũng muốn số đồng tiền sau khi đổi là ít nhất (cho túi tiền đỡ nặng khi đi đây đi đó). Bạn hãy giúp ông ta tìm cách đổi tiền. Điều kiện: . Input: Hai số và . Output: Nếu không thể đổi được, in ra . Ngược lại, in ra hai dòng: * Dòng đầu tiên in ra số đồng tiền ít nhất có thể. * Dòng thứ hai in ra số, số thứ là số đồng xu của mệnh giá thứ .
Lời giải:
Nếu xem "khối lượng" là mệnh giá và "giá trị" bằng
Ta xây dựng mảng
Kết quả của bài toán là
Cài đặt:
## include <iostream>
## include <vector>
using namespace std;
// Struct để truy vết
struct Trace
{
int coin; // Chỉ số đồng tiền được thêm vào
int i; // i và j dùng để truy vết trong bảng QHĐ
int j;
Trace(int c = 0, int row = 0, int col = 0)
: coin(c), i(row), j(col) {};
};
const int N = 1e6 + 10;
int n, m, A[N];
vector<int> L, P;
vector<vector<Trace>> d;
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> A[i];
// Quy ước inf = -1
P = vector<int>(m + 1, -1);
L.resize(m + 1);
d = vector<vector<Trace>>(n + 1, vector<Trace>(m + 1));
// Bước QHĐ
for (int i = 1; i <= n; i++)
{
L[0] = 0;
for (int j = 1; j <= m; j++)
if (A[i] > j)
{
L[j] = P[j];
d[i][j] = Trace(0, i - 1, j);
}
else
{
// L[j] = min(P[j], L[j - A[i]]);
// Nếu P[j] và L[j - A[i]] khác inf
if (P[j] != -1 && L[j - A[i]] != -1)
{
if (P[j] < L[j - A[i]] + 1)
{
L[j] = P[j];
d[i][j] = Trace(0, i - 1, j);
}
else
{
L[j] = L[j - A[i]] + 1;
d[i][j] = Trace(i, i, j - A[i]);
}
}
// Chỉ L[j - A[i]] là inf
else if (P[j] != -1)
{
L[j] = P[j];
d[i][j] = Trace(0, i - 1, j);
}
// Chỉ P[j] là inf
else if (L[j - A[i]] != -1)
{
L[j] = L[j - A[i]] + 1;
d[i][j] = Trace(i, i, j - A[i]);
}
// Cả hai số là inf
else
L[j] = -1;
}
P = L;
}
cout << L[m] << '\n';
// Truy vết
if (L[m] != -1)
{
vector<int> cnt(n + 1);
Trace t = d[n][m];
while (t.coin != 0 && t.j != 0)
{
cnt[t.coin]++;
t = d[t.i][t.j];
}
for (int i = 1; i <= n; i++)
cout << cnt[i] << ' ';
}
}
3. Nhân ma trận¶
3.1. Mô hình¶
Khi nhân một ma trận kích thước
với một ma trận , số phép nhân phải thực hiện là . Mặt khác phép nhân các ma trận có tính kết hợp, tức là: Do đó khi tính tích nhiều ma trận, ta có thể thực hiện theo các trình tự khác nhau, mỗi trình tự tính sẽ quyết định số phép nhân cần thực hiện. Cho số và ma trận , ma trận thứ có kích thước là . Hãy xác định trình tự nhân ma trận sao cho số phép nhân cần thực hiện là ít nhất. Điều kiện: . Input: Số và số . Output: Số nguyên duy nhất là số phép nhân ít nhất.
3.2. Lời giải¶
Gọi
Xét tích
Nói cách khác, tích
, trong đó nếu
3.3. Code tham khảo¶
Để tính các giá trị
Nếu tính theo thứ tự, để tính được
Đệ quy có nhớ:
## include <iostream>
using namespace std;
const int N = 310;
int d[N], L[N][N], n;
int calc(int i, int j)
{
if (L[i][j] == -1)
{
if (i == j)
L[i][j] = 0;
else
{
L[i][j] = calc(i + 1, j) + d[i - 1] * d[i] * d[j];
for (int k = i; k < j; k++)
L[i][j] = min(L[i][j], calc(i, k) + calc(k + 1, j) + d[i - 1] * d[k] * d[j]);
}
}
return L[i][j];
}
int main()
{
cin >> n;
for (int i = 0; i <= n; i++)
cin >> d[i];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
L[i][j] = -1;
cout << calc(1, n);
}
## include <iostream>
using namespace std;
const int N = 310;
int d[N], L[N][N], n;
int main()
{
cin >> n;
for (int i = 0; i <= n; i++)
cin >> d[i];
for (int dis = 1; dis < n; dis++)
for (int i = 1; i + dis <= n; i++)
{
int j = i + dis;
L[i][j] = L[i + 1][j] + d[i - 1] * d[i] * d[j];
for (int k = i; k < j; k++)
L[i][j] = min(L[i][j], L[i][k] + L[k + 1][j] + d[i - 1] * d[k] * d[j]);
}
cout << L[1][n];
}
3.4. Một số bài toán khác¶
3.4.1. Chia đa giác¶
Cho một đa giác lồi
đỉnh được đánh số từ đến theo chiều kim đồng hồ. Bằng các đường chéo không cắt nhau, ta có thể chia đa giác thành tam giác. Hãy xác định cách chia có tổng các đường chéo ngắn nhất. Điều kiện: (với là tọa độ của đỉnh thứ ). Input: Một số tự nhiên và bộ . Output: In ra tổng các đường chéo của cách chia ngắn nhất.
Lời giải:
Để đơn giản ta coi mọi đoạn thẳng nối hai đỉnh bất kì đều là “đường chéo” (nếu nối hai đỉnh trùng nhau hoặc hai đỉnh liên tiếp thì có độ dài bằng
Gọi
Từ đó, ta rút ra công thức truy hồi sau:
nếu với , nếu
Cài đặt:
## include <iostream>
## include <cmath>
using namespace std;
struct Point
{
double x;
double y;
Point(double xx = 0.0, double yy = 0.0)
: x(xx), y(yy) {};
};
double distance(const Point& a, const Point& b)
{
return sqrtl((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
const int N = 310;
int n;
double L[N][N];
Point p[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> p[i].x >> p[i].y;
for (int dis = 3; dis <= n - 1; dis++)
for (int i = 1; i + dis <= n; i++)
{
int j = i + dis;
L[i][j] = L[i + 1][j] + distance(p[i + 1], p[j]);
for (int k = i + 1; k <= j - 1; k++)
L[i][j] = min(L[i][j], L[i][k] + L[k][j] + distance(p[i], p[k]) + distance(p[k], p[j]));
}
cout << L[1][n];
}
3.4.2. Biểu thức số học¶
Cho
số thực không âm được viết thành một hàng ngang theo thứ tự đó. Giữa hai số liên tiếp có một dấu +
hoặc*
cho trước. Hãy đặt các dấu ngoặc vào biểu thức để giá trị thu được là lớn nhất. Điều kiện:.
Lời giải:
Giả sử biểu thức ban đầu là +
hoặc *
theo đề bài.
Gọi
Vậy ta có công thức truy hồi như sau:
với
4. Ghép cặp¶
4.1. Mô hình¶
Có
lọ hoa sắp thành một hàng ngang và bó hoa được đánh số thứ tự từ đến . Cần cắm bó hoa trên vào lọ sao cho hoa có số thứ tự nhỏ phải đứng trước hoa có số thứ tự lớn. Giá trị thẩm mỹ tương ứng khi cắm hoa vào lọ thứ là . Hãy tìm một cách cắm sao cho tổng giá trị thẫm mỹ là lớn nhất. Chú ý rằng mỗi bó hoa phải được cắm vào một lọ và mỗi lọ cũng chỉ cắm tối đa một bó hoa. Điều kiện: .
4.2. Lời giải¶
Nhận xét rằng bài toán nêu trên là một bài toán ghép cặp có yêu cầu về thứ tự nên ta có thể giải quyết bằng phương pháp QHĐ.
Trạng thái của bài toán phụ thuộc vào hai yếu tố: số bông hoa đang được xét và số lọ hoa đang dùng. Ta có thể gọi
- Nếu
: chỉ có một cách cắm, nên - Nếu
: không thể cắm hoa vào lọ - Nếu
: ta có thể cắm hoa thứ vào lọ hoặc các lọ trước đó. Do đó có hai trường hợp: - Nếu hoa
được cắm vào lọ : khi đó cách cắm hoa vào lọ trước đó cũng phải có tổng giá trị thẩm mỹ lớn nhất. Do đó - Không cắm hoa
vào lọ (cắm vào lọ trước ), giá trị thẫm mỹ của cách cắm là
Tóm lại, công thức truy hồi của bài toán là:
*
Đáp án của bài toán là
Nhận xét:
* Ta chỉ cần điều kiện
4.3. Code tham khảo¶
Tương tự các bài toán trước, có hai phương pháp cài đặt QHĐ, với phương pháp tính từng hàng cài đặt nhanh và tiết kiệm hơn:
vector<int> P(k + 1), L(k + 1);
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= i; j++)
{
L[j] = max(P[j - 1] + v[i][j], P[j]);
}
P = L;
}
cout << L[k];
4.4. Một số bài toán khác¶
4.4.1. Xếp phòng học¶
Một trường học có
phòng học đánh số từ đến và nhóm học sinh đánh số từ đến . Cần xếp nhóm học sinh vào các phòng học khác nhau sao cho với hai nhóm có lần lượt là phòng học của hai nhóm thì . Nói cách khác, nhóm có số hiệu lớn hơn sẽ được ưu tiên phòng có số hiệu lớn hơn. Nếu phòng học nào đó có chứa học sinh thì số ghế thừa phải được chuyển ra ngoài, nếu thiếu ghế thì phải lấy thêm. Biết số ghế có sẵn trong phòng thứ là và số học sinh nhóm thứ là Tính số lần chuyển ghế ra vào ít nhất có thể. Điều kiện: .
Lời giải:
Khi xếp nhóm
4.4.2. Mua giày (Đề QG bảng B năm 2003)¶
Có
đôi giày, đôi giày thứ có kích thước . Có người cần mua giày, người thứ cần mua đôi giày kích thước . Khi người chọn mua đôi giày thì độ lệch sẽ là . Hãy tìm cách chọn mua giày cho người trên sao cho tổng độ lệch là ít nhất. Biết rằng mỗi người chỉ mua 1 đôi giày và 1 đôi giày cũng chỉ có tối đa một người mua. Điều kiện: .
Lời giải:
Bài này khác với bài Xếp phòng học ở trên ở chỗ: người có thứ tự nhỏ hơn không nhất thiết phải mua đôi giày thứ tự nhỏ hơn.
Tuy nhiên, để đưa bài toán về dạng ghép cặp, ta có nhận xét sau:
Cho
dãy số nguyên dương sắp thứ tự và . Gọi là một hoán vị của . Khi đó:
Chứng minh:
Gọi
Xét hiệu:
Do đó ta luôn có
Trở lại bài toán gốc, nếu ta sắp xếp các đôi giày và các người mua giày theo trình tự cỡ giày tăng dần, từ nhận xét đã chứng minh ta suy ra tổng độ lệch là nhỏ nhất khi người cần cỡ giày nhỏ hơn mua chiếc giày nhỏ hơn. Đến đây, bài toán giống với bài toán gốc và có thể giải bằng phương pháp QHĐ.
Kết¶
Điều khó nhất để giải một bài toán quy hoạch động là biết rằng đó là một bài toán QHĐ và tìm được công thức/trạng thái của nó. Việc mò mẫm từ đầu không hề dễ dàng, nhưng nếu ta đưa được bài toán cần giải về một bài toán QHĐ kinh điển đã biết thì việc xử lý sẽ đơn giản hơn nhiều. Do đó, tìm hiểu mô hình, công thức và cách cài đặt những bài toán QHĐ kinh điển là một việc rất cần thiết. Hi vọng trong bài viết này, các bạn đã tìm hiểu thêm được một số bài tập kinh điển cũng như các biến thể của chúng.