Tải bản đầy đủ

Chuyên Đề Quy Hoạch Động CĐ4

Chuyên đề

PHƯƠNG PHÁP QUY HOẠCH ĐỘNG
1.

Nguyên lý tối ưu của Bellman
Phương pháp quy hoạch động cùng nguyên lý tối ưu được nhà toán học Mỹ

R.Bellman đề xuất vào những năm 50 của thế kỷ 20. Phương pháp này đã được áp
dụng để giải hàng loạt bài toán thực tế trong các quá trình kỹ thuật cộng nghệ, tổ
chức sản xuất, kế hoạch hoá kinh tế…
Trong thực tế, ta thường gặp một số bài toán tối ưu loại sau: Có một đại lượng
f hình thành trong một quá trình gồm nhiều giai đoạn và ta chỉ quan tâm đến kết quả
cuối cùng là giá trị của f phải lớn nhất hoặc nhỏ nhất, ta gọi chung là giá trị tối ưu của
f. Giá trị của f phụ thuộc vào những đại lượng xuất hiện trong bài toán mà mỗi bộ giá
trị của chúng được gọi là một trạng thái của hệ thống và phụ thuộc vào cách thức đạt
được giá trị f trong từng giai đoạn mà mỗi cách tổ chức được gọi là một điều khiển.
Đại lượng f thường được gọi là hàm mục tiêu và quá trình đạt được giá trị tối ưu của f
được gọi là quá trình điều khiển tối ưu.
Bellman phát biểu nguyên lý tối ưu (cũng gọi là nguyên lý Bellman) mà ý
tưởng cơ bản là như sau: “Với mỗi quá trình điều khiển tối ưu, đối với trạng thái bắt

đầu A0, với trạng thái A trong quá trình đó, phần quá trình kể từ trạng thái A xem như
trạng thái bắt đầu cũng là tối ưu”.
Chú ý rằng nguyên lý này được thừa nhận mà không chứng minh.
Phương pháp tìm điều khiển tối ưu theo nguyên lý Bellman thường được gọi là
quy hoạch động. Thuật ngữ này nói lên thực chất của quá trình điều khiển là động:
có thể trong một số bước đầu tiên lựa chọn điều khiển tối ưu dường như không tốt
nhưng tựu chung cả quá trình lại là tốt nhất.
Hiểu một cách đơn giản hơn quy hoạch động là phương pháp giải bài toán từ
nhỏ đến lớn, việc giải – tìm phương án tối ưu của các bài toán nhỏ và lưu trữ các kết
quả này lại sẽ giúp ta có thể giải các bài toán với kích thước lớn dần đến khi đạt được
kết quả mong muốn.
1


2.

Ý tưởng và nội dung của phương pháp quy hoạch động
Xét bài toán sau:
Cho một dãy N số nguyên A1, A2,…,AN. Hãy tìm cách xoá đi một số ít nhất số

hạng để dãy còn lại là đơn điệu hay nói cách khác hãy chọn một số nhiều nhất các số
hạng sao cho dãy B gồm các số hạng đó theo trình tự xuất hiện trong dãy A là đơn
điệu.
Quá trình chọn B được điều khiển qua N giai đoạn để đạt được mục tiêu là số
lượng số hạng của dãy B là nhiều nhất, điều khiển ở giai đoạn i thể hiện việc chọn
hay không chọn Ai vào dãy B.
Giả sử dãy đã cho là 1 8 10 2 4 6 7. Nếu ta chọn lần lượt 1, 8, 10 thì chỉ chọn
được 3 số hạng nhưng nếu bỏ qua 8 và 10 thì ta chọn được 5 số hạng 1, 2, 4, 6, 7.
Khi giải một bài toán bằng cách “chia để trị” chuyển việc giải bài toán kích
thước lớn về việc giải nhiều bài toán cùng kiểu có kích thước nhỏ hơn thì thuật toán
này thường được thể hiện bằng các chương trình con đệ quy. Khi đó, trên thực tế,
nhiều kết quả trung gian phải tính nhiều lần.
Vậy ý tưởng cơ bản của quy hoạch động là : Tránh tính toán lại mọi thứ hai
lần, mà lưu giữ kết quả đã tìm kiếm được vào một bảng làm giả thiết cho việc tìm
kiếm những kết quả của trường hợp sau.
Chúng ta sẽ làm đầy dần giá trị của bảng này bởi các kết quả của những trường
hợp trước đã được giải. Kết quả cuối cùng chính là kết quả của bài toán cần giải. Nói
cách khác phương pháp quy hoạch động đã thể hiện sức mạnh của nguyên lý chia để
trị đến cao độ.
Quy hoạch động là kỹ thuật thiết kế bottom-up (từ dưới lên). Nó được bắt đầu


với những trường hợp con nhỏ nhất (thường là đơn giải nhất và giải được ngay).
Bằng cách tổ hợp các kết quả đã có (không phải tính lại) của các trường hợp con, sẽ
đạt đạt tới kết quả của trường hợp có kích thước lớn dần lên và tổng quát hơn, cho
đến khi cuối cùng đạt tới lời giải của trường hợp tổng quát nhất.
Trong một số trường hợp, khi giải một bài toán A, trước hết ta tìm họ bài toán
A(p) phụ thuộc tham số p (có thể p là một véc tơ) mà A(p0)=A với p0 là trạng thái ban
2


đầu của bài toán A. Sau đó tìm cách giải họ bài toán A(p) với tham số p bằng cách áp
dụng nguyên lý tối ưu của Bellman. Cuối cùng cho p=p0 sẽ nhận được kết quả của bài
toán A ban đầu.
3.

Các bước thực hiện
Bước 1: Lập hệ thức
Dựa vào nguyên lý tối ưu tìm cách chia quá trình giải bài toán thành từng giai

