Thuật toán đồ thị có hướng và chu trình
Trang 1Nhữ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
Trang 2If 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]];
Trang 3End;
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
Trang 4Bà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 ≤109, 1 ≤C1 < C2 < C3≤ 109
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;
Trang 5l1,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
Trang 6assign(f, fo); rewrite(f); writeln(f,w[t]);
close(f);
end;
BEGIN
critical_path;
result;
END