CHUYÊN ĐỀ NÂNG CAO 6CHUYÊN ĐỀ 6: QUY HOẠCH ĐỘNG Trong ngành khoa học máy tính, quy hoạch động là một phương pháp giảm thời gian chạy của các thuật toán thể hiện các tính chất của các bài
Trang 1CHUYÊN ĐỀ NÂNG CAO 6
CHUYÊN ĐỀ 6: QUY HOẠCH ĐỘNG
Trong ngành khoa học máy tính, quy hoạch động là một phương pháp giảm thời gian chạy của
các thuật toán thể hiện các tính chất của các bài toán con gối nhau (overlapping subproblem) và cấu trúc con tối ưu (optimal substructure).
Nhà toán học Richard Bellman đã phát minh phương pháp quy hoạch động vào năm 1953 Ngành này đã được thành lập như là một chủ đề về kỹ nghệ và phân tích hệ thống đã được tổ chức IEEE thừa nhận
AA MỘT SỐ BÀI TOÁN BẮT ĐẦU
1 Bài toán Fibonaci
Dãy Fibonaci được định nghĩa như sau:
1 (n 3)
Em hãy tìm số Fibonaci thứ n
* Ý tưởng đầu tiên – Thuật toán 1: Đệ quy
Nhìn vào công thức trên ta thấy ngay bản chất đệ quy, việc tính Fn ta có thể viết ngay rất dễ dàng
và ngắn gọn:
Function Fb(n:longint):int64;
Begin
If (n<=2) then exit(1) else fb:=fb(n-1) + fb(n-2);
End;
Ta sẽ nhận xét về cách làm này thông qua ví dụ sau:
Khi ta gọi: x:=Fb(10) thì việc tính fb(10) này sẽ tính như sau:
X:= fb(9) + fb(8)
X:= fb(7) + fb(8) + fb(6) + fb(7)
X:= fb(5) + fb(6) + fb(6) + fb(7) + fb(4) + fb(5) + fb(5) + fb(6)
…
Ta thấy ngay bước thứ 3: có rất nhiều lời gọi hàm trùng lặp nhau: f6 gọi 3 lần, f5 gọi 3 lần … chính điều này làm cho thời gian thực thi thuật toán này rất lớn Chính vì vậy thuật toán đệ quy này chỉ chạy được với dữ liệu nhỏ Với n>=44 thuật toán trên không khả thi
* Ý tưởng cải tiến – Thuật toán 2: Quy hoạch động
Để thuật toán của ta chạy nhanh hơn ta thấy cần cải tiến làm sao để mỗi fb(x) chỉ cần tính một lần
Để làm điều này ta gọi F[i] là số fibonaci thứ i (f là mảng một chiều)
Khi đó ta thấy việc tính mảng này rất nhanh chóng:
Function Fibo(n:longint):int64;
Var i:longint;
Begin
f[1]:=1; f[2]:=1;
For i:=1 to n do f[i] : = f[i-1] + f[i-2];
Exit(f[n]);
End;
Trong thuật toán trên: Việc tính các số fibonaci được lưu lại, mỗi số tính 1 lần, tính từ nhỏ đến lớn, Số fibo sau được tính thông qua các số fibo nhỏ hơn đó cũng là một phần của tư tưởng quy hoạch động
Trang 2(chú thích: có các thuật toán khác tốt hơn nữa để tính số fibonaci thứ n, tuy nhiên để làm quen với tư tưởng QHĐ thì ta tạm dừng ở thuật toán này)
2 Bài toán sắp bò: Nguồn: NTU – Tên bài: Sabo
Anh nông dân Bo có một đàn bò gồm rất nhiều con cái và con đực Trong một hội chợ, anh muốn sắp một hàng bò gồm n con Tuy nhiên những con bò đực rất hung hăng nếu đứng gần nhau, anh phải
sắp tối thiểu k con bò cái xen giữa hai con bò đực để chúng khỏi húc nhau.
Bạn hãy giúp anh Bo đếm thử xem có bao nhiêu cách để sắp một hàng gồm n con bò mà hai con
bò đực bất kỳ không húc nhau (anh Bo có rất nhiều bò nên không sợ thiếu bò cái hoặc bò đực)
Ví dụ với n = 4 và k= 1, ta có 8 cách xếp như sau (M: bò đực, F: bò cái):
FFFF, MFFF, FMFF, FFMF, FFFM, MFMF, MFFM, FMFM
Dữ liệu nhập:
- Gồm hai số nguyên n và k cách nhau một khoảng trắng ( 1 ≤ n, k ≤ 1.000)
Dữ liệu xuất:
- Số cách xếp hàng thỏa mãn yêu cầu Do số lượng này có thể rất lớn nên chỉ cần in ra tối đa 6 chữ số cuối cùng (modulo 1.000.000)
Ví dụ
* ý tưởng đầu tiên: đệ quy quay lui
Ta nhận thấy bài này có thể quy về: Có bao nhiêu dãy nhị phân có độ dài n sao cho các kí tự 1
cách nhau k kí tự 0.
Ta sẽ duyệt quay lui tất cả cách sắp bò (sinh ra tất cả các dãy nhị phân có độ dài n thỏa mãn điều kiện) và ta đếm
Nhận xét: ở đây n<=1000 vậy cách liệt kê dãy nhị phân này không thể chạy được trong thời gian cho phép
* ý tưởng 2: Quy hoạch động
Ta gọi B[i] là số cách sắp i con bò thỏa mãn điều kiện của bài toán Kết quả bài toán là B[n]
Vấn đề đặt ra ở bài này là ta chưa có công thức để tính b[i] (Không giống bài 1: đã có công thức tính F[i] = f[i-1] + f[i-2]).
Vậy để tính được mảng B này ta cần phải suy luận để tìm ra công thức Trong những trường hợp như vậy, cách làm chính là lấy giấy, bút, liệt kê thử xem với một số giá trị cụ thể:
Ví dụ: Với n=8 k=2 ta sẽ liệt kê thử số cách sắp:
Số cách sắp 1 con B[1] = 2 0 1
Số cách sắp 2 con B[2] = 3 00 01 10 (11 không được vì 2 bò đực sát nhau)
Số cách sắp 3 con B[3] = 4 000 001 010 100
Số cách sắp 4 con B[4] = 6 0000 0001 0010 0100 1000 1001
Số cách sắp 5 con B[5] = 9 00000 00001 00010 00100 01000 01001 10000 10001 10010
Số cách sắp 6 con B[6] = 13 000000 000001 000010 000100 …
Số cách sắp 7 con B[7] = 19 0000000 0000001 0000010 …
Số cách sắp 8 con B[8] = 28 00000000 00000010 00000100 …
…
Chắc chắn b[i] sẽ được tính thông qua các b[ ] nhỏ hơn nên ta thử:
B[1] = 1 + 1
B[2] = 2 + 1
B[3] = 3 + 1
Trang 3B[4] = b[3] + b[1] ( 6 = 4 + 2)
B[5] = b[4] + b[2] ( 9 = 6 + 3)
B[6] = b[5] + b[3]
…
B[i] = b[i-1] + b[i-k-1] Đây chính là thứ ta cần tìm
Sau khi thử như trên ta có thể thấy được quy luật như sau:
• Với i=1 đến k+1: b[i] = i+1
• Với i=k+2 đến n: b[i] = b[i-1] + b[i-k-1]
Công thức ta vừa tìm được ở trên chính công thức quy hoạch Chương trình áp dụng công thức trên như sau:
Const
tepvao='Sapbo.inp'; tepra='Sapbo.out';
modu=1000000;
Var
i,n,k:longint;
a,f:array[0 10001] of int64;
BEGIN
Assign(input,tepvao); reset(input);
Assign(output,tepra); rewrite(output);
Readln(n,k);
//QHD f[i]: so cach sap hang i con bo
f[1]:=2; {F[1] duoc goi la co so QHD}
for i:=2 to k+1 do f[i]:=(i+1);
for i:=k+2 to n do f[i]:=(f[i-1] + f[i-k-1]) mod modu;
//Ghi ket qua
write(f[n]);
Close(input); Close(output);
END.
3 Bài toán bàn cờ:
Xét một bàn cờ hình vuông kích thước nxn với mỗi ô c[i,j] là số lượng hạt đậu đang nằm ở ô [i,j] Một quân cờ xuất phát từ một ô bất kỳ của hàng 1, ta cần tìm một đường đi đến hàng cuối cùng n sao cho số lượng hạt đậu quân cờ nhặt được trên đường đi là lớn nhất
Quân cờ chỉ có thể đi thẳng xuống, chéo xuống sang trái hoặc chéo xuống sang phải quân cờ đi qua ô nào thì có thể nhặt được số lượng các hạt đậu trên ô này (n<=1000)
Ví dụ: với n=3, đáp án: 17
* ý tưởng đầu tiên: Đệ quy quay lui
Ta sẽ đệ quy quay lui tương tự bài Ốc sên ăn rau: Tìm tất cả đường đi từ hàng 1 đến hàng n: Đường đi nào có tổng lớn nhất thì ta ghi nhận
Ta thấy phương án đầu tiên này không khả thi khi n lớn (n<=1000)
* ý tưởng 2: Quy hoạch động
B[i,j] là số lượng hạt đậu nhặt được nhiều nhất khi đến được ô [i,j]
Ta có thể dễ dàng tìm được công thức để tính mảng 2 chiều B này như sau: Tại ô [i,j] ta nhận thấy
có 3 ô trước đó mà từ ô đó quân cờ có thể đến được ô [i,j] này:
Ví dụ: tại ô có giá trị 7: Có 3 ô ở hàng trên có thể đến được: chéo xuống sang trái là ô [i-1,j+1] = 3, chéo xuống sang phải [i-1,j-1] = 5, chéo xuống thẳng [i-1,j] = 2
Vậy thì ta thấy b[i,j] = max(của 3 ô này) + số lượng hạt tại ô [i,j]
Trang 4Hay B[i,j] = max (b[i-1,j-1], b[i-1,j], b[i-1,j+1]) + C[i,j]
Ta nhận thấy hàng đầu tiên b[1,j] = c[1,j] còn ta sẽ tính lần lượt từ hàng 2 trở đi xuống đến hàng
n mà đáp án sẽ là số lớn nhất trên hàng n
For j:=1 to n do b[1,j]:=c[1,j];
For i:=2 to n do
For j:=1 to n do B[i,j]:= max(b[i-1,j-1], b[i-1,j], b[i-1,j+1]) + C[i,j];
Thuật toán trên độ phức tạp là O(n2) và chi phí bộ nhớ là mảng 2 chiều nxn phần tử
* ý tưởng 3: Quy hoạch động rút gọn hơn
Ta nhận thấy cách làm 2 tuy tốt nhưng bộ nhớ sử dụng là mảng 2 chiều nên hiện tại với Freepascal ta chỉ có thể khai báo mảng tối đa khoảng b[1 10000,1 10000] (n<=10.000) vậy nên không thể chạy được với dữ liệu lớn
Ta thấy: Để đi đến dòng i thì chỉ có thể đi từ dòng i-1 Tính đến dòng thứ n thì ta thấy chỉ cần dòng n-1 còn các dòng trước không có tác dụng ( lãng phí bộ nhớ) Vậy nên ý tưởng ở đây là ta chỉ cần dùng các mảng một chiều, mỗi lần tính ta chỉ quan tâm 2 dòng: mảng lưu dòng i-1 và mảng lưu dòng i
Fillchar(t,sizeof(t),0);
For j:=1 to n do
For i:=1 to n do read(a[i]); //Đọc một dòng từ tệp
For i:=1 to n do B[i]:=max(t[i-1], t[i], t[i+1]) + a[i];
T:=b;
End;
Với cách làm này cho ta giải quyết được với n>=10.000
4 Một số bài tập mở đầu làm quen với QHĐ:
4.1 Lát gạch version 1: Nguồn: SPOJ – bài: LATGACH
Cho một hình chữ nhật kích thước 2xN (1<=N<=100) Hãy đếm số cách lát các viên gạch nhỏ kích thước 1×2 và 2×1 vào hình trên sao cho không có phần nào của các viên gạch nhỏ thừa ra ngoài, cũng không có vùng diện tích nào của hình chữ nhật không được lát
Input
Gồm nhiều test, dòng đầu ghi số lượng test T ( T<=100 )
T dòng sau mỗi dòng ghi một số N
Output
Ghi ra T dòng là số cách lát tương ứng
Example
Latgach1.in
p Latgach1.ou t Latgach1.in p Latgach1.o ut
3 1 2 3
1 2 3
2 4 5
5 8
Gợi ý: f[i] là số cách lát gạch cho sân kích thức 2 x i
4.2 Tổng đoạn:
Cho một dãy số nguyên A gồm n phần tử (ai<=10.000, n<=105) Nam thắc mắc liệu tổng của một đoạn bất kỳ trong dãy số trên là bao nhiêu Bạn hãy giúp cho Nam tính nhé!
Input
Dòng đầu ghi số lượng phần tử của dãy số N
Trang 5Dòng 2: ghi các phần tử của dãy số A
Dòng 3: ghi số lượng test T ( T<=105)
T dòng sau mỗi dòng ghi hai số a và b (nam muốn tính tổng của đoạn [a b]
Output Ghi ra T dòng tổng tương ứng của các bộ test
Example
Tongdoan.in
3
1 3 5 2
1 2
1 3
3 9
Gợi ý: nếu dùng cách duyệt bình thường O(n2) thì chắc chắn sẽ không chạy được với n=105, t=105
4.3 Hiệu lớn nhất: Nguồn: NTU – Bài: Hiso
Cho một dãy n số nguyên a1, a2, , an Hãy tìm hai chỉ số i, j sao cho i < j và hiệu aj - ai là lớn nhất
Dữ liệu vào: gồm 2 dòng
- Dòng 1: là số nguyên n (2 ≤ n ≤ 105)
- Dòng 2: gồm n số nguyên a1, a2, , an (0 ≤ ai ≤ 109)
Dữ liệu xuất:
- Là giá trị lớn nhất của hiệu aj - ai
Ví dụ
hieuso.inp hieuso.out hieuso.in
3
4.4 Lát gạch Version 2: Nguồn NTU – Bài: Laga
Có một khoảng sân hình chữ nhật kích thước 2 x n ô vuông, gồm 2 hàng và n cột Đánh số hàng từ 1 đến
2 theo thứ tự từ trên xuống dưới, đánh số cột từ 1 đến n theo thứ tự từ trái qua phải Người ta muốn lát sân bằng gạch màu trắng và điểm xuyết một số ô gạch màu đen, mỗi ô vuông được lát bởi một viên gạch, sao cho không có hai viên gạch màu đen nào chung cạnh với nhau Hỏi có tất cả bao nhiêu cách khác nhau để lát khoảng sân trên (hai cách lát sân được gọi là khác nhau nếu tồn tại tối thiểu một ô ở dòng i cột j được lát gạch màu trắng ở cách này và lát gạch màu đen ở cách kia)
Ví dụ với n = 2, ta có 7 cách lát sân sau đây:
Dữ liệu nhập: - Là một số nguyên n (1 ≤ n ≤ 1.000)
Dữ liệu xuất:
- Là số cách lát gạch khoảng sân theo yêu cầu trên Số lượng này có thể rất lớn nên chỉ cần in ra 8 số cuối (mod 100.000.000)
Ví dụ
4.5 Dạo chơi bằng xe buýt: Nguồn: SPOJ – Bài: KMbus
Một tuyến đường ở thành phố có các bến xe bus ở từng km tuyến đường Mỗi lần qua bến, xe đều đỗ
để đón khách Mỗi bến đều có điểm xuất phát Một xe chỉ chạy không quá B km kể từ điểm xuất phát
Trang 6của nó Hành khách khi đi xe sẽ phải trả tiền cho độ dài đoạn đường mà họ ngồi trên xe Cước phí cần trả để đi đoạn đường độ dài i là Ci(i=1,2 B) Một du khách xuất phát từ 1 bến nào đó muốn đi dạo L km theo tuyến nói trên Hỏi ông ta phải lên xuống xe như thế nào để tổng số tiền phải trả là nhỏ nhất có thể
Dữ liệu vào:
Dòng đầu ghi 2 số nguyên dương B, L
Dòng thứ i trong số B dòng tiếp theo ghi 1 số nguyên dương Ci ( 1 ≤ i ≤ B )
Kết qủa
Một dòng duy nhất là số tiền nhỏ nhất phải trả
Giới hạn
0 ≤ B ≤ 100 0 ≤ L ≤ 10000 0 ≤ Ci ≤ 100
Ví dụ
Dữ liệu:
5 7
3
4
6
9
22
Kết qủa
14