Những thuật toán hiệu quả trên đồ thị có hướng phi chu trình
Ngô Quốc Hoàn
Đồ thị là một lĩnh vực quan trọng trongtoán học rời rạc và có nhiều ứng dụng trong việc
giải các bài toántin học cũng như trong cuộc sống. Đồ thị có hướng phi chu trình làmột
trường hợp đặc biệt của đồ thị. Trong bài viết này chúng tôixin trình bày với các bạn
những thuật toán hết sức hiệu quả trênđồ thị có hướng phi chu trình và những bài toán
ứng dụng rất lýthú.
Trước hết ta xét một thuật toánquan trọng sau:
1. Thuật toán đánh số
Định lý: Giả sử G là đồ thị có hướngphi chu trình, khi đó các đỉnh của nó có thể đánh số
sao cho mỗicung của đồ thị chỉ hướng từ đỉnh có chỉ số nhỏ hơn đến đỉnhcó chỉ số lớn
hơn, nghĩa là mỗi cung của nó có thể biểu diễn dướidạng (v[i], v[j]) trong đó i < j.
+ các v[i] là số hiệu cũ của đỉnh, i là số hiệu mới
của đỉnh saukhi được đánh số
+ Đồ thị ở hình bên các đỉnh đãđược đánh số thoả mãn điều kiện nêu trong định lý.
- Để chứng minh định lý ta có thuật toán đánh số cácđỉnh của đồ thị thỏa mãn điều kiện
định lý như sau:
Thuật toán: ta tìm tất cả các đỉnh không có cung đi vào (gọi tắt làđỉnh trọc) lần lượt đánh
số các đỉnh này theo thứ tự tuỳ ý, sauđó xoá các đỉnh trọc vừađánh số và các cung đi ra
từnó khỏi đồ thị, sau đó ta làm lại như trên đối với các đỉnh trọcmới cho đến khi tất cả các
đỉnh được đánh số.
Thuật toán được mô tả trong thủ tụctựa pascal sau:
ProcedureNumbering;
(* đầu vào:đồ thị có hứng G=(V,E) với n đỉnh không chứa chu trình được cho bởidanh
sáchkề ke(v), v thuộc V.
đầu ra: là mảng chỉ số CS, sao cho mỗi cung đêù có dạng(CS[u], CS[v] ) trong đó
u < v.*)
BEGIN
For v thuộc V do Vao[v]:=0;
(* tính bậc vào của đỉnhv *)
For u thuộc V do
For v thuộc ke(u) do vao[v]:=vao[v]+1;
Queue:= rỗng ;
For v thuộc V do
If vao[v] = 0 then queue <- v;
Num := 0;
While Queue ≠ rỗng do
Begin
u <- Queue;
num:=num+1;
CS[num]:=u;
For v thuộc ke(u) do
Begin
Vao[v]:=vao[v]-1;
If vao[v]=0 then Queue <- v ;
End;
End;
END;
Chú ý:
+ Theo thuậttoán trên thì đỉnh được đánh số sau sẽ có số hiệu lớn hơn đỉnhđược đánh số
trước.
+ Nếu nhưđồ thị mà có chu trình thì áp dụngthuật toán đánh số trên thì nó sẽ không đánh
số các đỉnh thuộcchu trình vì chúng không bao giờ trọc, lúc đó ta có num < n. Vì vậy ta có
thể áp dụng thuật toán này để kiểm tra mộtđồ thị có chu trình hay không.
+ Độ phức tạpcủa thuật toán cỡ O(m).
- Từ thuật toán trên ta có thể xây dựng và phát triển rấtnhiều ứng dụng:
- Tìm đường đi ngắn nhất,dài nhất trên đồ thị có trọngsố và không chu trình.
- Tìm đường đi dài nhất tính theo số cạnh và không chu trình.
2. Thuật toán tìm đường đi ngắnnhất, dài nhất
Thuậttoán tìm đường đi ngắn nhất trên đồ thị có hướng phi chu trìnhđược mô tả trong thủ
tục sau:
Procedurecritical_path;
(* thủ tục tìmđường đi ngắn nhất từ đỉnhnguồn đến tất cả các đỉnh còn lại.
đầu vào: đồ thị G=(V,E), trong đó V = { v[1], v[2],..,v[n].
Với mỗi cung (v[i],v[j] ) thuộc V thì i < j.
đồ thị đượccho bởi ma trận trọng số [ a[i,j] ]
đầu ra: khoảng cách từv[1] đến cả các đỉnh còn lại được ghi trong mảng
D[v[i]] , i = 2 , 3,..,n
Truoc[v[i]] ghi nhận đỉnh đi trước v[i] trên đường đi từ v[1] đến v[i] *)
BEGIN
(* khởi tạo *)
For j :=2 to n do d[v[j]] :=a[v[1],v[j]];
D[v[1]] := 0;
For i:=1 to n �1 do
For j :=i+1 to n do
If (v[i],v[j] ) thuộcE then
Begin
If d[v[j]]>d[v[i]]+ a[v[i],v[j]] then
Begin
D[v[j]]:=d[v[i]] + a[v[i],v[j]];
Truoc[v[j]]:=v[i];
End;
End;
END ;
Ta thấy bản thuật toán trên là quyhoạch động với công thứcquy hoạch động là
D[v[j]] = min( d[v[j]] , d[v[i]] + a[v[i],v[j]] ).
Nhận xét:
+ trong thủ tục trên mỗicung của đồ thị phải xét quađúng một lần do đó độ phức tạp của
thuật toán chỉ có 0(m).
+ thủ tục trênchỉ cho phép tìm đườn đi ngắn nhất, vậy để tìm được đường đi dài nhất
ta phải đổidấu toàn bộ trọng số trên cung, hoặc đổi chiều bất đẳng thức trongthủ tục.
3. Thuật toán tìm đường đi dài nhấttính theo số cạnh:
- Thuật toántìm đường đi dài nhất tính theo số cạnh trên đồ thị không chu trìnhthực chất là
được suy biến ra từ thuật toán đánh số và có tên làthuật toán đánh mức, tác giả Trần Đức
Thiện đã có bài viết vềthuật toán này do vậy ở bài viết này tôi chỉ nói tư tưởng thuậttoán:
Thuật toán:
Bước 1: khởi tạo k =1;
Bước 2: đánh mức k cho các đỉnh trọc.
Bước 3: k := k +1, lặp lại bước 2 cho đến khi cácđỉnh được đánh mức.
- Để tìm đường đi : xuất phát từ đỉnh có mức cao nhấtta lần ngước trở về theo quy tắc nếu
đang ở mức k thì trở về đỉnhcó mức k-1.
4. một số bài toán ứngdụng:
Bài toán1: Bài toán thựchiện dự án:
Một công trình gồm n côngđoạn đánh số từ 1 đến n, cómột số công đoạn mà việc thực hiện
nó chỉ được tiến hành sau khimột số công đoạn nào đó đã hoàn thành. Thời gian hoàn
thành côngđoạn i là t[i] với (i = 1 , 2,.. n). Giảsử thời điểm bắt đầu tiến hành thi công công
trình là 0. Hãy tìmtiến độ thi công công trình (chỉrõ mỗi công đoạn phải
được bắt đầu thực hiện vào thời điểm nào)để cho công trình được hoàn thành xong trong
thời điểm sớm nhất có thể được.
- Dữ liệu vào file văn bản pert.inp:
Dòng thứ nhất là số công đoạn n, n dòngtiếp theo dòng thứ i ghi các thông tin của công
đoạn i : số thứ nhấtlà t[i], sau đó là các công việc trước công việc i.
- Dữ liệu ra file văn bản pert. out:
dòng đầu ghi thời điểm sớm nhất hoànthành toàn bộ công trình.
dòng thứ i trong n dòng tiếp theo ghithời điểm bắt đầu thực hiện công việc i.
Bài toán này ta có thể giải trên mô hình đồ thị như sau: mỗi côngđoạn là một đỉnh, công
đoạn i mà phải làm trước công đoạn j thìcó cung nối từ i tới j với trọngsố là t[i,j]. Gắn thêm
2 đỉnh giả 0 và n+1 với ý nghĩa tương ứng với2 sự kiện là lễ khởi công (phải được thực
hiện trước tất cả cáccông đoạn) và lễ khánh thành (phải được thực hiện sau tất cả cáccông
đoạn) và coi t[0] = t[n+1] = 0. Ta gọi đồ thị thu được là G thì rõràng G là đồ thị có hướng
phi chu trình. Ta thấy thời điểm hoàn thànhsớm nhất công việc chính là thời điểm công
đoạn cuối cùng đượcthực hiên xong. Vậy thực chất bài toán tìm thời điểm ngắn nhất
hoànthành các công việc chính là bài toán tìm đường đi dài nhất từđỉnh 0 đến đỉnh n+1 trên
đồ thị G, do đó ta có thể áp dụng thuậttoán tìm đường đi dài nhấttrên đồ thị có hướng phi
chu trình nêu ở phần trên để giải bàitoán này. Có lẽ để cài đặt bài này thì không có gì là
khó do vậytôi để cho các bạn tự cài đặt cũng là giúp các bạn phần nào hiểurõ thêm về thuật
toán.
Bài toán 2: Mua vé tàu hoả:
Tuyến đường sắt từ A đến B đi qua một số nhà ga. Tuyếnđường có thể biểu diễn bởi một
đoạn thẳng, các nhà ga là các điểmtrên nó, các nhà ga sẽ được đánh số từ 1 đến n bắt đầu
từ nhàga A đến B (n là số lượng nhà ga). Giá vé đi giữa hai nhà ga phụthuộc vào khoảng
cách giữa chúng, cụ thể cách tính giá vé đượccho bởi bảng sau:
Trong đó 1≤L1 < L2 <L3 ≤10
9
, 1 ≤C1 < C2 < C3≤ 10
9
Vé đi từ nhà ga này đến nhà ga khácchỉ có thể đặt mua nếu khoảng cách giữa chúng không
vượt quá L3. Nếukhoảng cách giữa hai nhà ga lớn hơn L3 thì ta phải mua một số vé.
Yêu cầu: tìm cách đặt mua vé để đi lạigiữa hai nhà ga cho trước với chi phí mua vé là nhỏ
nhất.
Dữ liệu: vào từ file Rticket.inp:
- Dòng đầu tiên ghi các sốnguyên L1, L2, L3, C1, C2, C3.
- Dòng thứ 2 ghi số N (2 ≤ N ≤8000).
- Dòng thứ 3 ghi hai số nguyên S, T làchỉ số của hai nhà ga mà ta cần tìm cách đặt mua vé
với chi phí nhỏnhất.
- Dòng thứ i trong số N-1 dòng tiếptheo ghi số nguyên là khoảng cách từ nhà ga A (ga 1)
đến nhà ga i (i = 2 , 3.. N)
Kết quả: ghi ra file Rticket.out chi phí nhỏ nhất tìm được:
Ví dụ:
Xét bài toán:
Việc đi lại(chi phí) từ S đến T tương đương với từ T đến S. Do vậy nếu sốhiệu S > T thì ta
đổi chỗ S và T.
Ta quan niệm trênmô hình đồ thị như sau:
mỗi nhà ga là một đỉnh của đồ thị, hai đỉnh i, j có cung nối nếu khoảng cách giữa i và j
≤L3, và i <j. Trọng số trên cung là giá vé từ i đến j. Hướng của các cung làhướng từ A đến
B. Rõ ràng đồ thị thu được là đồ thị có hướngphi chu trình đã được đánh số, bài toán đưa
về việc tìm đườngđi ngắn nhất trên đồ thị có hướng phi chu trình. Vậy ta chỉ cần dùngthuật
toán tìm đường đi ngắnnhất như: dijkstra, critical_path..
Tôi nghĩ các bạnnên làm bằng cả hai thuật toán trên để so sánh với nhau và bạn sẽthấy
critical_path trong trường hợp này là rất hiệu quả. Sau đây làtoàn văn chương trình:
Progammvtauhoa;
uses crt;
const
fi = ’Rticket.inp’;
fo = ’Rticket.out’;
MaxN = 8000 ;
Maxk = maxlongint div 2;
type
mang1 = array [1..MaxN] of longint;
var
a,w: mang1;
l1,l2,l3 : longint;
c1,c2,c3 : longint;
n,s,t : byte;
f : text;
procedure init;
var i,tg: byte;
begin
assign(f, fi); reset(f);
readln(f,l1,l2,l3,c1,c2,c3);
readln(f,n);
readln(f,s,t);
a[1]:= 0;
for i:=2 to n do readln(f,a[i]);
close(f);
if s > t then
begin
tg := s; s :=t; t := tg;
end;
end;
function cp(u,v :byte):longint;
var k:longint;
begin
k := a[v] - a[u];
if k <= l1 then cp := c1
else
if k <= l2 thencp := c2
else
if k <= l3then cp := c3
else cp :=Maxk;
end;
procedure critical_path;
var i, j : byte;
k : longint;
begin
for i:=s to t do w[i] := Maxk;
w[s] := 0;
for i:=s to t-1 do
for j:=i+1 to t do
begin
k := cp(i, j);
if w[j] > w[i] + k then w[j] := w[i] + k;
if k = Maxk then break;
end;
end;
procedure result;
begin