Professional Documents
Culture Documents
Truy vết
Truy vết
Truy vết là một vấn đề thuờng gặp trong quy hoạch đông. Có nhiều bạn đã thực hành quy
hoạch động nhiều rồi, tuy nhiên vẫn không tránh khỏi sự phức tạp của việc truy vết. Ở đây
mình trình bày một số phương pháp giúp các bạn cảm thấy dễ dàng hơn trong việc truy vết.
Mảng a Mảng F
bắt +---+---+---+---+ +---+---+---
đầu -> 4 | 2 | 6 | 5 | +---+
+---+---+---+---+ -> 36| 26| 21|
| 8 | 6 | 3 | 4 | 11|
+---+---+---+---+ +---+---+---
| 2 | 3 | 5 | 0 | +---+
+---+---+---+--- | 32|
+ kết 24| 15| 6 |
| 1 | 8 | 5 +---+---+---
| 2 <- thúc +---+
+---+---+---+---+ | 20| 18| 12|
2 |
+---+---+---
+---+
|
16| 15| 7 | 2 <-
+---+---+---
+---+
Cách giải
Xét F[i][j],
Nếu F[i][j]=F[i+1][j]+a[i][j] có nghĩa là ta phải đi xuống dưới.
Nếu F[i][j]=F[i][j+1]+a[i][j] thì ta phải đi sang phải.
(Nếu F[i+1][j]=F[i][j+1] thì ta có thể đi đến huớng nào cũng được (vẫn đúng))
Ví dụ
Xét ô F[2][2]=24, F[3][2]+a[2][2] = 18+6 = 24
nên nếu ta đi đến ô a[2][2]=24 thì ta phải đi xuống dưới
Từ dấu hiệu này, ta tạo ra hàm Trace có nội dung như sau:
1
hàm Truy_vết;
biến i, j : số;
[
i:=1; j:=1;
hễ (i<n) hoặc (j<n) thì [
nếu F[i][j] = F[i+1][j] + a[i][j]
thì [ i+=1; viết('Đi xuống dưới ô ',i,' - ',j); ]
ngược_lại [ j+=1; viết('Sang phải đến ô ',i,' - ',j); ];
];
];
Ghi nhớ
Dùng dấu hiệu F[i'][j'] = F[i][j] + a[i][j]. Mỗi khi đến ô (i,j), ta tìm ô (i',j') thỏa mãn dấu hiệu trên.
2. Lưu hướng đi
Cách truy vết ở trên chỉ sử dụng được khi ta biết chắc chắn ô (i,j) đi được đến
ô (i+1,j) và (i,j+1). Nếu bài toán có nhiều điều kiện hơn thì ta phải viết hàm Truy vết rất phức
tạp. Thế nên, ta sẽ dùng mảng T để đánh dấu hướng đi của mình.
Cách giải
Trong hàm F, nếu hướng đi của chúng ta là sang phải, ta sẽ gán T[i][j]=0. Nếu đi xuống dưới,
ta gán T[i][j]=1.
(hàm f ở đây mình không lưu vết để cho các bạn dễ hiểu về ý tưởng code)
hàm Truy_vết;
biến i, j : số;
[
i:=1; j:=1;
hễ (i<n) hoặc (j<n) thì [
nếu T[i][j] = 1
thì [ i+=1; viết('Đi xuống dưới ô ',i,' - ',j); ]
ngược_lại [ j+=1; viết('Sang phải đến ô ',i,' - ',j); ];
];
];
Ghi nhớ
2
Dùng mảng T để đánh dấu hướng đi.
Hàm quy hoạch động của chúng ta tính các giá trị của các bài toán con, sau đó tìm ra kết quả.
Hàm truy vết của chúng ta cũng phải tìm bài toán con là gì thì mình mới truy vết được. Vậy tại
sao chúng ta không thể lồng nội dung hai hàm làm một.
(hàm F ở đây mình chưa có nhớ để cho code đơn giản)
Bắt_đầu
nhập_vào;
viết(F(1, 1));
F(1, 1, đúng); // lời gọi lệnh truy vết
Kết_thúc.
3
Để cài đặt thuật toán ta dùng cấu trúc dữ liệu là một mảng 1 chiều L, L dùng lưu trữ giá trị
tương ứng của L(i). Sự khác biệt giữa phương pháp quy hoạch động và chia để trị từ trên
xuống là ở điểm này: các bài toán nhỏ được giải trước, và kết quả được lưu trữ lại để giải các
bài toán lớn hơn. Cấu trúc dữ liệu dùng để lưu trữ các kết quả đó gọi là bảng phương án.
Mã:
procedure Optimize;
var i,j;
begin
L[1] := 1;
for i := 2 to n do begin
L[i] := 1;
for j := 1 to i-1 do
if (a[j] <= a[i]) and (L[i] < L[j] + 1) then
L[i] := L[j] + 1;
end;
end;
Sau khi tính xong hàm quy hoạch động, làm thế nào để tìm lại kết quả? Có 2 phương án.
Phương pháp thứ nhất là dựa vào bảng phương án. Phương pháp thứ hai là xây dựng bảng
truy vết.
Dựa trên bảng phương án ta sẽ tìm được phần tử có L lớn nhất. Đó chính là phần tử cuối
cùng của dãy kết quả. Phần tử đứng ngay trước nó sẽ là phần tử j mà aj<ai và L=L[j]+1. Tìm
được phần tử j đó, ta lại tìm được phần tử đứng ngay trước nó bằng phương pháp tương tự.
Thủ tục lần vết tìm lại kết quả như sau:
Mã:
procedure Trace;
begin
i := 1;
for j := 2 to n do
if L[i] < L[j] then i := j;
for k := L[i] downto 1 do begin
kq[k] := i;
for j := 1 to i do
if (a[j]<=a[i]) and (L[i]=L[j]+1) then begin
i := j;
break;
end;
end;
end;
Thuật toán:
Với một số M bất kì, nếu ta biếtđược có tồn tại một cách chọn các gói kẹo để tổng số
kẹo của các gói được chọnbằng đúng M không, thì bài toán được giải sẽ quyết. Vì đơn
giản là ta chỉ cầnchọn số M sao cho M gần với Ai/2nhất (với i =1,2,..,N). Sau đó xếp các
gói kẹo để tổng bằng M vào phần một,phần thứ hai sẽ gồm các gói kẹo còn lại. Để
kiểm tra được điều trên ta sẽ xâydựng tất cả các tổng có thể có của N gói kẹo bằng
cách: ban đầu chưa có tổngnào được sinh ra. Làm lần lượt với các gói kẹo từ 1 đến N,
với gói kẹo thứ i,ta kiểm tra xem hiện tại có các tổng nào đã được sinh ra, giả sử các
tổng đó làx1, x2,.., xt vậy thì đến bước này sẽ có thểsinh ra các tổng x1, x2,.., xt và
Aivà x1+Ai,x2+Ai,..,xt+Ai.Với N gói kẹo, mà mỗi gói có không quá 100 cái kẹo vậy tổng
4
số kẹo không vượtquá N*100 <= 10000 cái kẹo. Dùng mảng đánh dấu D, nếu có thể
sinh được ratổng bằng k thì D[k] = 1 ngược lại D[k] = 0.
Chương trình thể hiện thuật toántrên.
Mã:
{$A+,B-,D+,E+,F-,G-,I+,L+,N-,O-,P-,Q+,R+,S+,T-,V+,X+,Y+}
{$M 16384,0,655360}
Program chia_keo;
uses crt;
const max = 100;
fi ='chiakeo.inp';
fo ='chiakeo.out';
var a,s : array[1..max]of integer;
d1,d2,tr : array[0..max*max]of integer;
n,m,sum : integer;
Procedure docf;
var f: text;
k : integer;
begin
assign(f,fi); reset(f);
readln(f,n);
sum:=0;
for k:=1 to n do
begin
read(f,a[k]);
sum:=sum+a[k];
end;
close(f);
end;
Procedure lam;
var i,j : integer;
Begin
fillchar(d1,sizeof(d1),0);
fillchar(tr,sizeof(tr),0);
d1[0]:=1;d2:=d1;
for i:=1 to n do
begin
for j:=0 to sum-a[i] do
if (d1[j]=1)and(d2[j+a[i]]=0) then
begin
d2[j+a[i]]:=1;
tr[j+a[i]]:=i;
end;
d1:=d2;
end;
end;
Procedure ghif;
var m,k : integer;
f :text;
Begin
fillchar(s,sizeof(s),0);
m:=sum div 2;
while d2[m]=0 do dec(m);
assign(f,fo);
rewrite(f);
writeln(f,sum-2*m);
while tr[m]>0 do
begin
s[tr[m]]:=1;
5
m:=m-a[tr[m]];
end;
for k:=1 to n do write(f,k+1,#32);
close(f);
end;
BEGIN {main}
docf;
lam;
ghif;
END.
Nhận xét:Chương trình trên đây cài đặt rất "thô", song dễ hiểu. Chương trình có thể cảitiến lại
để có thể chạy được số liệu lớn hơn, nhanh hơn. Ví dụ: bạn có cần để ýđến các tổng >sum/2
không? Có thể tích hợp cả ba mảng D1, D2 và TR làm mộtmảng không? Bạn đọc hãy chỉnh
lại để chương trình chạy tốt hơn.
for i := 1 to m do
for j := 1 to n do
if X[i]=Y[j] then L[i,j] := L[i-1,j-1]+1
else
L[i,j] := max(L[i-1,j],L[i,j-1]]);
end;
1] ta sẽ biết trong quá trình quy hoạch động ta đã chọn hướng đi nào:1,j1] và L[i1,j],
L[i,jĐể lần vết tìm nghiệm, ta dựa trên bảng phương án. Xâu con chung lớn nhất của X,Y có
độ dài L[m,n]. Dựa vào tương quan giá trị giữa L[i,j], L[i
Mã:
procedure Trace;
begin
i := m; j := n;
repeat
if X[i]=Y[j] then begin
kq[i] := 1;
i:=i-1; j:=j-1;
end
6
else
if L[i,j]=L[i-1,j] then i:=i-1
else j:=j-1;
until (i=0) or (j=0);
S := '';
for i:=1 t m do
if kq[i]=1 then S:=S+X[i];
end;
Dễ dàng kiểm tra thuật toán quy hoạch động tìm xâu con chung dài nhất có độ phức
tạp tính toán là O(n2) và đòi hỏi không gian bộ nhớ cũng là O(n2).
7
binarysearch.pas
Bài toán
Cho một mảng không giảm, tìm phần tử đầu tiên mà lớn hơn hoặc bằng một số cho trước.
Độ phức tạp
O(logn)
{$mode objfpc}
{$coperators on}
var i : integer;
begin
readln(n);
for i := 1 to n do
read(a[i]);
8
MAXSUMSQ - Chuỗi tổng tối đa
# quảng cáo-1
Cho một mảng A có n phần tử, gọi X là tổng tối đa của bất kỳ chuỗi liền kề nào
trong mảng. Có bao nhiêu chuỗi liên tiếp trong A tổng cộng lên X?
Đầu vào
Dòng đầu tiên chứa T số lượng các trường hợp thử nghiệm. Có các dòng 2T, 2
cho mỗi trường hợp thử nghiệm. Dòng đầu tiên chứa n, số phần tử trong
mảng. Dòng thứ hai chứa n số nguyên cách ly Ai.
Đầu ra
Đầu ra T dòng, một cho mỗi trường hợp thử nghiệm. Trên mỗi dòng, xuất hai số
nguyên tách biệt không gian; tổng chuỗi tối đa và số lượng các chuỗi có được
tổng tối đa này.
Thí dụ
Đầu vào mẫu
2
3
-1 -1 -1
4
2 0 -2 2
Đầu ra mẫu
-1 3
2 4
1 <= T <= 35
1 <= n <= 100000
-1000 <= Ai <= 1000
cadane (2).pas
Bài toán
Tìm dãy con liên tiếp có tổng lớn nhất.
Độ phức tạp
O(n)
uses math;
9
var
n: integer;
a, s: array [0..1000000] of integer;
procedure solve;
var
i: integer;
Min: integer = 0;
Max: integer = Low(integer);
CountMax: int64 = 0;
CountMin: integer = 1;
begin
readln(n);
for i := 1 to n do
read(a[i]);
for i := 1 to n do
s[i] := s[i-1] + a[i];
for i := 1 to n do
begin
if maximize(Max, s[i]-Min) then
CountMax := 0;
if Max=s[i]-Min then
CountMax += CountMin;
if minimize(Min, s[i]) then
CountMin := 0;
if Min=s[i] then
CountMin += 1;
end;
writeln(Max, ' ', CountMax);
end;
var
t: integer;
begin
readln(t);
for t := 1 to t do
solve;
10
end.
11