Bài viết có tham khảo và bổ sung, chỉnh sửa từ TopCoder và một số nguồn khác.
Người viết: Nguyễn Anh Bảo - Đại học Bách Khoa Hà Nội
Reviewer:
Quy hoạch động (QHĐ) (Dynamic Programming) là một trong những kĩ thuật quan trọng và cơ bản nhất trong lập trình thi đấu. Bài viết này sẽ trình bày và giải thích các khái niệm liên quan đến quy hoạch động đồng thời đưa ra các ví dụ minh họa.
Để mở đầu, ta xét ví dụ sau:
Bạn An có chiếc ghế màu trắng, chiếc ghế màu đen và chiếc ghế màu đỏ. An muốn chọn ra chiếc ghế để xếp thành một hàng ngang. Do An không thích màu đỏ nên An không muốn xếp hai chiếc ghế đỏ cạnh nhau. Tính số cách xếp ghế thỏa mãn điều kiện đó.
Điều kiện: .
Lưu ý: hai cách xếp được xem là khác nhau khi tồn tại một vị trí mà hai cách có hai loại ghế khác nhau.
Bây giờ ta sẽ xây dựng thuật giải:
Gọi số cách xếp cái ghế là . Ta xét chiếc ghế thứ .
Với ý tưởng trên, ta có thể giải bài toán này như các bài toán đệ quy đơn giản. Cài đặt như sau:
// Tính số cách sắp xếp n cái ghế
int solve(int n)
{
// Trường hợp cơ bản
if (n == 1)
return 3;
if (n == 2)
return 8;
// Bước đệ quy
return 2 * solve(n - 1) + 2 * solve(n - 2);
}
Thuật toán trên có độ phức tạp lũy thừa nên chỉ áp dụng được với nhỏ , không đủ nhanh so với yêu cầu bài toán.
Thuật toán trên chạy chậm vì một số hàm solve(i)
được gọi rất nhiều lần. Ta lấy ví dụ sau:
Giả sử cần tính solve(1000)
. Khi đó cần tính solve(999)
và solve(998)
.
Để tính solve(999)
cần gọi hàm solve(998)
và solve(997)
.
Để tính solve(998)
cần gọi hàm solve(997)
và solve(996)
.
Để tính solve(997)
cần gọi hàm solve(996)
và solve(995)
.
Ta có thể biểu diễn các hàm được gọi bằng một sơ đồ như sau:
Từ sơ đồ trên ta thấy có nhiều hàm bị gọi rất nhiều lần một cách không cần thiết:
solve(998)
được gọi lầnsolve(997)
được gọi lầnsolve(996)
được gọi lầnĐể khắc phục điều này ta có thể sử dụng một mảng nhớ sao cho là giá trị của solve(i)
:
int d[100010];
int solve(int n)
{
if (n == 1)
return 3;
else if (n == 2)
return 8;
else if (d[n] != 0)
return d[n];
else
{
d[n] = 2 * f(n - 1) + 2 * f(n - 2);
return d[n];
}
}
Thuật toán trên có độ phức tạp .
Với cách tiếp cận trên, ta quan tâm đến giá trị cuối cùng , sau đó mới xem xét những giá trị bé hơn cần thiết cho tính toán.
Nhưng với phương pháp quy hoạch động, ta sẽ quan tâm đến các bài toán với tham số nhỏ hơn trước tiên.
Quy hoạch động được sử dụng khi ta tìm được công thức liên hệ giữa kết quả bài toán có đầu vào cho trước với một (hoặc một số) bài toán con tương tự nhưng có đầu vào nhỏ hơn. Khi ta biết được một số trạng thái bắt đầu của bài toán, nói cách khác - bài toán con với những đầu vào rất nhỏ, ta có thể sử dụng QHĐ để tính ra kết quả cuối cùng.
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ới tham số cho trước.
Ví dụ, trạng thái trong bài này là số cách sắp xếp chiếc ghế thỏa mãn không có hai ghế đỏ cạnh nhau.
Để giải bài toán quy hoạch động, điều quan trọng nhất là tìm ra mối liên hệ giữa một trạng thái và các trạng thái có tham số nhỏ hơn.
Gọi là cách sắp xếp chiếc ghế thành một hàng dọc. Khi đó ta có:
Công thức được gọi là công thức truy hồi.
Sau khi đã biết công thức truy hồi và tính được , , ta có thể tìm .
#include <iostream>
using namespace std;
long long n, f[100010];
int main()
{
cin >> n;
f[1] = 3;
f[2] = 8;
for (int i = 3; i <= n; i++)
f[i] = 2 * f[i - 1] + 2 * f[i - 2];
cout << f[n];
return 0;
}
Độ phức tạp của thuật toán trên là , nhưng cách thực hiện đơn giản hơn đệ quy có nhớ.
Phân tích: Từ ví dụ trên, ta thấy phương pháp QHĐ được triển khai theo các bước sau:
Ta tiếp tục với ví dụ tiếp theo:
Cho loại đồng xu và giá tiền của mỗi loại là các số nguyên , và số nguyên dương . 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), nếu không tồn tại một số đồng xu có tổng là thì in ra .
Điều kiện: và .
Ta xây dựng thuật toán QHĐ:
Trạng thái của bài toán là số đồng xu nhỏ nhất có tổng giá tiền là . Ta sẽ dùng mảng để lưu số đồng xu ít nhất có tổng giá trị là , nếu không tồn tại các đồng xu có tổng là thì gán .
Cần một công thức truy hồi để tính theo .
Để ý thấy với bất kì, nếu có một đồng xu giá trị thì ta có thể thêm đồng đó vào các đồng có tổng giá trị là . Giả sử là số đồng xu ít nhất có tổng là , khi đó có đồng xu có tổng giá trị . Nếu thì ta cập nhật , nếu thì .
Sau đây là ví dụ: Cho các đồng xu với giá tiền . Và .
Đầu tiên, ta bắt đầu từ trạng thái cơ bản nhất: .
Xét đến tổng . Có duy nhất đồng xu nhỏ hơn hoặc bằng tổng , nên ta có .
Xét đến tổng . Cũng giống như tổng trước, chỉ có đổng xu không vượt quá , suy ra .
Đến tổng . Lần này có hai đồng xu không vượt quá là và . Nếu ta chọn đồng , ta có ; nếu ta chọn đồng , ta có . Rõ ràng nên ta chọn đồng và .
Xét tiếp đến tổng tổng đến bằng cách như trên.
Đâ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) |
---|---|---|
- | ||
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int f[N], v[N], n, S;
// Gán f[i] = -1 nếu không thể tìm được một số đồng xu tổng bằng i
int main()
{
cin >> n >> S;
for (int i = 1; i <= n; i++)
cin >> v[i];
for (int i = 1; i <= S; i++)
f[i] = -1;
for (int i = 1; i <= S; i++)
for (int j = 1; j <= n; j++)
if (v[j] <= i && f[i - v[j]] != -1)
{
if (f[i] != -1)
f[i] = min(f[i], f[i - v[j]] + 1);
else
f[i] = f[i - v[j]] + 1;
}
cout << f[S];
}
Nhận xét: Đôi khi, trạng thái trong bài QHĐ chính là yêu cầu của bài toán.
Phần này giới thiệu một lớp bài toán QHĐ điển hình. Ta bắt đầu bằng bài toán sau:
Cho dãy số nguyên dương . Tìm độ dài của dãy con không giảm dài nhất của dãy.
Dãy con của một dãy là dãy số thu được bằng cách bỏ đi một số phần tử của dãy ban đầu.
Điều kiện: và .
Đầu tiên cần xác định trạng thái của bài toán.
Ta đặt là độ dài của dãy con không giảm dài nhất kết thúc ở . là trạng thái của bài toán. Ta khởi tạo ( là một dãy không giảm).
Với mà thì ta có thể thêm vào dãy không giảm kết thúc ở , do đó nếu lớn hơn giá trị hiện tại của thì ta cập nhật .
Cuối cùng để tìm được độ dài dãy con không giảm dài nhất ta tính .
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int f[N], a[N], n;
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
f[i] = 1;
for (int i = 2; i <= n; i++)
for (int j = 1; j < i; j++)
if (a[j] <= a[i])
f[i] = max(f[i], f[j] + 1);
int mx = f[1];
for (int i = 2; i <= n; i++)
mx = max(mx, f[i]);
cout << mx;
}
Ví dụ minh họa:
Nhận xét: Một số bài QHĐ có trạng thái là yêu cầu bài toán với phần tử đầu tiên của dãy số.
Bài toán tìm dãy con không giảm dài nhất là một ví dụ điển hình của phương pháp QHĐ. Một số biến thể của bài toán này tạo thành một lớp các bài toán tương tự nhau. Các bài toán đó có một số tính chất đặc trưng sau:
For
duyệt qua các phần tử trong dãy.Một số biến thể:
Bài toán giống ví dụ 3, nhưng yêu cầu in ra dãy con đó. Ta có thể làm tương tự như trên, nhưng thêm mảng truy vết lưu vị trí mà . Ta có thể cài đặt như sau:
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e3 + 10;
int f[N], a[N], d[N], n;
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
// Bước QHĐ
for (int i = 1; i <= n; i++)
{
f[i] = 1;
for (int j = 1; j < i; j++)
if (a[j] <= a[i] && f[i] < f[j] + 1)
{
f[i] = f[j] + 1;
d[i] = j;
}
}
// Tìm t là vị trí cuối cùng của dãy dài nhất
int t = 1;
for (int i = 1; i <= n; i++)
if (f[i] > f[t])
t = i;
// In ra dãy con đó
vector<int> seq;
while (t)
{
seq.push_back(a[t]);
t = d[t];
}
for (auto i = seq.rbegin(); i != seq.rend(); i++)
cout << (*i) << ' ';
}
Có cuộc họp, cuộc họp thứ bắt đầu vào thời điểm và kết thúc ở thời điểm . Do chỉ có một phòng hội thảo nên 2 cuộc họp bất kì sẽ được cùng bố trí phục vụ nếu khoảng thời gian làm việc của chúng chỉ giao nhau tại đầu mút hoặc không giao nhau. Hãy bố trí phòng họp để phục vụ được nhiều cuộc họp nhất.
Điều kiện: và với mọi .
Input: Số nguyên và dòng tiếp theo có dòng thứ là thời điểm bắt đầu và kết thúc của cuộc họp thứ .
Output: một dòng gồm số thứ tự ban đầu của các cuộc họp được bố trí, theo thứ tự thời gian.
Hướng dẫn:
Sắp xếp các cuộc họp tăng dần theo thời điểm bắt đầu . Thế thì cuộc họp sẽ bố trí được sau cuộc họp khi và chỉ khi và . Yêu cầu bố trí được nhiều cuộc họp nhất có thể đưa về việc tìm dãy các cuộc họp dài nhất thoả mãn điều kiện trên.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Meeting
{
Meeting(int aa = 1, int bb = 1, int nn = 1)
: a(aa), b(bb), num(nn)
{ };
int a; // Thời điểm bắt đầu cuộc họp
int b; // Thời điểm kết thúc cuộc họp
int num; // Số thứ tự của cuộc họp
};
const int N = 1e3 + 10;
int n, f[N], d[N];
Meeting m[N];
// Hàm so sánh để sắp xếp
bool compare(const Meeting& x, const Meeting& y)
{
return x.a < y.a || (x.a == y.a && x.b < y.b);
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
m[i].num = i;
cin >> m[i].a >> m[i].b;
}
sort(m + 1, m + n + 1, compare);
// Bước quy hoạch động
for (int i = 1; i <= n; i++)
{
f[i] = 1;
for (int j = 1; j < i; j++)
if (m[j].b <= m[i].a && f[i] < f[j] + 1)
{
f[i] = f[j] + 1;
d[i] = j;
}
}
// Truy vết
int t = 1;
for (int i = 1; i <= n; i++)
if (f[i] > f[t])
t = i;
vector<int> seq;
while (t)
{
seq.push_back(m[t].num);
t = d[t];
}
for (auto i = seq.rbegin(); i != seq.rend(); i++)
cout << (*i) << ' ';
}
Trung tâm tính toán hiệu năng cao nhận được đơn đặt hàng của khách hàng. Khách hàng muốn sử dụng máy trong khoảng thời gian từ đến và trả tiền thuê là . Hãy bố trí lịch thuê máy để tổng số tiền thu được là lớn nhất mà thời gian sử dụng máy của 2 khách hàng bất kì được phục vụ đều không giao nhau (cả trung tâm chỉ có một máy cho thuê).
Điều kiện: và với mọi .
Hướng dẫn:
Tương tự như bài toán bố trí phòng họp, nếu sắp xếp các đơn đặt hàng theo thời điểm bắt đầu, ta sẽ đưa được về bài toán tìm dãy con có tổng lớn nhất. Bài toán này là biến thể của bài toán tìm dãy con tăng dài nhất, ta có thể cài đặt bằng đoạn chương trình như sau:
struct Value
{
Value(int aa = 1, int bb = 1, int cc = 1, int nn = 1)
: a(aa), b(bb), num(nn)
{ };
int a; // Thời điểm bắt đầu thuê
int b; // Thời điểm kết thúc thuê
int c; // Tiền thuê
int num; // Số thứ tự
}
const int N = 1e3 + 10;
int n, f[N], d[N];
Value m[N];
bool compare(const Value& x, const Value& y)
{
return x.a < y.a || (x.a == y.a && x.b < y.b);
}
int main()
{
// ...
sort(m + 1, m + n + 1, compare);
// Bước quy hoạch động
for (int i = 1; i <= n; i++)
{
f[i] = m[i].c;
for (int j = 1; j < i; j++)
if (m[j].b <= m[i].a && f[i] < f[j] + m[i].c)
{
f[i] = f[j] + m[i].c;
d[i] = j;
}
}
// ... truy vết
}
Cho tam giác trên mặt phẳng. Tam giác bao tam giác nếu 3 đỉnh của tam giác đều nằm trong tam giác (có thể nằm trên cạnh). Hãy tìm dãy tam giác bao nhau có nhiều tam giác nhất.
Điều kiện: và tọa độ các đỉnh của các tam giác thuộc đoạn đến .
Hướng dẫn:
Sắp xếp các tam giác tăng dần về diện tích. Khi đó tam giác sẽ bao tam giác nếu và 3 đỉnh của nằm trong . Từ đó có thể đưa về bài toán tìm dãy “tăng” dài nhất.
Bài toán có một số biến thể khác như tìm dãy hình tam giác, hình chữ nhật… bao nhau có tổng diện tích lớn nhất.
Việc kiểm tra điểm có nằm trong tam giác không có thể dựa trên 2 phương pháp sau:
Cho dãy số nguyên gồm phần tử và các số nguyên dương . Hãy tìm dãy con đổi dấu dài nhất của dãy đó.
Dãy con của dãy là dãy thu được bằng cách xóa đi một số phần tử của .
Điều kiện: và .
Dãy con đổi dấu phải thoả mãn các điều kiện sau:
Hướng dẫn:
Gọi là số phần tử của dãy con đổi dấu có phần tử cuối cùng là và phần tử cuối cùng lớn hơn phần tử đứng trước. Tương tự, là số phần tử của dãy con đổi dấu có phần tử cuối cùng là và phần tử cuối cùng nhỏ hơn phần tử đứng trước.
Ta dễ dàng suy ra:
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int a[N], P[N], Q[N], n, U, L;
int main()
{
cin >> n >> U >> L;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
{
P[i] = 1; Q[i] = 1;
for (int j = 1; j <= i - L; j++)
{
if (a[i] - U <= a[j] && a[j] < a[i])
Q[i] = max(Q[i], P[j] + 1);
if (a[j] > a[i] && a[j] <= a[i] + U)
P[i] = max(P[i], Q[j] + 1);
}
}
int mx = Q[1];
for (int i = 1; i <= n; i++)
mx = max(mx, max(P[i], Q[i]));
cout << mx;
}
Dãy số nguyên được gọi là dãy số WAVIO nếu tồn tại số tự nhiên sao cho:
Ví dụ dãy số
1 2 3 4 5 2 1
là 1 dãy WAVIO độ dài 7. Cho dãy gồm số nguyên, hãy chỉ ra một dãy con Wavio có độ dài lớn nhất trích ra từ dãy đó.
Điều kiện: và với mọi .
Hướng dẫn:
Ta sẽ quy hoạch động theo phần tử của dãy WAVIO như sau:
Khi đó, trong các dãy WAVIO có là đỉnh thì dãy dài nhất sẽ có độ dài . Đến đây, chỉ cần tìm .
Ở mục này, chúng ta sẽ làm quen với QHĐ hai chiều, ta bắt đầu bằng ví dụ sau:
Cho một bảng ô vuông gồm hàng và cột. Kí hiệu là ô ở hàng , cột . Giả sử có quả táo. Bạn An muốn đi từ đến . Ở mỗi bước, An đi sang phải hoặc xuống dưới đúng một ô. Khi An ở ô , An có thể lấy hết các quả táo ở ô đó. Tính số quả táo nhiều nhất mà An có thể lấy được.
Điều kiện: và với mọi .
Ý tưởng:
Bài toán này cũng tương tự như các ví dụ trước.
Đầu tiên, trạng thái của bài toán chính là số quả táo nhiều nhất An có thể lấy khi đi từ ô đến ô , gọi là .
Đầu tiên ta khởi tạo .
Với mọi , để đi từ đến An có hai lựa chọn: đi qua hoặc đi qua . An sẽ chọn đường đi thu được nhiều táo nhất, do đó .
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int m, n, a[N][N];
long long f[N][N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
cin >> a[i][j];
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
f[i][j] = max(f[i - 1][j], f[i][j - 1]) + a[i][j];
cout << f[m][n];
}
AvoidRoads - 2003 TCO Semifinals 4
ChessMetrics - 2003 TCCC Round 4
QHĐ hai chiều được áp dụng nhiều trong những bài toán phức tạp hơn. Tiêu biểu là lớp bài toán xếp đồ.
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, mỗi vật một cái để xếp vào 1 vali có trọng lượng tối đa sao cho tổng giá trị của vali là lớn nhất.
Trạng thái bài toán: tổng giá trị lớn nhất của vali nếu khối lượng không vượt quá .
Nhận xét: giá trị của vali phụ thuộc vào 2 yếu tố: có bao nhiêu vật đang được xét và trọng lượng của các vật. Do đó bảng phương án sẽ là bảng 2 chiều:
Tính : vật đang xét là với trọng lượng của vali không được quá . Có 2 khả năng xảy ra:
Tóm lại ta có .
long long L[1010];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= W; j++)
if (A[i] <= j)
L[i][j] = max(L[i - 1][j - A[i]] + B[i], L[i - 1][j]);
else
L[i][j] = L[i - 1][j];
Cho dãy . Tìm một dãy con của dãy đó có tổng bằng .
Điều kiện: và .
Hướng dẫn:
Đặt nếu có thể tạo ra tổng từ một dãy con của dãy gồm các phần tử . Ngược lại thì . Nếu thì đáp án của bài toán trên là “có”.
Ta có thể tính theo công thức: nếu hoặc .
Cài đặt:
Nếu áp dụng luôn công thức trên thì ta cần dùng bảng phương án hai chiều. Ta có thể nhận xét rằng để tính dòng thứ , ta chỉ cần dòng . Bảng phương án khi đó chỉ cần 1 mảng 1 chiều và được tính như sau:
long long L[1010];
L[0] = 1;
for (int i = 1; i <= n; i++)
for (int t = S; t >= a[i]; t--)
if (L[t - a[i]] == 1)
L[t] = 1;
Dễ thấy độ phức tạp bộ nhớ của cách cài đặt trên là , độ phức tạp thời gian là , với là tổng của số.
Bonus: Hãy thử kiểm tra xem vì sao trong vòng for
thứ hai, được duyệt từ về chứ không phải từ lên .
Cho gói kẹo, gói thứ có viên. Hãy chia các gói thành 2 phần sao cho chênh lệch giữa 2 phần là ít nhất.
Điều kiện: và .
Hướng dẫn:
Gọi là tổng số kẹo của gói. Chúng ta cần tìm số lớn nhất thoả mãn:
Khi đó sẽ có cách chia với chênh lệch 2 phần là là nhỏ nhất và dãy con có tổng bằng ở trên gồm các phần tử là các gói kẹo thuộc phần thứ nhất. Phần thứ hai là các gói kẹo còn lại. Ta quy hoạch động mảng như sau: nếu tồn tại một số phần tử của dãy từ đến có tổng bằng . Khi đó:
Cuối cùng, ta cần tìm số lớn nhất không vượt quá sao cho tồn tại số nguyên dương để , hay .
#include <iostream>
using namespace std;
const int N = 310;
int n, a[N];
bool L[N][N];
int main()
{
cin >> n;
int t = 0;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
t += a[i];
}
for (int i = 0; i <= n; i++)
L[i][0] = true;
for (int i = 1; i <= n; i++)
for (int j = 1; 2 * j <= t; j++)
{
L[i][j] |= L[i - 1][j];
if (a[i] <= j)
L[i][j] |= L[i - 1][j - a[i]];
}
int mx = 0;
for (int i = 1; 2 * i <= t; i++)
if (L[n][i])
mx = i;
cout << mx << ' ' << t - mx;
}
Người đánh cá Clement bắt được con cá, khối lượng con cá thứ là , đem bán ngoài chợ. Ở chợ cá, người ta không mua cá theo từng con mà mua theo một lượng nào đó. Chẳng hạn , ...
Ví dụ: có 3 con cá, khối lượng lần lượt là: . Mua lượng sẽ phải lấy con cá thứ 2 và và thứ 3. Mua lượng thì lấy con thứ nhất. Không thể mua lượng . Nếu bạn là người đầu tiên mua cá, có bao nhiêu lượng bạn có thể chọn?
Điều kiện: , .
Hướng dẫn
Thực chất bài toán là tìm các số mà có một dãy con của dãy có tổng bằng .
Ta có thể dùng phương pháp đánh dấu của bài chia kẹo ở trên rồi đếm các giá trị mà .
Cho số tự nhiên . Ban đầu các số được đặt liên tiếp theo đúng thứ tự cách nhau bởi dấu "?":
A1 ? A2 ? ... ? AN
. Cho trước số nguyên , có cách nào thay các dấu?
bằng dấu+
hay dấu−
để được một biểu thức số học cho giá trị là không?
Hướng dẫn:
Đặt nếu có thể điền dấu vào số đầu tiên và cho kết quả bằng . Ta có công thức sau để tính :
L[1, a[1]] = 1
L[i, t] = 1
nếu L[i - 1, t + a[i]] = 1
hoặc L[i - 1, t - a[i]] = 1
.Nếu L[n, S] = 1
thì câu trả lời của bài toán là có.
Khi cài đặt, có thể dùng một mảng 2 chiều (lưu toàn bộ bảng phương án) hoặc 2 mảng một chiều (để lưu dòng và dòng ). Chú ý là chỉ số theo của các mảng phải có cả phần âm (tức là từ đến , với là tổng của số), vì trong bài này chúng ta dùng cả dấu -
nên có thể tạo ra các tổng âm.
Bài này có một biến thể là đặt dấu sao cho kết quả là một số chia hết cho . Ta có thuật giải tương tự bài toán trên bằng cách thay các phép cộng, trừ bằng các phép cộng và trừ theo modulo và dùng mảng đánh dấu với các giá trị từ 0 đến (là các số dư có thể có khi chia cho ). Đáp số của bài toán là .
Cho số nguyên dương. Hãy chia chúng thành 2 nhóm sao cho tích của tổng 2 nhóm là lớn nhất.
Hướng dẫn:
Một người có mảnh đất và dải đất. Các mảnh đất có thể coi là một tứ giác và các dải đất thì coi như một đường thẳng. Dọc theo các dải đất ông ta trồng các cây bách, dải đất thứ có cây bách. Ông ta cũng trồng các cây bách trên viền của các mảnh đất, mảnh đất thứ có cây bách. Cả ở trên các mảnh đất và dải đất, xen giữa 2 cây bách ông ta trồng một cây ôliu. Ông ta cho con trai được chọn các mảnh đất và dải đất tuỳ ý với điều kiện tổng số cây bách không vượt quá . Người con trai phải chọn thế nào để có nhiều cây ôliu (loài cây mà anh ta thích) nhất.
Hướng dẫn
Dễ thấy mảnh đất thứ có cây ôliu và dải đất thứ có cây ôliu. Coi các mảnh đất và dải đất là các “đồ vật”, đồ vật thứ có khối lượng và giá trị (nếu là mảnh đất thì , nếu là dải đất thì ). Ta cần chọn các “đồ vật”, sao cho tổng “khối lượng” của chúng không vượt và tổng “giá trị” là lớn nhất. Đây chính là bài toán xếp balô đã trình bày ở trên.
Quy hoạch động là phương pháp tự nhiên và có thể áp dụng được trong rất nhiều bài toán. 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ó, hãy thử xác định trạng thái của nó và mối liên hệ giữa các trạng thái. Đôi khi ta cần phân tích một chút để đưa bài toán về QHĐ như các ví dụ ở trên.
Chúc các bạn học tập tốt!