toán loang theo lớp
Nguyễn Tuấn Dũng
Tìm kiếm theo chiều rộng (Breadth First Search - BFS) - còn gọi là thuật toán loang và tìm
kiếm theo chiều sâu (Depth First Search - DFS) là 2 thuật toán cơ bản trong lý thuyết đồ
thị. Mặc dù thuật toán DFS với cấu trúc dữ liệu kiểu ngăn xếp (Stack) tuân theo quy luật
LIFO - Last in, First out (vào sau, ra trước) có thể cài đặt dễ dàng bằng đệ quỵ Nhưng vẫn
nảy sinh những bài toán mà để giải quyết nó thuận tiện hơn cần có một thuật toán tìm kiếm
khác, đó là BFS. Với cấu trúc dữ liệu kiểu hàng đợi (Queue), tuân theo quy luật FIFO -
First in, First out (vào trước, ra trước). Cài đặt thuật toán loang không cần dùng đệ quy và
cũng không đến nỗi phức tạp, đó là một ưu điểm. Hơn nữa, thuật toán tìm kiếm theo chiều
sâu 'đi sâú vào đồ thị, lưu các đỉnh trên đường đi vào ngăn xếp, còn thuật toán tìm kiếm
theo chiều rộng 'quét ngang' đồ thị và lưu các đỉnh vào hàng đợị Như ta biết, có thể tồn tại
nhiều đường đi từ một đỉnh A đến đỉnh B trên đồ thị. Nếu bằng BFS tìm được một đường
đi đầu tiên từ đỉnh A đến đỉnh B thì đường đi đó sẽ là đường đi qua ít đỉnh nhất từ A để
đến được B. Nhưng nếu bằng DFS thì ngược lại, đường đi tìm được đầu tiên chưa chắc đã
là đường đi qua ít đỉnh nhất. Mà trong một số bài toán, mặc dù vấn đề cần giải quyết có thể
được biến đổi, làm cho phức tạp hơn bởi người ra đề nhưng chung qui vẫn có thể đưa về
việc tìm đường đi sao cho phải qua ít đỉnh nhất. Khi đó cài đặt thuật toán loang là giải pháp
thích hợp. Đối với BFS cơ bản thì các bạn đã từng gặp trên các số báo trước đây của ISM.
Tuy nhiên, trong các bài toán cụ thể, mặc dù vẫn với yêu cầu trên nhưng chúng ta lại phải
sử dụng thuật toán loang một cách linh hoạt và sáng tạo mới đạt được kết quả mong đợị
Một ví dụ minh hoạ cho điều đó được đưa ra trong cách giải bài toán MEET - 'Gặp gỡ' (thi
Quốc gia 1998 -1999) dưới đây:
Trên một lưới ô vuông M*N (M,N < 100), người ta đặt robot A ở góc trái trên, robot B ở
góc phải dướị Mỗi ô của lưới có thể đặt một vật cản hoặc không (ô trái trên và phải dưới
không có vật cản). Hai robot bắt đầu di chuyển đồng thời với tốc độ như nhau và không
robot nào được dừng lại trong khi robot kia di chuyển (trừ khi nó không thể đi được nữa).
Tại mỗi bước, robot chỉ có thể di chuyển theo 4 hướng - đi lên, đi xuống, sang trái, sang
phải - vào các ô kề cạnh. Hai robot sẽ gặp nhau nếu chúng đứng trong cùng một ô vuông.
Bài toán đặt ra là tìm cách di chuyển ít nhất mà 2 robot phải thực hiện để có thể gặp nhau.
Dữ liệu vào trong file Meet.inp :
- dòng đầu ghi 2 số M,N.
- M dòng tiếp theo, mỗi dòng ghi N số 0 hoặc 1 mô tả trạng thái của các ô vuông: 1-có vật
cản, 0-không có vật cản.
Các số trên cùng một dòng của file dữ liệu cách nhau ít nhất một dấu trắng.
Kết quả ghi ra file Meet.out :
- nếu 2 robot không thể gặp nhau thì ghi ký tự #.
- Ngược lại, ghi hai dòng, mỗi dòng là một dãy các lý tự viết liền nhau mô tả các bước đi
của robot : U-đi lên, D-đi xuống, L- sang trái, R- sang phảị Dòng đầu là các bước đi của A,
dòng sau là của B.
Ví dụ:
Meet.inp
4 6
0 1 1 0 0 0
0 0 0 0 0 1
0 0 1 0 0 1
0 1 0 1 0 0
Meet.out
DRRR
LULU
Với dạng bài kiểu này thì ai cũng nghĩ ngay đến thuật toán loang. Nhưng loang như thế
nàỏ vì ta có hai robot cùng di chuyển. Và tất nhiên, bài toán này trở nên hay hơn so với các
bài loang thông thường. Chắc hẳn ai cũng biết một thí nghiệm phổ biến về sự giao thoa
sóng: 'cho 2 chiếc kim (không cách xa nhau lắm) dao động cùng tần số trên mặt nước, khi
đó có thể quan sát thấy những vòng tròn đồng tâm trên mặt nước cứ loang rộng dần ra cho
đến một lúc nào đó, 2 vòng tròn đầu tiên được tạo ra bởi sự dao động của 2 chiếc kim này
gặp nhau, giao thoa với nhaú.
Từ thí nghiệm đó, chúng ta có thể đưa ra cách di chuyển cho 2 robot theo kiểu loang chiều
rộng, nhưng chỉ loang từng lớp, từng lớp một đối với mỗi robot, giống như 2 vòng tròn
loang rộng dần ra trên mặt nước.
Về mặt ý tưởng thì sự loang của 2 robot là song song, đồng thời với nhaụ Nhưng bắt tay
vào làm cụ thể thì không cho phép chúng ta cài đặt thuật toán theo kiểu song song ở đâỵ Vì
thế cách giải quyết của tôi là sử dụng một vòng lặp (Procedure Strati-BFS) thực hiện luân
phiên 2 thủ tục: cứ cho robot 1 loang ra một lớp mới xung quanh (Procedure BFSRobot1)
thì dừng lại để cho robot 2 cũng loang ra một lớp xung quanh khác (Procedure
BFSRobot2). Vòng lặp sẽ kết thúc nếu hai lớp vừa mới loang của 2 robot này giao nhau
(PathFound - 2 robot sẽ gặp được nhau) hoặc cả hai robot đều không thể di chuyển được
nữa (Stoped - bài toán vô nghiệm). Tôi tạm gọi đây là thuật toán 'loang theo lớp'. Bản chất
này thực ra đã tồn tại trong cách lặp của thuật toán loang cơ bản và ở đây, việc khai thác
được nó giúp chúng ta giải quyết tốt vấn đề đặt rạ Tuy nhiên, khái niệm lớp ở đây nên hiểu
như thế nàỏ Các bạn đã biết trong cách lặp của BFS thì tại mỗi lần lặp, ta sẽ lấy ra (get out)
các đỉnh trong hàng đợi từ Queue[first] đến Queue[last] để đưa vào (put in ) hàng đợi các
đỉnh khác kề với các đỉnh đã lấy rạ Sau đó các biến first, last sẽ được điều chỉnh để trở
thành vị trí đầu và vị trí cuối của đoạn chứa các đỉnh mới này trong Queue (xem Procedure
BFSRobot1; và Procedure BFSRobot2;). Chúng ta gọi tập hợp các đỉnh từ Queue[first] đến
Queue[last] trong mỗi bước lặp của BFS là một lớp.
Dưới đây là chương trình giảị Trong trương trình có sử dụng một số thủ thuật cơ bản khá
quen thuộc trong bài toán tìm đường đi trên lưới ô vuông như kỹ thuật 'ràó - viền xung
quanh mảng A bằng các số 1 để không phải kiểm tra điều kiện vượt khỏi giới hạn của lưới
trong quá trình di chuyển robot: A[0,j] = A[M+1,j] = A[i,0] = A[i,N+1] = 1 (i = 1,..., M ; j
= 1,... N). Và 2 mảng hằng Hi, Hj giúp ta có thể dùng vòng for để thực hiện 4 hướng đi L,
U, R, D dễ dàng hơn trong khi loang, thuận tiện cho việc viết chương trình.
Mảng New1 được dùng để đánh dấu các ô vuông thuộc lớp mà robot 1 vừa loang tới, khi
cho robot 2 loang nếu gặp phải một trong các ô này thì nghĩa là tìm được nghiệm của bài
toán. Có thể tiết kiệm bộ nhớ nếu không dùng New1, mà đánh dấu ngay bằng mảng A với
cách cộng thêm 100 vào A[x,y] nếu ô (x,y) thuộc lớp mà robot 1 vừa loang tới (vì A[x,y]
chỉ ghi toàn số 1 hoặc số 0).
Hai mảng Q1, Q2 là hàng đợi cho việc loang robot 1 và robot 2. Nếu cần tiết kiệm bộ nhớ
có thể sử dụng chung một hàng đợi Q cho việc loang của 2 robot, robot 1 sử dụng phần đầu
từ Q[1] trở đi còn robot 2 sử dụng phần cuối từ Q[max*max] trở lại, vì tổng số ô vuông mà
cả 2 robot đi qua không vượt quá kích thước lưới M*N cho đến khi hai lớp loang của 2
robot giao nhau, mà vòng lặp lại được kết thúc ngay tại đây (tôi đã thử cài đặt kiểu này,
chương trình vẫn chạy ra kết quả đúng).
Còn việc đánh dấu đường đi được thực hiện bởi cách cộng thêm k vào A[x,y] nếu ô(x,y) là
ô được đi đến từ ô trước đó theo hướng k (số tự nhiên k chạy từ 1 đến 4 lần lượt chỉ 4
hướng đi của robot: L, U, R, D). xm,ym là toạ độ ô vuông 2 robot gặp nhaụ Phải sử dụng
thêm biến pred2 để ghi nhận hướng mà từ đó Robot 2 đi tới (xm,ym) vì A (xm,ym) đã
dùng để ghi nhận hướng mà từ đó Robot 1 đi đến ô gặp nhau. Việc tìm lại đường đi cho
từng robot được thực hiện trong procedure FindS1S2.
Trong khuôn khổ bài báo, không thể giải thích kỹ chương trình, vì vậy các bạn có thể
nghiên cứu chi tiết thêm qua chương trình giải cụ thể dưới đây:
Uses Crt;
Const Inf = 'Meet.inp';
Outf = 'Meet.out';
Max = 100;
Hi : Array[1..4]of integer = (0,-1,0,1);
Hj : Array[1..4]of integer = (-1,0,1,0);
Type Square=record x,y:byte; end;
Var A : Array[0..Max+1,0..Max+1]of byte;
Q1,Q2 : Array[1..Max*Max]of square;
New1 : Array[0..Max+1,0..Max+1]of Boolean;
First1,Last1,First2,Last2,N,M,Pred2,Xm,Ym : Word;
PathFound,Stoped : boolean;
S1,S2 : string;
Procedure Readinp;
var f:text; i,j:word;
Begin
fillchar(A,sizeof(A),1); {rào xung quanh}
assign(f,inf); reset(f);
readln(f,M,N);
for i:=1 to M do
begin
for j:=1 to N do read(f,a[i,j]);
readln(f);
end;
close(f);
End;
Procedure Init;
Begin
first1:=1; last1:=1;
Q1[1].x:=1; Q1[1].y:=1;
first2:=1; last2:=1;
Q2[1].x:=M; Q2[1].y:=N;
End;
Procedure BFSRoBot1;
var x,y,i,j,k:word;
begin
j:=last1;
for i:=first1 to last1 do
for k:=1 to 4 do
begin
x:=Q1[i].x + Hi[k];
y:=Q1[i].y + Hj[k];
if A[x,y]=0 then
begin
inc(A[x,y],k);
inc(j);
Q1[j].x:=x; Q1[j].y:=y;
New1[x,y]:=true;
end;
end;
first1:=last1+1; last1:=j;
if first1>last1 then Stoped:=true;
end;
Procedure BFSRoBot2;
var x,y,i,j,k:word;
begin
j:=last2;
for i:=first2 to last2 do
begin
for k:=1 to 4 do
begin
x:=Q2[i].x + Hi[k];
y:=Q2[i].y + Hj[k];
if A[x,y]=0 then
begin
inc(A[x,y],k);
inc(j);
Q2[j].x:=x; Q2[j].y:=y;
end;
if New1[x,y] then
begin
PathFound:=true;
Pred2:=k;
xm:=x; ym:=y;
Exit;
end;
end;
end;
first2:=last2+1; last2:=j;
if first2>last2 then Stoped:=true;
end;
Procedure StratiBFS;
Begin
PathFound:=False;
Stoped:=False;
Repeat
fillchar(New1,sizeof(New1),false);
BFSRoBot1;
BFSRoBot2;
Until Pathfound or Stoped;
End;
Procedure FindS1S2;
var x,y,k:byte;
begin
s1:=''; s2:='';
x:=xm; y:=ym;
{RoBot1}
repeat
k:=A[x,y];
case k of
1 : s1:=s1+'L';
2 : s1:=s1+'U';
3 : s1:=s1+'R';
4 : s1:=s1+'D';
end;
x:=x-Hi[k];
y:=y-Hj[k];
until (x=1)and(y=1);
{RoBot2}
x:=xm; y:=ym; A[x,y]:=Pred2;
repeat
k:=A[x,y];
case k of
1 : s2:=s2+'L';
2 : s2:=s2+'U';
3 : s2:=s2+'R';
4 : s2:=s2+'D';
end;
x:=x-Hi[k];
y:=y-Hj[k];
until (x=M)and(y=N);
end;
Procedure WriteOut;
var f:text; i,j:byte;