Tải bản đầy đủ (.doc) (21 trang)

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

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (177.08 KB, 21 trang )

PHƯƠNG PHÁP QUY HOẠCH ĐỘNG
(Đặng Tuấn Thành – THPT Chuyên Nguyễn Tất Thành, tỉnh Yên Bái)
Tư tưởng cơ bản của phương pháp quy hoạch động là sử dụng bảng để lưu trữ lời
giải của các bài toán con đã được giải. Khi giải bài toán con cần đến nghiệm của các bài
toán con cỡ nhỏ hơn, ta chỉ cần lấy lời giải trong bảng mà không cần phải giải lại. Chính
vì thế mà các thuật toán được thiết kế bằng phương pháp quy hoạch động sẽ rất hiệu
quả.
Để giải quyết một bài toán bằng phương pháp quy hoạch động, chúng ta cần tiến
hành những công việc sau:
1) Tìm các tham số mô tả bài toán;
2) Tìm nghiệm của các bài toán con nhỏ nhất;
3) Tìm công thức (hoặc quy tắc) xây dựng nghiệm của bài toán con thông qua
nghiệm của bài toán con cỡ nhỏ hơn;
4) Tạo bảng (dựa trên các tham số mô tả bài toán) để lưu trữ nghiệm của các bài
toán con. Tính nghiệm của các bài toán con theo công thức đã tìm và lưu trữ
vào bảng;
5) Từ các bài toán con đã giải để tìm nghiệm của bài toán.
1. Lớp các bài toán giải được bằng phương pháp quy hoạch động
• Bài toán tối ưu
o Max / Min
• Bài toán đếm
o Đếm số cấu hình
o Thứ tự từ điển
• Bài toán lập bảng phương án
o Xây dựng cấu hình
o Trò chơi
2. Các cách cài đặt
• Bottom-up (từ dưới lên)
o Khi đã xác định được thứ tự các bài toán con cần giải
o Cài đặt bằng vòng lặp
o Có thể giải thừa các bài toán con không cần thiết


• Top-down (từ trên xuống)
o Không cần thiết xác định được thứ tự các bài toán con cần giải
o Cài đặt bằng đệ quy có nhớ  có thể đặt cận
o Chỉ giải các bài toán con cần thiết
Độ phức tạp của thuật toán thiết kế theo phương pháp quy hoạch động
Giả sử bài toán được mô tả bằng các tham số

và cần tính hàm mục tiêu

. Khi đó, số bài toán con (hay còn gọi là số trạng thái) cần tính là

1


Gọi

là độ phức tạp tính nghiệm của bài toán con thông qua nghiệm của bài toán

con cỡ nhỏ hơn (còn gọi là chi phí chuyển trạng thái).
Khi đó độ phức tạp của thuật toán bằng: Số trạng thái
(

chi phí chuyển trạng thái

).

3. Những vấn đề cần chú ý khi giải bài toán bằng phương pháp quy hoạch động
1. Tìm các tham số mô tả bài toán
Việc tìm các tham số mô tả bài toán được dựa trên các đặc điểm, tính chất của bài
toán (công việc này còn được gọi là đoán nhận trạng thái). Cụ thể, cần xác định có

những tham số nào, ý nghĩa mỗi tham số, miền giá trị của từng tham số. Đây là công
việc quan trọng mang ý nghĩa quyết định đến việc giải quyết bài toán.
2. Tối ưu thuật toán
Qua cách tính độ phức tạp thuật toán theo phương pháp quy hoạch động (độ phức
tạp bằng số trạng thái nhân với chi phí chuyển trạng thái) ta nhận thấy, để giảm độ phức
tạp thuật toán có thể giảm thiểu số trạng thái hoặc giảm chi phí chuyển trạng thái.
3. Một số ví dụ minh hoạ
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
2


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:
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;
3


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]);
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,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
4


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: 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
5


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
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.
6


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
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]);
7


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 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
8


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;

Bài toán 3: 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à:
Phương án    
1

7

0

0

0

2


6

1

0

0

3

5

2

0

0

4

5

1

1

0

5


4

3

0

0

6

4

2

1

0

9


7

3

3

1

0


8

3

2

2

0

9

4

1

1

1

10

3

2

1

1


11

2

2

2

1

Bài giải
Bước 1: 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).
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 Chia(i, j)
j: số học sinh
j=0

Chia(i, j) = 0

