Thuật toán Ford Bellman

Một phần của tài liệu Xây dựng thuật toán song song tìm đường đi ngắn nhất với CUDA báo cáo nghiên cứu khoa học sinh viên (Trang 51)

5. Bố cục đề tài

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 { (adsbygoogle = window.adsbygoogle || []).push({});

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 9: end for 10: end if (adsbygoogle = window.adsbygoogle || []).push({});

Algorithm 1 chạy trên CPU còn Algorithm 2 chạy trên GPU. Vòng lặp while ở

Single Source Shortest Path (SSSP)

SSSP không đi qua đồ thị theo cấp như BFS. Chi phí của mỗi đỉnh đã truy cập có thể thay đổi do một con đường có chi phí thấp được tìm thấy ở mỗi lần lặp.

Thực hiện, dùng mảng Va lưu các đỉnh và Ea lưu các cạnh, một mảng đánh dấu Ma kiểu boolean kích thước |V| và một mảng trọng số Wa kích thước |E|. Trong mỗi lần lặp, kiểm tra mảng Ma, nếu đúng thì lấy chi phí hiện tại từ mảng Ca cộng với trọng số của các đỉnh lân cận từ mảng Wa. Chi phí của mỗi đỉnh lân cận được cập nhật nếu lớn hơn chi phí của đỉnh hiện tại cộng với trọng số cạnh. Chi phi mới không phản ánh trong Ca nhưng được cập nhật trong một mảng thay thế là Ua. Tại vị trí cuối việc thực thi của nhân (kernel), kernel 2 so sánh chi phí Ca với Ua nếu Ca lớn hơn Ua thì cập nhật Ca và đánh dấu Ma.

Algorithm 3 CUDA SSSP (Graph G(V;E;W), Source Vertex S)

1: Create vertex array Va , edge array Ea and weight array Wa from G(V;E;W) 2: Create mask array Ma , cost array Ca and Updating cost array Ua of size V 3: Initialize mask Ma to false, cost array Ca and Updating cost array Ua to

4: Ma[S] = true, Ca[S] =0, Ua[S] =0 5: while Ma not Empty do

6: for each vertex V in parallel do

7: Invoke CUDA SSSP KERNEL1(Va;Ea;Wa;Ma;Ca;Ua) on the grid 8: Invoke CUDA SSSP KERNEL2(Va;Ea;Wa;Ma;Ca;Ua) on the grid 9: end for

10: end while

Algorithm 4 CUDA SSSP KERNEL1(Va; Ea; Wa; Ma; Ca; Ua) 1: tid = getThreadID

2: if Ma[tid] then 3: Ma [tid]= false

4: for all neighbors nid of tid do

6: Ua[nid]= Ca[tid] + Wa[nid] 7: end if

8: end for 9: end if

Algorithm 5 CUDA SSSP KERNEL2(Va; Ea; Wa; Ma; Ca; Ua) 1: tid = getThreadID

2: if Ca[tid] > Ua[tid] then 3: Ca[tid]= Ua[tid] 4: Ma[tid]= true 5: end if

6: Ua[tid]= Ca[tid]

All Pairs Shortest Path

Đề cập đến thuật toán Floyd, dùng ma trận kề để biểu diễn đồ thị, dùng V2 luồng (threads) chạy song song. Thuật toán Floyd cũng có thể dùng O(V) luồng, mỗi luồng sẽ chạy một vòng lặp bên trong nó. Việc tiếp cận này chậm hơn nhiều so với dùng V2 luồng vì truy cập tuần tự của toàn bộ mảng bởi một luồng. Ví dụ trên đồ thị một nghìn đỉnh nó mất khoảng 9 giây so với 1 giây được thực hiện bởi Algorithm 6.

Algorithm 6 Parallel-Floyd-Warshall (G(V;E;W))

1: Create adjacency Matrix A from G(V;E;W) 2: for k from 1 to V do

3: for all Elements in the Adjacency Matrix A, where i ≥ 1; V ≥ j in parallel do 4: A[i; j]=min(A[i; j], A[i;k]+A[k; j])

5: end for 6: end for

Đoạn mã CUDA kernel sẽ được thực hiện ở dòng số bốn của Algorithm 6. Phần

“Parallel Graph Searching Algorithms using CUDA” của tác giả J. M. Kemp.

Bài báo này đề cập đến các thuật toán đồ thị như Breadth First Search (BFS),

thuật toán Dijkstra.

Breadth First Search (BFS)

Đối với BFS, ánh xạ luồng (thread) tới đỉnh thể hiện ở hình 2.11. Sử dụng một block có kích thước 512, số lượng của block và do đó threads là được tính toán thông qua số lượng đỉnh trong đồ thị đó để tìm kiếm. (adsbygoogle = window.adsbygoogle || []).push({});

Hình 2.11: Ánh xạ threads đến vertices

Cũng như mảng đồ thị, Va là mảng chứa các đỉnh và Ea là mảng chứa các cạnh, fa là mảng biên (frontier array). Mỗi đỉnh sẽ có thread riêng của mình. Thread ID (tid) là một số nguyên mà có thể được ánh xạ tới một đỉnh, đỉnh được định danh là một số nguyên (được thể hiện trong hình 2.11). Trong mỗi lần lặp của kernel, mỗi đỉnh sẽ kiểm tra trong mục fa của mình. Nếu là đúng thì đỉnh đang xét đánh dấu lại các đỉnh lân cận của nó là đúng trong fa. Kernel sẽ kiểm tra xem nếu đỉnh mục tiêu là đúng trong fa thì kernel sẽ dừng và việc tìm kiếm là hoàn tất. Nếu không quá trình này cứ tiếp tục lặp đi lặp lại cho đến khi đỉnh mục tiêu là được tìm thấy hoặc tất cả nội dung trong fa là đúng.

Ban đầu, lời gọi kernel được đặt bên trong một vòng lặp cũa đoạn mã trên host. Sau mỗi lần thực hiện kernel, fa được sao chép trở lại host để kiểm tra đỉnh mục tiêu đã

được tìm thấy hay không, hoặc nội dung trong fa là đúng. Thời gian dành cho việc sao chép dữ liệu từ device sau mỗi lần thực hiện kernel là tốn kém, do đó việc thiết kế các thuật toán đã thay đổi để giữ cho việc kiểm tra này thực hiện bên trong kernel, loại bỏ việc chuyển giao dữ liệu cải thiện đáng kể hiệu suất của thuật toán.

Thuật toán Dijkstra

Việc thực hiện thuật toán Dijkstra cũng tương tự như BFS. Tuy nhiên, cần thêm một mảng Ca lưu chi phí đường đi từ một đỉnh đến đích và một mảng cờ fla để theo dõi các phần tử trong Ca hiện đang được cập nhật. Giống như BFS, threads là được ánh xạ đến vertex với kích thước block là 512 là số lượng được tính toán thông qua số đỉnh của đồ thị. Trong quá trình thực hiện kernel, mỗi đỉnh kiểm tra trong mục fa của mình dùng thread ID (tid). Nếu giá trị là đúng thì nó đánh dấu tất cả các đỉnh lân cận trong fa là đúng. Từ đây nó đánh dấu các đỉnh có mục tương ứng trong fla, đỉnh sau đó tìm kiếm các đỉnh lân cận trong Ca và cập nhật nếu chi phí nhận được từ đỉnh hiện tại tới một đỉnh lân cận là nhỏ hơn. Tương ứng như vậy các mục tong fla là được đánh dấu là sai (false). Mục đích của fla là ngăn chặn các thread khác truy cập vào cùng một mục trong Ca. Nếu không có sự ngăn chặn này thì có thể xảy ra mâu thuẫn với hai thread cùng đọc một giá trị và cập nhật cùng một lúc, kết quả là không chính xác.

CHƢƠNG 3: XÂY DỰNG THUẬT TOÁN SONG SONG TÌM ĐƢỜNG ĐI NGẮN NHẤT VỚI CUDA

3.1. Song song hóa một số thuật toán tuần tự tìm đƣờng đi ngắn nhất 3.1.1. Song song hóa thuật toán Dijkstra

 Trước tiên ta hãy đi phân tích thuật toán Dijkstra tuần tự có thể song song hóa được những phần nào (trình bày ở mục 1.1.4 và mục 1.1.5). Từ đó ta áp dụng phương pháp trình bày ở mục 1.1.6 để thực hiện việc song song.

 Thuật toán Dijkstra tuần tự (xem ở mục 2.2.3.1)  Phần khởi tạo

for v V do

Begin

d[v]=+; // không có đướng đi từ S đến v

if a[S,v]>0 // nếu có đường đi từ S đến v thì Begin

d[v]= a[S,v]; End

Mark[v]=0; // đánh dấu đỉnh tự do (tức chưa được chọn để đi) pre[v]=S;// trước v là S

End

 Phần lặp (phần tối ưu nhãn d[v] và lưu vết pre)

Lặp n-1 lần // vì lần bước khởi tạo đã làm 1 bước rồi cho nên làm n-1 bước

còn lại Begin

Tìm đỉnh u thuộc V mà u là đỉnh tự do thỏa mãn sao cho d[u] có giá trị nhỏ nhất

Mark[u]=1; // đánh dấu u là nhãn cố định. for v V do

d[v] = d[u] + a[u,v]; // cập nhận lại nhãn d[v] pre[v]= u; // cập nhật lại trước v là u }

End

 Để song song hóa thuật toán Dijkstra trước tiên ta chia thuật toán Dijkstra thành

Một phần của tài liệu Xây dựng thuật toán song song tìm đường đi ngắn nhất với CUDA báo cáo nghiên cứu khoa học sinh viên (Trang 51)