đoạn, sau đó tìm hệ thức biểu diễn tương quan quyết định của bước đang xử lý với
các bước đã xử lý trước đó. Hoặc tìm cách phân rã bài toán thành các “bài toán con”
tương tự có kích thước nhỏ hơn, tìm hệ thức nêu quan hệ giữa kết quả bài toán kích
thước đã cho với kết quả của các “bài toán con” cùng kiểu có kích thước nhỏ hơn của
nó nhằm xây dựng phương trình truy toán (dạng hàm hoặc thủ tục đệ quy).
Về một cách xây dựng phương trình truy toán:
Ta chia việc giải bài toán thành n giai đoạn. Mỗi giai đoạn i có trạng thái ban
đầu là t(i) và chịu tác động điều khiển d(i) sẽ biến thành trạng thái tiếp theo t(i+1) của
giai đoạn i+1 (i=1,2,…,n-1). Theo nguyên lý tối ưu của Bellman thì việc tối ưu giai
đoạn cuối cùng không làm ảnh hưởng đến kết quả toàn bài toán. Với trạng thái ban
đầu là t(n) sau khi làm giai đoạn n tốt nhất ta có trạng thái ban đầu của giai đoạn n-1
là t(n-1) và tác động điều khiển của giai đoạn n-1 là d(n-1), có thể tiếp tục xét đến
giai đoạn n-1. Sau khi tối ưu giai đoạn n-1 ta lại có t(n-2) và d(n-2) và lại có thể tối
ưu giai đoạn n-2 … cho đến khi các giai đoạn từ n giảm đến 1 được tối ưu thì coi như
hoàn thành bài toán. Gọi giá trị tối ưu của bài toán tính đến giai đoạn k là Fk, giá trị
tối ưu của bài toán tính riêng ở giai đoạn k là Gk thì
Fk = Fk-1 + Gk
ax {G k (t (k ), d ( k ))  Fk  1 (t (k  1))} (*)
Hay là: F1 (t (k )) m
d ( k )

Bước 2: Tổ chức dữ liệu và chương trình
Tổ chức dữ liệu sao cho đạt các yêu cầu sau:


Dữ liệu được tính toán dần theo các bước.



Dữ liệu được lưu trữ để giảm lượng tính toán lặp lại.
3


Kích thước miền nhớ dành cho lưu trữ dữ liệu càng nhỏ càng tốt, kiểu dữ



liệu được chọn phù hợp, nên chọn đơn giản dễ truy cập.
Cụ thể
 Các giá trị của Fk thường được lưu trữ trong một bảng (mảng một chiều
hoặc hai, ba, v.v… chiều).
 Cần lưu ý khởi trị các giá trị ban đầu của bảng cho thích hợp, đó là các kết
quả của các bài toán con có kích cỡ nhỏ nhất của bài toán đang giải:
F1 (t (1)) m ax {G 1 (t (1), d (1))  F0 (t (0))}
d (1)

 Dựa vào công thức, phương trình truy toán (*) và các giá trị đã có trong
bảng để tìm dần các giá trị còn lại của bảng.
 Ngoài ra còn cần mảng lưu trữ nghiệm tương ứng với các giá trị tối ưu
trong từng gian đoạn.
 Dựa vào bảng lưu trữ nghiệm và bảng giá trị tối ưu trong từng giai đoạn đã
xây dựng, tìm ra kết quả bài toán.
Bước 3: Làm tốt
Làm tốt thuật toán bằng cách thu gọn hệ thức (*) và giảm kích thước miền nhớ.
Thường tìm cách dùng mảng một chiều thay cho mảng hai chiều nếu giá trị một dòng
(hoặc cột) của mảng hai chiều chỉ phụ thuộc một dòng (hoặc cột) kề trước.
Trong một số trường hợp có thể thay mảng hai chiều với các giá trị phần tử chỉ
nhận giá trị 0, 1 bởi mảng hai chiều mới bằng cách dùng kỹ thuật quản lý bit.
4.

Ví dụ minh họa
Cho số tự nhiên n ≤ 100. Hãy cho biết có bao nhiêu cách phân tích số n thành

tổng của dãy các số nguyên dương, các cách phân tích là hoán vị của nhau chỉ tính
là một cách.
n = 5 có 7 cách phân tích:
1. 5 = 1 + 1 + 1 + 1 + 1
2. 5 = 1 + 1 + 1 + 2
3. 5 = 1 + 1 + 3
4. 5 = 1 + 2 + 2
4