i = 0 and j ≠ 0

Chia(i, j) = 1

i
Chia(i, j) = Chia(i, i)
10


i≥j

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
Bước 2: 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.
Bước 3: 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.
11














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)


Mảng cc được mô tả như sau:
j-1

j

i-j

[i-j,j]

...

...


i

[i,j1]

[i,j]

Const MN = 70;{ gioi han tren cua m va n }
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ó:
 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:
 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

12


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 4. Bảng số (VOIR2_2012)
Giả sử A là lưới ô vuông gồm m dòng và n cột. Các dòng của lưới được đánh số
từ 1 đến m, từ trên xuống dưới. Các cột của lưới được đánh số từ 1 đến n, từ trái sang
phải. Ô nằm trên giao của dòng i và cột j của lưới gọi là ô (i, j).
Với số nguyên dương x, gọi f(x) là số lượng số nguyên dương không vượt quá x
mà trong biểu diễn nhị phân có hai bít 1 đứng cạnh nhau. Ví dụ, f(5)=1 vì trong các số
nguyên dương bé hơn hoặc bằng 5 chỉ có số 3 có biểu diễn nhị phân với hai bít 1 đứng
cạnh nhau.
Cho dãy số nguyên dương gồm m×n số b1, b2, ..., bm×n. Ta sẽ lần lượt điền các số
hạng của dãy
f(b1) mod 3, f(b2) mod 3, ..., f(bm×n) mod 3
vào các ô của lưới A theo thứ tự từ trên xuống dưới từ trái qua phải. Gọi bảng số
thu được là B.
Xét truy vấn sau đây đối với bảng số thu được B: Cho hai số nguyên p và q (1 ≤ p
≤ q ≤ m), hãy cho biết diện tích lớn nhất của hình chữ nhật gồm các ô nằm trong phạm
vi từ dòng thứ p đến dòng thứ q của bảng B mà trong đó chênh lệch giữa phần tử lớn
nhất và phần tử nhỏ nhất không vượt quá 1.
Yêu cầu: Cho m, n, dãy số b 1, b2, ..., bm×n và k bộ pi, qi (i = 1, 2, ..., k) tương ứng với k
truy vấn, hãy đưa ra các câu trả lời cho k truy vấn.
Dữ liệu: Vào từ file văn bản NUMTAB.INP trong đó:
• Dòng đầu tiên chứa hai số nguyên m, n (1 ≤ m, n ≤ 1000);

• Dòng tiếp theo chứa dãy số b1, b2, ..., bm×n (mỗi số không vượt quá 109);
• Dòng tiếp theo chứa số nguyên k (1 ≤ k ≤ 106);
• Dòng thứ i trong số k dòng tiếp theo chứa 2 số nguyên pi và qi (i = 1, 2, ..., k).
Hai số liên tiếp trên cùng một dòng được ghi cách nhau bởi dấu cách.
Kết quả: Ghi ra file văn bản NUMTAB.OUT gồm k dòng, mỗi dòng chứa một số là câu
trả lời cho truy vấn theo thứ tự xuất hiện trong dữ liệu vào.
Ví dụ:
NUMTAB.INP

NUMTAB.OUT
13


33
387632466
4
11
12
13
33

3
4
4
3

Tìm hiểu chương trình giải sau (sẽ được trao đổi khi tập huấn).
#include <iostream>
#include <fstream>
using namespace std;

