5. Bố cục đề tài
2.2.2. Các khái niệm mở đầu
Xét đồ thị có hướng G = (V,E) với V là tập các đỉnh, E là tập các cạnh, với các cung được gán trọng số, nghĩa là mỗi cung (u,v)E của nó được đặt tương ứng với một số thực a(u,v) gọi là trọng số của nó. Chúng ta sẽ đặt a(u,v)= 0, nếu (u,v)E . Nếu dãy v0, v1 , ... , vp là một đường đi trên G, thì độ dài của nó được định nghĩa là tổng sau:
p
∑a(vi-1, vi)
i=1
tức là , độ dài của đường đi chính là tổng các trọng số trên các cung của nó (chú ý rằng nếu chúng ta gán trọng số cho tất cả các cung đều bằng 1, thì ta thu được định nghĩa độ dài đường đi như là số cung của đường đi).
Bài toán tìm đường đi ngắn nhất trên đồ thị dưới dạng tổng quát có thể được phát biểu dưới dạng tổng quát như sau: Tìm đường đi có độ dài nhỏ nhất từ một đỉnh xuất phát SV đến đỉnh cuối (đích) tV. Đường đi như vậy sẽ gọi là đường đi ngắn nhất từ S đến t còn độ dài của nó sẽ kí hiệu là d(S,t) và còn gọi là khoảng cách từ S đến t (khoảng cách định nghĩa như vậy có thể là số âm ). Nếu như không tồn tại đường đi từ S đến t thì ta đặt d(S,t)= + từ đó ta thấy chu trình trong đồ thị có độ dài dương, thì trong đường đi ngắn nhất không có đỉnh nào lặp lại (đường đi như thế gọi là đường đi cơ bản).
Mặt khác, nếu trong đồ thị có chu trình với độ dài âm (gọi là chu trình âm) thì khoảng cách giữa 1 số cặp đỉnh nào đó của đồ thị có thể là không xác định, bởi vì, bằng cách đi vòng theo chu trình này một số đủ lớn lần, ta có thể chỉ ra đường đi giữa các đỉnh này có độ dài nhỏ hơn bất kì số thực cho trước nào. Trong trường hợp như vậy, có thể đặt vấn đề tìm đường đi cơ bản ngắn nhất, tuy nhiên bài toán đặt ra sẽ trở nên phức tạp hơn rất nhiều, bởi vì nó chứa bài toán xét sự tồn tại đường đi Hamintơn trong đồ thị như là một trường hợp riêng.
Trước hết cần chú ý rằng nếu biết khoảng cách từ S đến t, thì đường đi ngắn nhất từ S đến t, trong trường hợp trọng số không âm, có thể tìm một cách dễ dàng. Để tìm đường đi, chỉ cần chú ý là đối với cặp đỉnh S, tV tuỳ ý (St) luôn tìm được đỉnh v sao cho:
d(S,t)= d(S,v) + a(v,t)
Thực vậy đỉnh v như vậy chính là đỉnh đi trước đỉnh t trong đường đi ngắn nhất từ S đến t. Tiếp theo ta có thể tìm được u sao cho d(S,v)= d(S,u) + a(u,v),... Từ giả thiết về tính không âm của các trọng số dễ dàng suy ra rằng dãy t, v, u... không chứa đỉnh lặp lại và kết thúc ở đỉnh S. Rõ ràng dãy thu được xác định đường đi ngắn nhất từ S đến t.
2.2.3. Đƣờng đi ngắn nhất xuất phát từ một đỉnh
Phần lớn các thuật toán tìm khoảng cách giữa hai đỉnh S và t được xây dựng nhờ kỹ thuật tính toán mà ta có thể mô tả đại thể như sau: từ ma trận trọng số a[u,v], u, vV, ta tính cận trên d[v] của khoảng cách từ s đến tất cả các đỉnh vV. Mỗi khi phát hiện d[u]+a[u, v] < d[v] cận trên d[v] sẽ được tốt lên: d[v] = d[u] + a[u,v].
Quá trình đó sẽ kết thúc khi nào chúng ta không làm tốt thêm được bất cứ cận trên nào. Khi đó, rõ ràng giá trị của mỗi d[v] sẽ cho ta khoảng cách từ mỗi đỉnh S đến v. Khi thể hiện kỹ thuật tính toán này trên máy tính, cận trên d[v] sẽ được gọi là nhãn của đỉnh v, còn việc tính lại các cận trên này sẽ gọi là phép gán nhãn cho đồ thị và toàn bộ thủ tục thường gọi là thủ tục gán nhãn. Nhận thấy rằng để tính khoảng cách từ S đến tất cả các đỉnh còn lại của đồ thị. Hiện nay vẫn chưa biết thuật toán nào cho phép tìm đường đi ngắn nhất giữa hai đỉnh làm việc thực sự hiệu quả hơn những thuật toán tìm đường đi ngắn nhất từ một đỉnh đến tất cả các đỉnh còn lại.
Sơ đồ tính toán mà ta vừa mô tả còn chưa xác định, bởi vì còn phải chỉ ra thứ tự chọn các đỉnh u và v để kiểm tra điều kiện d[u] + a[u,v] < d[v] . Thứ tự chọn này có ảnh hưởng rất lớn đến hiệu quả thuật toán.
2.2.3.1. Thuật toán Dijkstra
Trong trường hợp trọng số trên các cung là không âm thuật toán do Dijkstra đề nghị để giải quyết bài toán tìm đường đi ngắn nhất từ đỉnh s đến các đỉnh còn lại của đồ thị làm việc hữu hiệu hơn rất nhiều so với thuật toán khác. Thuật toán được xây dựng trên cơ sở gán cho các đỉnh các nhãn tạm thời. Nhãn của mỗi đỉnh cho biết cận trên của độ dài đường đi ngắn nhất từ S đến nó. Các nhãn này sẽ được biến đổi theo thủ tục lặp, mà ở mỗi một bước lặp có một nhãn tạm thời trở thành nhãn cố định . Nếu nhãn của một đỉnh nào đó trở thành cố định thì nó sẽ cho ta không phải là cận trên mà là độ dài đường đi ngắn nhất từ đỉnh s đến nó. Thuật toán được mô tả như sau:
Bước 1: Khởi tao
Với đỉnh v V, gọi nhãn d[v] là độ dài đường đi ngắn nhất từ S (đỉnh bắt đầu) đến v. Ta sẽ tính các nhãn d[v].
Ban đầu d[v] được khởi gán: d[S] = 0 và d[v] = +nếu không có đường đi trực tiếp từ S đến v ngược lại d[v] = a[S,v] nếu có đường đi trực tiếp từ S đến v.
Nhãn của mỗi đỉnh có hai trạng thái tự do hay cố đinh, nhãn tự do là nhãn mà có thể tối ưu thêm được nữa, nhãn cố định là nhãn mà không thể tối ưu thêm tức là d[v] đã bằng độ dài đường đi ngắn nhất từ S đến v nên không
thể tối ưu thêm. Để làm điều này ta có thể sử dụng kỹ tuật đánh dấu Mark[v]= true hay Mark[v]= false tùy theo d[v] tự do hay cố định. Ban đầu đặt các nhãn đều tự do.
Bước 2: Lặp
- Bước lặp gồm hai thao tác:
Cố định nhãn: Chọn trong các đỉnh có nhãn tự do, lấy ra đỉnh u là đỉnh có d[u] nhỏ nhất, và cố định nhãn đỉnh d[u].
Sửa nhãn (tối ưu): Dùng đỉnh u, xét tất cả các đỉnh v và sửa lại các d[v] theo công thức: d[v] = min(d[v], d[u] + a[u,v]).
- Bước lặp sẽ kết thúc khi mà không còn đỉnh nào để cố định nữa (tức là tìm được đường đi ngắn nhất từ S đến tất cả các đỉnh còn lại), hoặc tại thao tác cố định nhãn, tất cả các đỉnh tự do đều có nhãn bằng +.
Bước 3: Kết hợp với việc lưu vết đường đi trên từng bước sửa nhãn, thông báo đường đi ngắn nhất tìm được, hoặc thông báo không tìm thấy đường đi xuất phát từ đỉnh S.
Có thể đặt câu hỏi ở bước cố định nhãn, tại sao đỉnh u như vậy được cố định nhãn, giả sử d[u] còn có thể tối ưu thêm được nữa thì tất phải có một đỉnh t mang nhãn tự do sao cho d[u] > d[t] + d[t,u]. Do trọng số a[t,u] không âm nên d[u] > d[t] , trái với cách chọn d[u] là nhỏ nhất. Tất nhiên trong lần đầu tiên thì S là đỉnh được cố định nhãn do d[S]= 0.
2.2.3.2. Thuật toán Ford Bellman
Thuật toán Ford Bellman có thể được phát biểu đơn giản như sau:
Với đỉnh xuất phát S. Gọi d[v] là khoảng cách từ S đến v với các giá trị khởi tạo là: d[S]= 0, d[v]= a[S,v] nếu có đường đi trực tiếp từ S đến v ngược lại d[v]=+ nếu không có đường đi trực tiếp từ S đến v.
Sau đó ta tối ưu hóa dần các nhãn d[v] như sau :
Xét mọi cặp đỉnh u,v của đồ thị, nếu có một cặp đỉnh u,v mà d[v] > d[u] + a[u,v], tức là nếu độ dài đường đi từ S đến v lớn hơn tổng độ dài đường đi từ S đến v cộng với
chi phí đi từ u đến v. Thì ta sẽ hủy bỏ đường đi từ S đến v đang có và coi đường đi từ S đến v chính là đường đi từ S đến u sau đó đi tiếp từ u đến v. Chú ý rằng đặt a[u,v] = + nếu (u,v) không là cung. Thuật toán sẽ kết thúc nếu như không thể tối ưu thêm bất kỳ một nhãn d[v] nào nữa.
Tính đúng của thuật toán:
- Tại bước khởi tạo thì mỗi d[v] chính là độ dài ngắn nhất của đường đi từ S tới v không quá 0 cạnh.
- Giả sử khi bắt đầu bước lặp thứ i (i ≥ 1), d[v] đã bằng độ dài đường đi ngắn nhất từ S tới v qua không quá i-1 cạnh. Do tính chất đường đi từ S tới v không quá i cạnh sẽ phải thành lập bằng cách: Lấy một đường đi từ S tới một đỉnh u nào đó qua không quá i-1 cạnh, rồi đi tiếp tới v bằng cung (u,v), nên độ dài đường đi ngắn nhất từ S tới v qua không quá i cạnh sẽ được tính bằng giá trị nhỏ nhất trong các giá trị (nguyên lý tối ưu Bellman):
Độ dài đường đi ngắn nhất từ S tới v không quá i-1 cạnh
Độ dài đường đi ngắn nhất từ S tới u không quá i-1 cạnh cộng với số cạnh (u,v), (∀u). Vì vậy sau bước lặp tối ưu các d[v] bằng công thức:
d[v]bước i = min(d[v]bước i-1, d[u]bước i-1 + c[u, v]), (∀u)
thì các d[v] sẽ bằng độ dài đường đi ngắn nhất từ S tới v qua không quá i cạnh. - Sau bước lặp tối ưu thứ n-1, ta có d[v] = độ dài đường đi ngắn nhất từ S tới v qua
không quá n-1 cạnh. Vì đồ thị không có chu trình âm nên sẽ có một đường đi ngắn nhất từ S tới v là đường đi cơ bản (qua không quá n-1 cạnh). Tức là d[v] sẽ là độ dài đường đi ngắn nhất từ S tới v.
2.2.4. Đƣờng đi ngắn giữa giữa tất cả các cặp đỉnh thuật toán Floyd
Với một đơn đồ thị có hướng, trọng số G = (V, E), n đỉnh và m cạnh. Bài toán đặt ra là hãy tính tất cả các d(i, j) là khoảng cách từ i đến j. Ta có thể áp dụng thuật toán tìm đường đi ngắn nhất như là Ford Bellman để tìm lần lượt quãng đường giữa tất cả các cặp đỉnh. Nhưng có cách làm gọn hơn mà chỉ cần 1 lần chạy có thể tìm được đường đi ngắn nhất giữa mọi cặp đỉnh trong đồ thị, và đó là thuật toán Floyd.
Tư tưởng của thuật toán như sau, ví dụ có một đường đi từ i đến j và nếu đường đi từ i tới j đang có lại dài hơn đường đi từ i tới k cộng với đường đi từ k tới j thì ta hủy bỏ đường đi từ i tới j hiện thời và coi đường đi từ i tới j sẽ là nối của hai đường đi từ i tới k rồi từ k tới j.
Thuật toán Floyd tìm đường đi ngắn nhất giữa các cặp đỉnh trong đồ thị:
Input: Đồ thị G = (V, E) được biểu diễn dạng ma trận trọng số A, và là một đồ thị có trọng số dương.
Output: Xác định độ dài đường đi dài nhất giữa mọi cặp đỉnh (i, j), kết quả lưu trong một ma trận A.
Xác định vết đường đi ngắn nhất giữa mọi cặp đỉnh và vết được lưu trong ma trận pre.
Xác định giá trị đầu cho hai ma trận A và pre
Ma trận pre lưu vết đường đi, chương trình chưa bắt đầu nên giá trị của pre được gán tất cả các giá trị pre[i, j]= j.
Giá trị của ma trận A chính là độ dài ngắn nhất giữa mọi cặp đỉnh, khởi đầu giá trị của A chính là độ dài thực trên đồ thị G(V,E). Chính là độ dài thực tế giữa hai đỉnh i và j. for i=0 to n-1 do for j=0 to n-1 do { pre[i, j]=j; if(A[i, j] ≤ 0) A[i, j]=+∞ }
Xác định độ dài đường đi ngắn nhất từ đỉnh i tới j trên đồ thị, và lưu vết đường đi qua n lần lặp.
o Thực hiện N lần lặp, sau lần lặp thứ k ma trận A sẽ chứa độ dài đường đi ngắn nhất giữa mọi cặp đỉnh đi qua các đỉnh thuộc tập
{1, 2, 3, …, k). Cứ như vậy, sau n lần lặp, ma trận A sẽ chứa độ dài đường đi ngắn nhất giữa mọi cặp đỉnh trong đồ thị.
o Giá trị A[i,j] chính là độ dài đường đi ngắn nhất giữa mọi cặp đỉnh trong đồ thị.
o Ký hiệu Ak là ma trận độ dài đường đi ngắn nhất ở lần lặp thứ k, tức là Ak[i, j] chính là độ dài đường đi ngắn nhất từ i đến j chỉ đi qua các đỉnh thuộc {1, 2, 3, …, k}. Ak[i, j] được tính theo công thức sau:
Ak[i, j]= min(Ak-1[i, j], Ak-1[i, k] + Ak-1[k, j]) o Ở bước lặp thứ k thì giá trị A[k, k] không thay đổi
for k=1 to n do for i=1 to n do for j=1 to n do
if A[i, j] > A[i, k] + A [k, j] then {
A[i, j]= A[i, k] + A[k, j]; pre[i, j]=pre[i, k]; }
2.3. Tình hình nghiên cứu thuật toán song song tìm đƣờng đi ngắn nhất 2.3.1. Tình hình nghiên cứu trên thế giới
Từ các thuật toán tuần tự áp dụng để giải quyết bài toán tìm đường đi ngắn nhất được trình bày ở mục 2.2 , thì các thuât toán song song cũng được quan tâm và phát triển, phần lớn các công trình nghiên cứu theo hướng này đều do những tác giả ngoài nước thực hiện.
Phương hướng nghiên cứu chủ yếu để xây dựng thuật toán song song tìm đường đi ngắn nhất dựa trên cơ sở các thuật toán tuần tự đã được nghiên cứu trước đó mà cụ thể là các thuật toán như: Dijkstra, Ford Bellman, Floyd…
2.3.2. Tình hình nghiên cứu trong nƣớc
Ở nước ta hiện nay, thuật toán nghiên cứu giải quyết bài toán tìm đường đi ngắn nhất chủ yếu là các thuật toán tuần tự. Các công trình nghiên cứu đều dựa vào các thuật toán tuần tự và có thể được cải tiến để trở thành một thuật toán tốt hơn. Do đó, mặc dù việc nghiên cứu các thuật toán song song để giải quyết bài toán tìm đường đi ngắn nhất đã hình thành từ lâu trên thế giới nhưng ở nước ta thì vấn đề này vẫn còn khá mới mẻ.Vì vậy mà bài nghiên cứu của nhóm tác giả sẽ nghiên cứu và song song hóa ba thuật toán tìm đường đi ngắn nhất đó là: Dijkstra, Ford Bellman, Floyd.
2.3.3. Một số công trình tiêu biểu
“Accelerating large graph algorithms on the GPU using CUDA” của nhóm tác giả
Pawan Harish và P. J. Narayanan:
Bài báo này đề cập đến các thuật toán đồ thị (Graph Algorithms) và thực thi CUDA (CUDA Implementation). Cách biểu diễn đồ thị ở đây là dùng danh sách kề
(Adjacency list).
Breadth First Search (BFS)
Giải quyết BFS dùng level synchronization. BFS đi qua đồ thị theo nhiều cấp
(level). Khi một cấp được truy cập thì không được truy cập lại lần nữa. Biên BFS (The BFS frontier) tướng ứng với tất cả các nút đang xử lý ở cấp hiện tại (current level). Không duy trì hàng đợi cho mỗi đỉnh trong quá trình BFS thực thi bởi vì sẽ gánh chịu thêm các chi phí của việc duy trì các chỉ mục của mảng mới (new array indices) và thay đổi cấu hình của lưới (grid) ở mọi cấp độ của việc thực hiện nhân (kernel). Điều này làm chậm tốc độ thực hiện của mô hình CUDA.
Cung cấp luồng (threads) ánh xạ tới đỉnh (vertexs). Hai mảng kiểu logic (kiểu boolen) là frontier và visited tương ứng là Fa và Xa có kích thước là |V| được tạo để chứa
biên BFS (BFS frontier) và các đỉnh được truy cập (visited vertices). Và một mảng số
nguyên lưu chi phí (cost) là Ca chứa số lượng tối thiểu các cạnh của mỗi đỉnh từ đỉnh nguồn S. Trong mỗi lần lặp mỗi đỉnh sẽ kiểm tra mảng Fa. Nếu là đúng thì cập nhật lại Ca và cập nhật tất cả các chi phí của cạnh lân cận nếu lớn hơn chi phí của nó cộng thêm 1
dùng danh sách cạnh Ea. Đỉnh sẽ loại bỏ giá tri của nó từ mảng Fa và thêm vào mảng Xa. Đỉnh này cũng thêm các đỉnh lân cận với nó vào mảng Fa nếu đỉnh lân cận là chưa được truy cập. Quá trình này lặp cho tới khi Fa là rỗng.
Algorithm 1: CUDA BFS (Graph G(V;E), Source Vertex S)
1: Create vertex array Va from all vertices and edge Array Ea from all edges in G(V;E),
2: Create frontier array Fa, visited array Xa and cost array Ca of size V. 3: Initialize Fa , Xa to false and Ca to
4: Fa[S]= true, Ca[S]= 0 5: while Fa not Empty do
6: for each vertex V in parallel do
7: Invoke CUDA BFS KERNEL(Va; Ea; Fa; Xa; Ca ) on the grid. 8: end for
9: end while
Algorithm 2: CUDA BFS (Va; Ea; Fa; Xa; Ca ) 1: tid = getThreadID
2: if Fa[tid] then
3: Fa[tid]= false, Xa[tid]= true 4: for all neighbors nid of tid do 5: if NOT Xa[nid] then
6: Ca[nid] = Ca[tid] + 1 7: Fa[nid]= true
8: end if