5. 5 = 1 + 4
6. 5 = 2 + 3
7. 5 = 5
(Lưu ý: n = 0 vẫn coi là có 1 cách phân tích thành tổng các số nguyên dương (0 là
tổng của dãy rỗng)

5


Bước 1: Lập hệ thức
Nhận xét:
Nếu gọi F[m, v] là số cách phân tích số v thành tổng các số nguyên dương ≤ m.
Khi đó: Các cách phân tích số v thành tổng các số nguyên dương ≤ m có thể chia làm
hai loại:
-

Loại 1: Không chứa số m trong phép phân tích, khi đó số cách phân tích loại này
chính là số cách phân tích số v thành tổng các số nguyên dương < m, tức là số
cách phân tích số v thành tổng các số nguyên dương ≤ m - 1 và bằng F[m - 1, v].

-

Loại 2: Có chứa ít nhất một số m trong phép phân tích. Khi đó nếu trong các cách
phân tích loại này ta bỏ đi số m đó thì ta sẽ được các cách phân tích số v - m
thành tổng các số nguyên dương ≤ m (Lưu ý: điều này chỉ đúng khi không tính
lặp lại các hoán vị của một cách). Có nghĩa là về mặt số lượng, số các cách phân
tích loại này bằng F[m, v - m]
Trong trường hợp m > v thì rõ ràng chỉ có các cách phân tích loại 1, còn trong

trường hợp m ≤ v thì sẽ có cả các cách phân tích loại 1 và loại 2. Vì thế:

F[m 1, v]; if m > v

F[m, v]= �
F[m-1,v]+F[m,v-m]; if m �v

Bước 2: Tổ chức dữ liệu và chương trình
Ta có công thức xây dựng F[m, v] từ F[m - 1, v] và F[m, v - m]. Công thức này có
tên gọi là công thức truy hồi đưa việc tính F[m, v] về việc tính các F[m', v'] với dữ
liệu nhỏ hơn. Tất nhiên cuối cùng ta sẽ quan tâm đến F[n, n]: Số các cách phân tích n
thành tổng các số nguyên dương ≤ n.
Ví dụ với n = 5, bảng F sẽ là:
F
0
1
2
3
4
5
m

0
1
1
1
1
1
1

1
0
1
1
1
1
1

2
0
1
2
2
2
2

3
0
1
2
3
3
3

4
0
1
3
4
5
5

5 V
0
1
3
5
6
7

Nhìn vào bảng F, ta thấy rằng F[m, v] được tính bằng tổng của:
6


Một phần tử ở hàng trên: F[m - 1, v] và một phần tử ở cùng hàng, bên trái: F[m, v m].
Cài đặt:
program Analysis_Counting;
const
max = 100;
var
F: array[0..max, 0..max] of Integer;
n, m, v: Integer;
begin
Write('n = '); ReadLn(n);
FillChar(F[0], SizeOf(F[0]), 0);
F[0, 0] := 1;
for m := 1 to n do
for v := 0 to n do
if v < m then F[m, v] := F[m - 1, v]
else F[m, v] := F[m - 1, v] + F[m, v - m];
WriteLn(F[n, n], ' Analyses');
end.

Bước 3: Làm tốt
Cải tiến dùng 2 mảng 1 chiều
Cách làm trên có thể tóm tắt lại như sau: Khởi tạo dòng 0 của bảng, sau đó dùng
dòng 0 tính dòng 1, dùng dòng 1 tính dòng 2 v.v… tới khi tính được hết dòng n. Có
thể nhận thấy rằng khi đã tính xong dòng thứ k thì việc lưu trữ các dòng từ dòng 0 tới
dòng k - 1 là không cần thiết bởi vì việc tính dòng k + 1 chỉ phụ thuộc các giá trị lưu
trữ trên dòng k. Vậy ta có thể dùng hai mảng một chiều: Mảng Current lưu dòng hiện
thời đang xét của bảng và mảng Next lưu dòng kế tiếp, đầu tiên mảng Current được
gán các giá trị tương ứng trên dòng 0. Sau đó dùng mảng Current tính mảng Next,
mảng Next sau khi tính sẽ mang các giá trị tương ứng trên dòng 1. Rồi lại gán mảng
Current := Next và tiếp tục dùng mảng Current tính mảng Next, mảng Next sẽ gồm
các giá trị tương ứng trên dòng 2 v.v… Vậy ta có cài đặt cải tiến sau:
program Analysis_Counting_2;
const max = 100;
var
Current, Next: array[0..max] of Integer;
n, m, v: Integer;
begin
Write('n = '); ReadLn(n);
FillChar(Current, SizeOf(Current), 0);
Current[0] := 1;
for m := 1 to n do

7


begin
for v := 0 to n do
if v < m then Next[v] := Current[v]
else Next[v] := Current[v] + Next[v - m];
Current := Next;
end;
WriteLn(Current[n], ' Analyses');
end.

5. Một số bài toán tối ưu giải bằng phương pháp quy hoạch động
Bài toán 1: Bài toán cái túi
Trong siêu thị có n gói hàng (n ≤ 100), gói hàng thứ i có trọng lượng là W[i] ≤
100 và trị giá V[i] ≤ 100. Một tên trộm đột nhập vào siêu thị, tên trộm mang theo một
cái túi có thể mang được tối đa trọng lượng M (M ≤ 100). Hỏi tên trộm sẽ lấy đi
những gói hàng nào để được tổng giá trị lớn nhất.
Input: file văn bản BAG.INP
-

Dòng 1: Chứa hai số n, M cách nhau ít nhất một dấu cách

-

n dòng tiếp theo, dòng thứ i chứa hai số nguyên dương W[i], V[i] cách
nhau ít nhất một dấu cách

Output: file văn bản BAG.OUT
-

Dòng 1: Ghi giá trị lớn nhất tên trộm có thể lấy

-

Dòng 2: Ghi chỉ số những gói bị lấy
BAG.INP BAG.OUT
5 11
11
33

521

44
54
9 10
44
Bài giải:
Nếu gọi F[i, j] là giá trị lớn nhất có thể có bằng cách chọn trong các gói {1, 2,
…, i} với giới hạn trọng lượng j. Thì giá trị lớn nhất khi được chọn trong số n gói với
giới hạn trọng lượng M chính là F[n, M].
Công thức truy hồi tính F[i, j].
Với giới hạn trọng lượng j, việc chọn tối ưu trong số các gói {1, 2, …, i - 1, i}
để có giá trị lớn nhất sẽ có hai khả năng:
8


o Nếu không chọn gói thứ i thì F[i, j] là giá trị lớn nhất có thể bằng cách chọn
trong số các gói {1, 2, …, i - 1} với giới hạn trọng lượng là j. Tức là F[i, j] =
F[i - 1, j]
o Nếu có chọn gói thứ i (tất nhiên chỉ xét tới trường hợp này khi mà W[i] ≤ j) thì
F[i, j] bằng giá trị gói thứ i là V[i] cộng với giá trị lớn nhất có thể có được bằng
cách chọn trong số các gói {1, 2, …, i - 1} với giới hạn trọng lượng j - W[i].
Tức là về mặt giá trị thu được: F[i, j] = V[i] + F[i - 1, j - W[i]]
Vì theo cách xây dựng F[i, j] là giá trị lớn nhất có thể, nên F[i, j] sẽ là Max trong 2
giá trị thu được ở trên.
Cơ sở quy hoạch động:
Dễ thấy F[0, j] = giá trị lớn nhất có thể bằng cách chọn trong số 0 gói = 0.
Tính bảng phương án:
Bảng phương án F gồm n + 1 dòng, M + 1 cột, trước tiên được điền cơ sở quy
hoạch động: Dòng 0 gồm toàn số 0. Sử dụng công thức truy hồi, dùng dòng 0 tính
dòng 1, dùng dòng 1 tính dòng 2, v.v… đến khi tính hết dòng n.
Truy vết
Tính xong bảng phương án thì ta quan tâm đến F[n, M] đó chính là giá trị lớn
nhất thu được khi chọn trong cả n gói với giới hạn trọng lượng M. Nếu F[n, M] = F[n
- 1, M] thì tức là không chọn gói thứ n, ta truy tiếp F[n - 1, M]. Còn nếu F[n, M] ≠
F[n - 1, M] thì ta thông báo rằng phép chọn tối ưu có chọn gói thứ n và truy tiếp F[n 1, M - W[n]]. Cứ tiếp tục cho tới khi truy lên tới hàng 0 của bảng phương án.
program Bag;
const
InputFile = 'BAG.INP';
OutputFile = 'BAG.OUT';
max = 100;
var
W, V: Array[1..max] of Integer;
F: array[0..max, 0..max] of Integer;
n, M: Integer;
procedure Enter;
var
i: Integer;
fi: Text;
begin
Assign(fi, InputFile); Reset(fi);
ReadLn(fi, n, M);
for i := 1 to n do ReadLn(fi, W[i], V[i]);

9


Close(fi);
end;
procedure Optimize;
var
i, j: Integer;
begin
FillChar(F[0], SizeOf(F[0]), 0);
for i := 1 to n do
for j := 0 to M do
begin {Tính F[i, j]}
F[i, j] := F[i - 1, j];
if (j >= W[i]) and (F[i, j] < F[i - 1, j - W[i]] +
V[i]) then
F[i, j] := F[i - 1, j - W[i]] + V[i];
end;
end;
procedure Trace;
var
fo: Text;
begin
Assign(fo, OutputFile); Rewrite(fo);
WriteLn(fo, F[n, M]);
while n <> 0 do
begin
if F[n, M] <> F[n - 1, M] then
begin
Write(fo, n, ' ');
M := M - W[n];
end;
Dec(n);
end;
Close(fo);
end;
begin
Enter;
Optimize;
Trace;
end.

Bài toán 2: Chia thưởng
Cần chia hết m phần thưởng cho n học sinh sắp theo thứ tự từ giỏi trở xuống sao
cho mỗi bạn không nhận ít phần thưởng hơn bạn xếp sau mình.
1  m, n  70.
Hãy tính số cách chia.
Thí dụ, với số phần thưởng m = 7, và số học sinh n = 4 sẽ có 11 cách chia 7 phần
thưởng cho 4 học sinh theo yêu cầu của đầu bài. Đó là:
10


Phương án    
1
2
3
4
5
6
7
8
9
10
11

7
6
5
5
4
4
3
3
4
3
2

0
1
2
1
3
2
3
2
1
2
2

0
0
0
1
0
1
1
2
1
1
2

0
0
0
0
0
0
0
0
1
1
1

Bài giải
Lập hệ thức
Gọi Chia(i, j) là số cách chia i phần thưởng cho j học sinh, ta thấy:
- Nếu không có học sinh nào (j = 0) thì không có cách chia nào (Chia = 0).
- Nếu không có phần thưởng nào (i = 0) thì chỉ có một cách chia (Chia(0,j) = 1

- mỗi học sinh nhận 0 phần thưởng). Ta cũng quy ước Chia(0, 0) = 1.
- Nếu số phần thưởng ít hơn số học sinh (i < j) thì trong mọi phương án chia,

từ học sinh thứ i + 1 trở đi sẽ không được nhận phần thưởng nào:
Chia(i, j) = Chia(i, i) nếu i < j.
Ta xét tất cả các phương án chia trong trường hợp i  j. Ta tách các phương án
chia thành hai nhóm không giao nhau dựa trên số phần thưởng mà học sinh đứng cuối
bảng thành tích, học sinh thứ j, được nhận:
- Nhóm thứ nhất gồm các phương án trong đó học sinh thứ j không được nhận

thưởng, tức là i phần thưởng chỉ chia cho j - 1 học sinh và do đó, số cách
chia, tức là số phần tử của nhóm này sẽ là: Chia(i, j - 1).
- Nhóm thứ hai gồm các phương án trong đó học sinh thứ j cũng được nhận

thưởng. Khi đó, do học sinh đứng cuối bảng thành tích được nhận thưởng thì
mọi học sinh khác cũng sẽ có thưởng. Do ai cũng được thưởng nên ta bớt
của mỗi người một phần thưởng (để họ lĩnh sau), số phần thưởng còn lại (i j) sẽ được chia cho j học sinh. Số cách chia khi đó sẽ là Chia(i - j, j).
11


Tổng số cách chia cho trường hợp i  j sẽ là tổng số phần tử của hai nhóm, ta có:
Chia(i, j) = Chia(i, j - 1) + Chia(i - j, j).
Tổng hợp lại ta có:

Điều kiện
i: số phần thưởng
j: số học sinh
j=0
i = 0 and j  0
iij

Chia(i, j)
Chia(i, j) = 0
Chia(i, j) = 1
Chia(i, j) = Chia(i, i)
Chia(i, j) = Chia(i, j – 1) + Chia(i – j,

j)
Các tính chất của hàm Chia(i, j)
Chia i phần thưởng cho j học sinh
Tổ chức dữ liệu và chương trình
Ta có phương án đầu tiên của giải thuật Chia như sau:
Phương án đệ quy. Hàm Chia(i,j) tính số cách chia i phần thưởng cho j học sinh
function Chia(i,j: integer):longint;
begin
if j = 0 then Chia := 0
else {j > 0 }
if i = 0 then {i = 0; j > 0 }
Chia := 1
else {i,j > 0 }
if i < j then {0 < i < j }
Chia := Chia(i,i)
else {i >= j > 0 }
Chia := Chia(i,j-1)+Chia(i-j,j);
end;

Phương án này chạy chậm vì phát sinh ra quá nhiều lần gọi hàm trùng lặp. Bảng
dưới đây liệt kê số lần gọi hàm Chia khi giải bài toán chia thưởng với bảy phần
thưởng (m = 7) và 4 học sinh (n = 4). Thí dụ, hàm Chia(1,1) sẽ được gọi 9 lần,…
Tổng số lần gọi hàm Chia là 79. 79 lần gọi hàm để sinh ra kết quả 11 là quá tốn kém.









12




0

9

1

1

0



9

9

2

1

0



6

6

1

0

0



5

5

2

1

1



3

3

1

1

0



2

2

1

0

0



1

1

0

0

0



1

1

1

1

1

Số lần gọi hàm Chia cục
bộ
khi tính hàm Chia(7,4)
Làm tốt
Phương án 1 khá dễ triển khai nhưng chương trình sẽ chạy rất lâu. Diễn tả đệ quy
thường trong sáng, nhàn tản, nhưng khi thực hiện sẽ sinh ra hiện tượng gọi lặp lại những
hàm đệ quy. Cải tiến là tránh những lần gọi lặp như vậy. Muốn thế chúng ta tính sẵn các
giá trị của hàm theo các trị của đầu vào khác nhau và điền vào một mảng hai chiều cc.
Mảng cc được mô tả như sau:
const
MN = 70;{ gioi han tren cua m va n }

j-1

var cc: array[0..MN,0..MN] of longint;
Ta quy ước cc[i, j] chứa số cách chia i phần thưởng
cho j học sinh.
Theo phân tích của phương án 1, ta có:

i-j
...
i

[i,j1]

j
[i-j,j]
...
[i,j]

 cc[0, 0] = 1; cc[i, 0] = 0, với i:=1..m.
 cc[i, j] = cc[i, i], nếu i < j
 cc[i, j] = cc[i, j-1]+cc[i-j, j], nếu i  j.
Từ đó ta suy ra quy trình điền trị vào bảng cc như sau:
 Khởi trị
 cc[0,0 ]:= 1;
 với i := 1..m: cc[i,0] := 0;
 Điền bảng: Lần lượt điền theo từng cột j:= 1..n. Tại mỗi cột j ta đặt:
13


 với i := 0..j-1: cc[i,j] := cc[i,i];
 với i := j..m: cc[i,j] := cc[i,j-1]+cc[i-j,j];
Nhận kết quả: Sau khi điền bảng, giá trị cc[m, n] chính là kết quả cần tìm.
Phương án dùng mảng 2 chiều:
function Chia2(m,n: integer):longint;
var i,j: integer;
begin
cc[0,0] := 1;
for i := 1 to m do cc[i,0] := 0;
for j := 1 to n do
begin
for i := 0 to j-1 do cc[i,j] := cc[i,i];
for i := j to m do
cc[i,j] := cc[i,j-1]+cc[i-j,j];
end;
Chia2 := cc[m,n];
end;

Bài toán 3: Phép nhân tổ hợp dãy ma trận
Với ma trận A={a[i, j]} kích thước p×q và ma trận B={b[i, j]} kích thước q×r.
Người ta có phép nhân hai ma trận đó để được ma trận C={c[i, j]} kích thước p×r.
Mỗi phần tử của ma trận C được tính theo công thức:
q

C  i, j   k 1 a[i, j ].b[k , j ],1 i  p,1  j r
Ví dụ:
A là ma trận kích thước 3 4, B là ma trận kích thước 4 5 thì C sẽ là ma trận kích
thước 3 5
1
1 2 3 4  

 0
 5 6 7 8  
 9 10 11 12   3

 1


0 2 4 0
  14 6 9 36 9 

1 0 5 1 

34
14
25
100
21


0 1 6 1 
  54 22 41 164 33 
1 1 1 1 

Để thực hiện phép nhân hai ma trận A(p×q) và B(q×r) ta có thể làm như đoạn chương
trình sau:
for i := 1 to p do
for j := 1 to r do
begin
c[i, j] := 0;
for k := 1 to q do c[i, j] := c[i, j] + a[i, k] * b[k, j];
14


end;
Phí tổn để thực hiện phép nhân ma trận có thể đánh giá qua số lần thực hiện
phép nhân số học, với giải thuật nhân hai ma trận kể trên, để nhân ma trận A cấp pxq
với ma trận B cấp qxr ta cần thực hiện p.q.r phép nhân số học. Phép nhân ma trận
không có tính chất giao hoán nhưng có tính chất kết hợp (A.B).C = A.(B.C)
Vậy nếu A là ma trận cấp 3x4, B là ma trận cấp 4x10 và C là ma trận cấp 10x15 thì:
- Để tính (A.B).C, phép tính (A.B) cho ma trận kích thước 3x10 sau 3.4.10=120
phép nhân số, sau đó nhân tiếp với C được ma trận kết quả kích thước 3x15 sau
3.10.15=450 phép nhân số. Vậy tổng số phép nhân số học phải thực hiện sẽ là
570.
- Để tính A.(B.C), phép tính (B.C) cho ma trận kích thước 4x15 sau 4.10.15=600
phép nhân số, lấy A nhân với ma trận này được ma trận kết quả kích thước
3x15 sau 3.4.15=180 phép nhân số. Vậy tổng số phép nhân số học phải thực
hiện sẽ là 780.
Vậy thì trình tự thực hiện có ảnh hưởng lớn tới chi phí. Vấn đề đặt ra là tính số phí
tổn ít nhất khi thực hiện phép nhân một dãy các ma trận:
n

 m[i] m[1].m[2]......m[n]
i 1

Với :
m[1] là ma trận kích thước a[1] x a[2]
m[2] là ma trận kích thước a[2] x a[3]

m[n] là ma trận kích thước a[n] x a[n+1]
Dữ liệu: file văn bản MULTMAT.INP
- Dòng 1: Chứa số nguyên dương n ≤ 100
- Dòng 2: Chứa n + 1 số nguyên dương a[1], a[2], …, a[n+1] (∀i: 1 ≤ a[i] ≤ 100)
cách nhau ít nhất một dấu cách
Kết quả: file văn bản MULTMAT.OUT
- Dòng 1: Ghi số phép nhân số học tối thiểu cần thực hiện

15


- Dòng 2: Ghi biểu thức kết hợp tối ưu của phép nhân dãy ma
trận
MULTMAT.INP
MULTMAT.OUT
6
Number of numerical multiplications: 31
3231223
((m[1].(m[2].m[3])).((m[4].m[5]).m[6]))
Bài giải:
Trước hết, nếu dãy chỉ có một ma trận thì chi phí bằng 0, tiếp theo ta nhận thấy
chi phí để nhân một cặp ma trận có thể tính được ngay. Vậy có thể ghi nhận được chi
phí cho phép nhân hai ma trận liên tiếp bất kỳ trong dãy. Sử dụng những thông tin đã
ghi nhận để tối ưu hoá phí tổn nhân những bộ ba ma trận liên tiếp … Cứ tiếp tục như
vậy cho tới khi ta tính được phí tổn nhân n ma trận liên tiếp.
Công thức truy hồi
Gọi f[i, j] là số phép nhân số học tối thiểu cần thực hiện để nhân đoạn ma trận liên
tiếp:
j

 m[t ] m[i].m[i  1].m[i  2]......m[ j ]
t i

Thì khi đó f[i, i] = 0 với  i.
j

Để tính

m[t ] có thể có nhiều cách kết hợp:
t i

j


 k
  j
m[t ]  m[u ]   m[v]  k : i k
t i
 u i
  vk 1

Với một cách kết hợp (phụ thuộc vào cách chọn vị trí k), chi phí tối thiểu phải thực
k

hiện bằng: f[i, k] (là chi phí tối thiểu tính

m[u ] ) cộng với f[k+1, j] (là chi phí tối
u i

j

thiểu tính

m[v]

cộng với a[j].a[k+1].a[j+1] (là chi phí thực hiện phép nhân cuối

v k 1

k

cùng giữa ma trận m[u ] và ma trận
u i

j

m[v]

v k 1

16


Từ đó suy ra: do có nhiều cách kết hợp, mà ta cần chọn cách kết hợp để có chi phí ít
nhất nên ta sẽ cực tiểu hoá f[i, j] theo công thức:

f [i, j ] min ( f [i, k ]  f [k  1, j ]  a[i ].a[k  1].a[ j  1])
i k  j

Tính bảng phương án
Bảng phương án F là bảng hai chiều, nhìn vào công thức truy hồi, ta thấy f[i, j]
chỉ được tính khi mà f[i, k] cũng như f[k + 1, j] đều đã biết (  k: i ≤ k < j). Tức là ban
đầu ta điền cơ sở quy hoạch động vào đường chéo chính của bảng(  i: f[i, i] := 0), từ
đó tính các giá trị thuộc đường chéo nằm phía trên (tính các f[i, i + 1]), rồi lại tính
các giá trị thuộc đường chéo nằm phía trên nữa (các f[i, i + 2]) … Đến khi tính được
f[1, n] thì dừng lại
Tìm cách kết hợp tối ưu
Tại mỗi bước tính f[i, j], ta ghi nhận lại Tr[i, j] là điểm k mà cách tính:
j


 k
  j


m
[
t
]

m
[
u
]

m
[
v
]




 
t i
 u i
  vk 1

cần số phép nhân số học ít nhất trên tất cả các cách chọn k. Sau đó, muốn in ra phép
kết hợp tối ưu để nhân

j

tr [ i , j ]

t i

u i

m[t ] , ta sẽ in ra cách kết hợp tối ưu để nhân  m[u ] và
j

cách kết hợp tối ưu để nhân

 m[v] (có kèm theo dấu đóng mở ngoặc) đồng thời

v tr [ i , j ]1

viết thêm dấu “.” vào giữa hai biểu thức đó.
program Matrix_Multiplications;
const
InputFile = 'MULTMAT.INP';
OutputFile = 'MULTMAT.OUT';
max = 100;
var
a: array[1..max + 1] of Integer;
f: array[1..max, 1..max] of Integer;
tr: array[1..max, 1..max] of Integer;
n: Integer;
fo: Text;
procedure Enter;
var
i: Integer;
fi: Text;

17


begin
Assign(fi, InputFile); Reset(fi);
ReadLn(fi, n);
for i := 1 to n + 1 do Read(fi, a[i]);
Close(fi);
end;
procedure Optimize;
var
i, j, k, len: Integer;
x, p, q, r: Integer;
begin
for i := 1 to n do
for j := i to n do
if i = j then f[i, j] := 0
else f[i, j] := High(Integer);
for len := 2 to n do
for i := 1 to n - len + 1 do
begin
j := i + len - 1;
for k := i to j - 1 do
begin
p := a[i];q:= a[k + 1]; r := a[j + 1];
x := f[i, k] + f[k + 1, j] + p * q * r;
if x < f[i, j] then
begin
f[i, j] := x;
tr[i, j] := k;
end;
end;
end;
end;
procedure Trace(i, j: Integer);
var
k: Integer;
begin
if i = j then Write(fo, 'm[', i, ']')
else
begin
Write(fo, '(');
k := tr[i, j];
Trace(i, k);
Write(fo, '.');
Trace(k + 1, j);
Write(fo, ')');
end;
end;
begin
Enter;
Optimize;
Assign(fo, OutputFile); Rewrite(fo);

18


WriteLn(fo, 'Number of numerical multiplications: ', f[1,
n]);
end.

Trace(1, n);
Close(fo);

Bài toán 4: Palindrome
Dãy kí tự s được gọi là đối xứng (palindrome) nếu các phần tử cách đều đầu
và cuối giống nhau. Cho dãy s tạo bởi n kí tự gồm các chữ cái hoa và thường phân
biệt và các chữ số. Hãy cho biết cần xoá đi từ s ít nhất là bao nhiêu kí tự để thu được
một dãy đối xứng. Giả thiết rằng sau khi xoá bớt một số kí tự từ s thì các kí tự còn lại
sẽ tự động xích lại sát nhau.
Dữ liệu vào ghi trong tệp văn bản PALIN.INP với cấu trúc như sau:
 Dòng đầu tiên là giá trị n, 1  n  1000.
 Dòng thứ hai là n kí tự của dãy viết liền nhau.
Kết quả ghi trong tệp văn bản PALIN.OUT: số lượng kí tự cần xóa.
PALIN.INP PALIN.OUT
9
4
baeadbadb
Thí dụ, với dãy s gồm 9 kí tự, s = 'baeadbadb' thì cần xoá ít nhất 4 kí tự, chẳng
hạn, các kí tự thứ 5, 7, 8 và 9 sẽ thu được dãy đối xứng chiều dài 5 là baeab:
baeadbadb  baeab
Dĩ nhiên là có nhiều cách xoá. Thí dụ, có thể xoá các kí tự thứ 2, 3, 4 và 6 từ dãy s
để thu được dãy con đối xứng khác là bdadb với cùng chiều dài 5:
baeadbadb  bdadb
Tuy nhiên đáp số là số ít nhất các kí tự cần loại bỏ khỏi s thì là duy nhất và bằng 4.
Bài giải
Với một nhận xét nhỏ ta có thể phát hiện ra rằng chỉ cần dùng một mảng một
chiều kích thước n và một vài biến đơn là đủ.
Gọi dãy dữ liệu vào là s. Ta tìm chiều dài của dãy con đối xứng v dài nhất trích
từ s. Khi đó số kí tự cần xoá từ s sẽ là t = length(s) - length(v). Dãy con ở đây được
hiểu là dãy thu được từ s bằng cách xoá đi một số phần tử trong s. Thí dụ với dãy
19


s = baeadbadb thì dãy con đối xứng dài nhất của s sẽ là baeab hoặc bdadb,… Các dãy
này đều có chiều dài 5.
Lập hệ thức: Gọi p(i, j) là chiều dài của dãy con dài nhất thu được khi giải bài
toán với dữ liệu vào là đoạn s[i..j]. Khi đó p(1, n) là chiều dài của dãy con đối xứng
dài nhất trong dãy n kí tự s[1..n] và do đó số kí tự cần loại bỏ khỏi dãy s[1..n] sẽ là
n-p(1,n)
Đó chính là đáp số của bài toán.
Ta liệt kê một số tính chất quan trọng của hàm hai biến p(i, j). Ta có:
- Nếu i > j, tức là chỉ số đầu trái lớn hơn chỉ số đầu phải, ta quy ước đặt p(i, j) = 0.
- Nếu i = j thì p(i, i) = 1 vì dãy khảo sát chỉ chứa đúng 1 kí tự nên nó là đối xứng.
- Nếu i < j và s[i] = s[j] thì p(i, j) = p(i + 1, j – 1) + 2. Vì hai kí tự đầu và cuối dãy

s[i,j] giống nhau nên chỉ cần xác định chiều dài của dãy con đối xứng dài nhất
trong đoạn giữa là s[i + 1, j – 1] rồi cộng thêm 2 đơn vị ứng với hai kí tự đầu và
cuối dãy là được.
- Nếu i < j và s[i]  s[j], tức là hai kí tự đầu và cuối của dãy con s[i..j] là khác nhau

thì ta khảo sát hai dãy con là s[i..(j – 1)] và s[(i + 1)..j] để lấy chiều dài của dãy con
đối xứng dài nhất trong hai dãy này làm kết quả:
p(i,j) = max(p(i,j-1),p(i+1,j))
Vấn đề đặt ra là cần tính p(1, n). Mà muốn tính được p(1, n) ta phải tính được các
p(i, j) với mọi i, j = 1..n.
Phương án đệ quy
Phương án đệ quy dưới đây như mô tả trong hàm nguyên rec(i, j) tính trực tiếp giá
trị p(i, j) theo các tính chất đã liệt kê. Đáp số cho bài toán khi đó sẽ là n-rec(1,n)
function rec(i,j: integer): integer;
begin
if i > j then rec := 0
else if i = j then rec := 1
else {i < j}
if s[i] = s[j]
then rec := rec(i+1,j-1)+2
else {i < j & s[i]  s[j]}
rec := max(rec(i,j-1),rec(i+1,j));
end;

Dùng một mảng hai chiều

20


Gọi đệ quy sẽ phát sinh các lời gọi hàm trùng lặp. Khắc phục điều này bằng
cách sử dụng một mảng hai chiều để tính trước các giá trị của hàm p(i, j), mỗi giá trị
được tính tối đa một lần. Nếu dùng một mảng hai chiều, thí dụ mảng p[0..n, 0..n] thì
giá trị của p[i, j] sẽ được điền lần lượt theo từng cột, từ cột thứ 1 đến cột thứ n. Tại
mỗi cột ta điền từ dưới lên trên. Ta lưu ý:
- Phần tử tại cột i, dòng j là giá trị p[i, j] chính là chiều dài của dãy con đối xứng

dài nhất khi khảo sát dãy con s[i..j].
- Với các trị i > j, ta quy định p[i, j] = 0. Như vậy nửa tam giác dưới của ma trận

p sẽ chứa toàn 0.
- Nếu i = j thì p[i, j] = 1. Như vậy, mọi trị trên đường chéo chính của ma trận p sẽ

là 1.
- Với các ô còn lại, toạ độ (i, j) sẽ thoả điều kiện i < j, nên p[i, j] sẽ được tính như

sau:
if s[i] = s[j] then p[i,j] = p[i+1,j-1]+2
else p[i,j] :=

max(p[i,j-1],p[i+1,j]);

Ta thực hiện điền một vài giá trị cho bảng trên để rút ra quy luật.
Hãy bắt đầu với cột 1: p[1, 1] = 0;
Sau đó đến cột 2:
p[2, 2] = 1; p[1, 2] = max(p[1, 1], p[2, 2]) = 1, vì s[1]  s[2].
Rồi đến cột 3:
p[3,3]=1; p[2,3] = max(p[2, 2], p[3, 3]) = 1, vì s[2]  s[3];
p[1,3] = max(p[1,2], p[2,3]) = 1, vì s[1]  s[3],…
Dùng hai mảng một chiều
Ta sẽ không theo đuổi phương án dùng mảng hai chiều mà hãy căn cứ vào quy
luật điền mảng hai chiều để vận dụng cho hai mảng một chiều là v[0..(n + 1)] và d[0..
(n + 1)]. Theo kinh nghiệm, ta nên khai báo kích thước mảng rộng hơn chừng hai
phần tử để sử dụng các phần tử này như những lính canh chứa các giá trị khởi đầu
phục vụ cho các trường hợp chỉ số i, j nhận các giá trị 0 hoặc n + 1.
Giả sử mảng v chứa các giá trị đã điền của cột j – 1 trong mảng hai chiều p. Ta sẽ
điền các giá trị cho cột j của mảng p vào mảng một chiều d. Như vậy, tại bước j, phần
tử v[i] sẽ ứng với phần tử p[j – 1, i] còn phần tử d[i] sẽ ứng với p[j, i]. Thủ tục điền
21


trị cho cột d tại bước j dựa theo kết quả lưu trong cột v của bước j – 1 khi đó sẽ như
sau:
for i := j-1 downto 1 do
begin
if s[i] = s[j] then d[i] := v[i+1]+2
else d[i] := max(v[i],d[i+1]);
end;

Sau mỗi lần lặp với j := 1..n ta chuyển giá trị của d cho v để chuẩn bị cho bước
tiếp theo.
procedure QHD;
var i,j: integer;
begin
fillchar(v,sizeof(v),0);
for j := 1 to n do
begin
d[j] := 1;
for i := j-1 downto 1 do
begin
if s[i]= s[j] then d[i] := v[i+1]+2
else d[i] := max(v[i],d[i+1]);
end;
v := d;
end;
writeln(nl,n-d[1]); {dap so}
end;

6. Một số bài tập
Bài 1: Xâu đối xứng
Tên chương trình: PALINDR.PAS
Palindrome là xâu ký tự mà nếu đọc nó từ trái sang phải cũng như từ phải sang
trái ta được cùng một xâu. Một xâu ký tự bất kỳ luôn có thể biểu diễn như là một dãy
các palindrome nếu như ta coi xâu chỉ gồm một ký tự luôn là một palindrome.
Ví dụ: Xâu ‘bobseesanna’ có thể biểu diễn dưới dạng dãy các palindrome theo nhiều
cách, chẳng hạn:
‘bobseesanna’ = ‘bob’ + ‘sees’ + ‘anna’
‘bobseesanna’ = ‘bob’ + ‘s’ + ‘ee’ + ’s’ + ‘anna’
‘bobseesanna’ = ‘b’ +’o’ + ‘b’ + ‘sees’ + ‘a’ + ‘n’ + ‘n’ + ‘a’
Yêu cầu: Cho xâu ký tự s, cần tìm cách biểu diễn xâu s dưới dạng một dãy gồm
số ít nhất các palindrome.

22


Dữ liệu: tệp văn bản PALINDR.INP, gồm một dòng chứa xâu ký tự s gồm không quá
1000 ký tự.
Kết quả: tệp văn bản PALINDR.OUT một số nguyên dương k là số lượng ít nhất các
palindrome trong biểu diễn tìm được.
Ví dụ:
PALINDR.INP
bobseesanna

PALINDR.OUT
3

Bài 2: Người thợ lặn
Tên chương trình: PROGMAN.PAS
Mỗi thợ lặn đều sử dụng trang thiết bị đặc biệt khi lặn. Có một xylanh với 2
bình chứa: một chứa ôxy và một chứa Nitơ. Phụ thuộc vào thời gian lặn dưới nước và
độ sâu của việc lặn anh ấy dùng đến lượng oxy và nitơ khác nhau. Anh ta được tuỳ ý
sử dụng một số xylanh. Mỗi xylanh được miêu tả bởi trọng lượng và dung tích mà nó
chứa. Anh ấy biết trước việc lặn của mình cần bao nhiêu oxy và nitơ.
Yêu cầu: Bạn hãy tìm một hoặc một số xylanh có trọng lượng nhỏ nhất anh ấy
mang theo để hoàn thành việc lặn.
Dữ liệu: tệp văn bản PROGMAN.INP
- Dòng đầu là ghi 2 số nguyên: t, a (1<=t<=21, 1<=a<=79: t là dung tích oxy và
a là dung tích nitơ anh ta cần, dung tích tính theo lít)
- Dòng 2 ghi số n nguyên dương (1<=n<=1000 là số xylanh)
- n dòng sau, dòng thứ i+2 ghi 3 số nguyên ti, ai, wi, trong đó 1<=ti<=21,
1<=ai<=79, 1<=wi<=800 lần lượt là dung tích oxy, dung tích nitơ và trọng
lượng của bình thứ i)
Dữ liệu cho trên cùng hàng được ngăn cách nhau bởi ít nhất một dấu cách.
Kết quả: tệp văn bản PROGMAN.OUT
Ghi 1 số duy nhất là tổng trọng lượng các bình người thợ lặn cần mang theo để hoàn
thành việc lặn của anh ta.
Ví dụ:
PROGMAN.INP

PROGMAN.OUT
23


5 60
5
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119

249

Bài 3: Đường đi nhỏ nhất
Tên chương trình: DUONGDI.PAS
Cho hình chữ nhật gồm M dòng, N cột. Các dòng được đánh số từ 1 đến M,
các cột được đánh số từ 1 đến N. Trên mỗi ô đều có một số nguyên x (|x| <32000).
Hãy tìm đường đi từ ô (1,1) đến ô (M,N) sao cho tổng các số trong các ô trên đường
đi là bé nhất. Biết rằng từ một ô bất kỳ chỉ có thể đi sang ô chung cạnh bên phải hoặc
xuống dưới.
Dữ liệu: tệp văn bản DUONGDI.INP
- Dòng đầu tiên chứ 2 số M, N
- M dòng tiếp theo mỗi dòng chứa N số nguyên tương ứng với M dòng và N cột
của hình chữ nhật.
Kết quả: tệp văn bản DUONGDI.OUT
- Chứa một số duy nhất là tổng các số trên đường đi cần tìm.
DUONGDI.INP
23
121
011

DUONGDI.OUT
3

24


Bài 4: Người buôn lậu
Tên chuơng trình: SMUGGLER.PAS
Byteotia là quốc gia giàu có nhờ vàng. Vì vậy, trong rất nhiều năm người dân
của nước này bán vàng qua nước láng giềng là Bitland. Thật không may mắn, gần
đây Bitland bị thâm hụt ngân sách. Vì vậy, Nhà vua của Bitland quyết định đánh thuế
rất nặng (50%) vào vàng và các khoáng sản khác mua từ nước Byteotia.
Các nhà hoá học nước Byteotia vừa nghiên cứu ra các cách chuyển vàng thành
một số kim loại khác và do đó để chuyển được thì phải mất một lượng phí nhất định
cho 1 kg trọng lượng. Do đó các thương gia đưa ra ý tưởng chuyển vàng thành một
kim loại khác có giá trị nhỏ hơn (để giảm thuế) rồi chuyển qua biên giới, sau khi tới
Bitland thì chuyển ngược lại từ kim loại thành vàng để thu được lợi nhuận cao nhất.
Yêu cầu: Bạn hãy viết chương trình giúp các thương gia Byteotia quyết định chuyển
vàng qua biên giới (thuế là 50% giá trị 1 kg vàng) hay đưa ra một trình tự đổi từ vàng
thành các kim loại và từ kim loại thành vàng với số tiền phải trả cộng thêm 50% giá
trị kim loại có giá trị nhỏ nhất trong dãy biến đổi là nhỏ nhất có thể.
Dữ liệu: tệp văn bản SMUGGLER.INP
N (số kim loại khác nhau, 1<=n<=5000)
P1
… 0<=Pi<=109, Pi nguyên, là giá 1kg kim loại thứ i,
Pn
M (0<=M<=100 000, M nguyên dương là số cách đổi khác nhau)
A1 b1 c1
……………..

Am bm cm
(Ai bi ci nguyên dương, thể hiện kim lại thứ a i đổi lấy kim loại thứ bi với chi
phí ci, 1<=ai, bi<=n, 0<=ci<=10 000, mỗi cặp ai bi xuất hiện nhiều nhất 1 lần
trong file input)
Kết quả: tệp văn bản SMUGGLER.OUT
Ghi 1 số duy nhất là giá tiền ít nhất các thương gia Byteotia phải trả.
Ví dụ:
SMUGGLER.INP SMUGGLER.OUT
25


Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Tải bản đầy đủ ngay

×