const int MAX = 1000+5;
int a[MAX][MAX], m, n;
int kq01[MAX][MAX], kq12[MAX][MAX], w[MAX][MAX];
int xLen, xArr[32];
int fx[20][2][2][2][2];
int fy[1<<15][2][2][2][2];
int dx[20][2][2][2][2];
int dy[1<<15][2][2][2][2];
int qhd(int pos, int firstDigit, int firstOk, int lastDigit, int lastOk) {
int &res = fx[pos][firstDigit][firstOk][lastDigit][lastOk];
if (dx[pos][firstDigit][firstOk][lastDigit][lastOk]) return res;
if (pos>15) return ((firstDigit == lastDigit) && (firstOk == lastOk));
res = 0;
for (int cs = 0; cs<=max(xArr[pos],firstOk); cs++)
if (firstDigit+cs!=2)
res += qhd(pos+1, cs, firstOk | (csdx[pos][firstDigit][firstOk][lastDigit][lastOk] = 1;
return res;
}
14


int tinhSL15(int x, int firstDigit, int firstOk, int lastDigit, int lastOk) {
int &res = fy[x][firstDigit][firstOk][lastDigit][lastOk];
if (dy[x][firstDigit][firstOk][lastDigit][lastOk]) return res;
dy[x][firstDigit][firstOk][lastDigit][lastOk] = 1;
xLen = 15;
for (int i=1; i<=15; i++) {
xArr[xLen] = x & 1;
xLen--;

x = x >> 1;
}
memset(fx,0,sizeof(fx));
memset(dx,0,sizeof(dx));
return (res = (qhd(1, firstDigit, firstOk, lastDigit, lastOk)));
}
int tinhSL(int x) {
int xA = x >> 15, xB = x & ((1<<15)-1);
int res = 0;
for (int digit=0; digit<=1; digit++)
for (int ok=0; ok<=1; ok++) {
int tmp = tinhSL15(xB, digit, ok, 0, 0);
tmp += tinhSL15(xB, digit, ok, 0, 1);
tmp += tinhSL15(xB, digit, ok, 1, 0);
tmp += tinhSL15(xB, digit, ok, 1, 1);
res = res + tinhSL15(xA, 0, 0, digit, ok )*tmp;
}
return (x+1 - res);
}
void docFile() {
15


int x;
memset(fy,0,sizeof(fy));
memset(dy,0,sizeof(dy));
scanf("%d%d",&m,&n);
for (int i=1; i<=m; i++)
for (int j=1; j<=n; j++) {
scanf("%d",&x);

a[i][j] = tinhSL(x) % 3;
}
}
void chuanBi(int A, int B, int kq[MAX][MAX]) {
int s[MAX], p[MAX], h[MAX], top;
memset(h,0,sizeof(h));
memset(p,0,sizeof(p));
memset(kq,0,sizeof(kq));
memset(w,0,sizeof(w));
memset(s,0,sizeof(s));
for (int i=1; i<=m; i++) {
//tinh do cao
for (int j=1; j<=n; j++)
if ((a[i][j]>=A)&&(a[i][j]<=B)) h[j]++;
else h[j] = 0;
top = 0;
for (int j=1; j<=n+1; j++) {
if (h[j]>s[top]) {
top++;
s[top] = h[j];
p[top] = j;
16


continue;
}
while (s[top]>h[j]) {
w[i][s[top]] = max(w[i][s[top]],s[top] * (j-p[top]));
top--;
}

if (h[j]>s[top]) {
top++;
s[top] = h[j];
}
}
// tinh cac do cao con lai
int sMax = 0;
for (int j=1; j<=i; j++) {
sMax = max(sMax, w[i][i-j+1] / (i-j+1));
w[i][i-j+1] = max(w[i][i-j+1],sMax*(i-j+1));
}
}
for (int i=1; i<=m; i++)
for (int j=i; j>=1; j--) kq[j][i] = max ( kq[j+1][i], w[i][i-j+1]);
for (int i=1; i<=m; i++)
for (int j=i+1; j<=m; j++) kq[i][j] = max ( kq[i][j], kq[i][j-1]);
}
void truyVan() {
int q, i, j;
scanf("%d",&q);
for (int k=0; kscanf("%d%d",&i, &j);
17


printf("%d\n",max(kq01[i][j],kq12[i][j]));
}
}
int main() {
freopen("numtab.inp","r",stdin);

freopen("numtab.out","w",stdout);
docFile();
chuanBi(0,1,kq01);
chuanBi(1,2,kq12);
truyVan();
return 0;
}
5. Một sối 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.
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

PALINDR.OUT
18



bobseesanna

3

Bài 2: Phân tích (ACMVN2011)
Cho một số nguyên dương
nguyên tố,

có dạng

là số

.

Ta gọi

là một cách phân tích của số
là các số nguyên dương và


sao cho

với

Hai cách phân tích

được gọi là khác nhau nếu tồn tại
.


Yêu cầu: Đếm số cách phân tích khác nhau
Ví dụ,

.

ra thừa số nếu

ta

thành tích các thừa số.



4

cách

phân

tích

như

sau:

Input
- Dòng đầu là số
-

là số bộ dữ liệu trong file,


dòng sau, mỗi dòng chứa một số nguyên dương

Output
- Gồm

dòng, mỗi dòng chứa một số là số cách phân tích khác nhau của từng bộ

dữ liệu tương ứng với dữ liệu vào.
FACTOR.INP

FACTOR.OUT

2

4

30

8

100
Chú ý: 50% số test có
Bài 3:

Thi đầu bếp giỏi (VOIR2_2012)

ĐBG2012 là cuộc thi đầu bếp giỏi do Liên hiệp hội các khách sạn du lịch tổ chức. Cuộc
thi được diễn ra trong khoảng thời gian từ thời điểm A đến hết thời điểm B. Trong
khoảng thời gian này, mỗi thí sinh tham gia cuộc thi phải nấu đúng n món ăn, được

đánh số từ 1 đến n. Thanh là một đầu bếp kì cựu và quyết tâm giành giải cao nhất của
19


cuộc thi này. Để chuẩn bị tốt nhất cho cuộc thi, Thanh đã tính được thời gian cần thiết
để nấu món thứ i là t i đơn vị thời gian và đánh giá món ăn đó có mức độ quan trọng là
wi. Dự đoán là Ban giám khảo sẽ đi chấm bài thi của mình vào khoảng thời điểm D,
Thanh đã đánh giá rằng nếu món ăn thứ i nấu xong ở thời điểm fi thì độ hấp dẫn của nó
sẽ bị giảm một lượng: |fi –D|×wi . Để đạt được kết quả cao, Thanh cần xây dựng kế
hoạch nấu n món ăn sao cho tổng lượng giảm độ hấp dẫn của tất cả n món ăn là nhỏ
nhất. Biết rằng, tại mỗi thời điểm Thanh chỉ có thể nấu một món ăn và mỗi món ăn phải
được nấu liên tục từ lúc bắt đầu cho đến khi nấu xong.
Yêu cầu: Cho biết số lượng các món ăn n, thời gian thực hiện nấu n món ăn t 1, t2, ..., tn
và các thời điểm A, B, D, hãy xây dựng kế hoạch nấu n món ăn sao cho tổng lượng
giảm độ hấp dẫn của n món ăn là nhỏ nhất.
Dữ liệu: Vào từ file văn bản DBG2012.INP:

Dòng đầu tiên chứa bốn số nguyên n, A, B và D (1 ≤ n ≤ 1000, 0 ≤ A ≤ D ≤
B ≤ 10000);

Dòng thứ i trong số n dòng tiếp theo chứa 2 số nguyên t i và wi là thời gian
cần thiết để nấu món ăn i và mức độ quan trọng của nó (0 < t i, wi ≤ 100, i = 1,
2, ..., n; t1+ t2+...+ tn ≤ B − A).
Hai số liên tiếp trên cùng dòng được ghi cách nhau bởi dấu cách.
Kết quả: Ghi ra file văn bản DBG2012.OUT tổng lượng giảm độ hấp dẫn của n món ăn
theo kế hoạch tìm được.
Ví dụ:

0


DBG2012.INP

DBG2012.OUT

3 0 100 50
20 2
40 3
30 2
40 50

130

Hình vẽ minh hoạ
Món 2

60

Món Món 3
1

90

Trong những năm gần đây, việc giải một bài toán thường có sự kết hợp giữa
nhiều phương pháp. Trong đó có sự kết hợp giữa PP Quy hoạch động và tìm kiếm nhị
phân để tối ưu thuật toán. Đây cũng là hướng nghiên cứu tiếp theo cho giáo viên và học
sinh môn Tin học trong thời gian tới. Nắm vững phương pháp quy hoạch động là tiền đề
tốt để nghiên cứu nội dung trên.
Trong quá trình nghiên cứu và biên soạn tài liệu tôi chỉ đề cập đến những ví dụ
đơn giản có tính chất cơ bản nhất, giúp cho học sinh dễ dàng nắm được phương pháp
quy hoạch động. Tôi mong các đồng nghiệp góp ý để chuyên đề này được tốt hơn!

Trân trọng cảm ơn!
Yên Bái, ngày 10 tháng 9 năm 2012
20


Tài liệu tham khảo
1.
2.
3.
4.

Tài liệu giáo khoa chuyên Tin học – Hồ Sĩ Đàm
Tài liệu tập huấn GV Chuyên Tin năm 2012
Chuyên đề Quy hoạch động – Phạm Chí Hiếu – Chuyên Thái Nguyên
Ebook DSAP – Lê Minh Hoàng

21



×