You are on page 1of 11

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.

1. Dùng dấu hiệu

Ta xem ví dụ với bài toán sau:


Có một bảng n*n. Một con rô bốt đi từ ô (1,1) đến ô (n,n). Ở ô (i,j), rô bốt có thể đi sang ô
(i+1, j) hoặc (i, j+1). Trên mỗi ô vuông có một giá trị. Ta cần tìm một đừong đi của rô bốt sao
cho thu được tổng giá trị lớn nhất có thể. (Xem hình vẽ)
Gọi F(i,j) là số điểm lớn nhất có thể ăn được khi vừa được di chuyển đến ô (i,j). Từ ô (i,j), ta
có hai lựa chọn là đi sang phải hoặc đi xuống dưới, vì vậy, công thức QHD sẽ là:
F(i,j) = a[i][j] + max(F(i+1,j), F(i,j+1))
với F(i,j) = -oo nếu i>n hoặc j>n. F(n,n) = a[n][n]. Điều ta cần tìm là F(1,1). Kết quả là F[1]
[1]=36.

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 F(i, j : số) : số;


biến
qd1 : số = -999999999;
qd2 : số = -999999999;
[
nếu (i>n) hoặc (j>n) thì trả_về -999999999;
nếu (i=n) và (j=n) thì trả_về a[n][n];
qd1 := a[i][j] + F(i, j+1);
qd2 := a[i][j] + F(i+1, j);
F := max(qd1, qd2);
nếu F=qd1 thì T[i][j] := 1;
nếu F=qd2 thì T[i][j] := 0;
];

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.

3. Viết mã truy vết trong hàm quy hoạch động.

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)

hàm F(i, j : số; dò : boolean = sai) : số;


biến
qd1 : số = -999999999;
qd2 : số = -999999999;
[
nếu (i>n) hoặc (j>n) thì trả_về -999999999;
nếu (i=n) và (j=n) thì trả_về a[n][n];
qd1 := a[i][j] + F(i, j+1);
qd2 := a[i][j] + F(i+1, j);
F := max(qd1, qd2);
nếu dò thì
nếu F = qd1 thì [ viết('Sang phải'); F(i, j+1, đúng); ]
ngược_lại [ viết('Xuống dưới'); F(i+1, j, đúng); ];
];

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.

1. Dãy con đơn điệu dài nhất


Cho dãy a1,a2,..an là dãy n số nguyên. Xoá đi một số phần tử của dãy trên và giữ nguyên
trình tự của các phần tử còn lại thì ta được một dãy con của dãy ban đầu. Bài toán yêu cầu là
xoá đi một số ít nhất các phần tử để dãy con thu được là dãy đơn điệu (chẳng hạn đơn điệu
tăng). Yêu cầu này tương đương với yêu cầu chọn ra một số nhiều nhất các phần tử của dãy
để dãy thu được (giữ nguyên thứ tự) là một dãy đơn điệu tăng.
Chẳng hạn dãy con tăng dài nhất của dãy 7 số hạng sau:
1354267
Là dãy con 1 3 5 6 7 có được từ việc xoá 2 phần tử 4 và 2 đi.
Để giải bài toán này bằng phương pháp quy hoạch động, ta trước hết phải xây dựng hàm
quy hoạch động.
Gọi L(i) là độ dài dãy con đơn điệu tăng lớn nhất, có các phần tử lấy trong miền từ a1 đến ai
và phần tử cuối cùng là ai.
Ta có công thức quy hoạch động như sau:
1. L(1) = 1
2. L(i) = max(1, L(j)+1 với mọi j<i và aj<=ai).
Chúng ta giải thích công thức như sau:
L(1)=1 là hiển nhiên.
Nếu ta không ghép ai vào dãy nào thì L(i)=1. Nếu ta ghép ai vào một dãy nào đó thì phải có
một phần tử j đứng trước i, thoả mãn aj<=ai. Dãy con dài nhất có phần tử cuối cùng là aj có
L(j) phần tử, vậy ghép thêm ai sẽ được dãy con có phần tử cuối cùng là ai và có L(j)+1 phần
tử.
Trong tất cả các lựa chọn đó, ta chọn lựa chọn tối ưu nhất để tính L(i). Đó là nguyên nhân vì
sao ta có hàm max.

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;

Bài toán chia kẹo


Có N gói kẹo, gói thứ i có Aicái kẹo. Không được bóc bất kỳ một gói kẹo nào, cần chia
N gói kẹo thành haiphần sao cho độ chênh lệch số kẹo giữa hai gói là ít nhất.
Dữ liệu vào trong file "chiakeo.inp" có dạng :
- Dòng đầu tiên là số N(N<=100);
- Dòng thứ hai là N số Ai(i=1, 2,.., N; Ai <=100).
Kết quả ra file "chiakeo.out" có dạng:
- Dòng đầu là độ chênh lệchnhỏ nhất giữa hai phần có thể được.
- Dòng hai là một dãy N số,nếu si =1 thì gói thứ i thuộc phần 1, nếu si =2 thì góithứ i
thuộc phần 2

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.

2. Xâu con chung dài nhất


Cho 2 xâu X,Y. Hãy tìm một xâu S thoả mãn:
1. S có thể nhận được từ X,Y bằng cách xoá đi một số kí tự (tức là S là xâu con của X và của
Y).
2. Độ dài của S là lớn nhất.
Trước hết ta xây dựng hàm quy hoạch động:
Gọi L(i,j) là độ dài xâu con chung dài nhất của xâu X (i) gồm i kí tự phần đầu của X (X (i) =
[1..i]) và xâu Y(j) gồm j kí tự phần đầu của Y (Y(j) = [1..j]).
Ta có công thức quy hoạch động như sau:
1. L(0,j)=L(i,0)=0.
1)+1 nếu X1,j2. L(i,j) = L(i = Y[j].
1)) nếu X1,j), L(i,j3. L(i,j) = max(L(i <> Y[j].
Công thức 1. là hiển nhiên. Công thức 2 và 3 được hiểu như sau: Nếu X 1)+1.1,j1) phần tử,
vậy X (i) và Y(j) có xâu con chung dài nhất L(i1,j1] có xâu con chung dài nhất L(i1] và
Y[1..j=Y[j] thì ta sẽ chọn ngay cặp phần tử đó, đoạn còn lại của 2 xâu là X [ 1..i
Ngược lại, nếu X <> Y[j] thì ta có 2 giải pháp: hoặc bỏ qua X và so X (i-1) với Y(j). Cách đó
cho xâu con dài nhất L(i-1,j) phần tử. Ngược lại, ta có thể bỏ qua Y[j] và so X (i) với Y(j-1) để
có xâu con dài nhất L(i,j-1) phần tử. Trong 2 cách chọn ta sẽ chọn cách tối ưu hơn.
Bảng phương án của ta sẽ là một bảng 2 chiều L[0..m,0..n], với n,m lần lượt là độ dài của X
và Y. Chương trình con tính bảng phương án như sau:
Mã:
procedure Optimize;
var i,j;
begin
for i := 0 to m do L[i,0] := 0;
for j := 0 to n do L[0,j] := 0;

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,j1] và L[i1,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)

Code này của : Nguyễn Tiến Trung Kiên

{$mode objfpc}
{$coperators on}

function bsearch(const a : array of integer; n, key :


integer) : integer;
var ll, rr, i : integer;
begin
ll := 1; rr := n; i := (ll+rr) div 2;
while (ll<>i) and (rr<>i) do
begin
if a[i]>=key
then rr := i
else ll := i;
i := (ll+rr) div 2;
end;
for i := ll to rr do
if a[i]>=key then exit(i);
exit(0);
end;

var a : array [0..1230997] of integer;


n, key : integer;

var i : integer;
begin
readln(n);
for i := 1 to n do
read(a[i]);

while not seekeof do


begin
readln(key);
i := bsearch(a, n, key);
if i=0 then writeln('Not found')
else writeln(a[i]);
end;
end.

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

Những ràng buộc

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)

Code này của Nguyễn Tiến Trung Kiên


{$mode objfpc}
{$coperators on}

uses math;

9
var
n: integer;
a, s: array [0..1000000] of integer;

function maximize(var a: integer; b: integer): boolean;


begin
Result := a<b;
if a<b then a:=b;
end;

function minimize(var a: integer; b: integer): boolean;


begin
Result := a>b;
if a>b then a:=b;
end;

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

You might also like