Thuật toán Dijkstra

Một phần của tài liệu Các thuật toán tìm đường đi ngắn nhất trong đồ thị lý thuyết, thuật toán và ứng dụng (Trang 34 - 40)

Thuật toán Dijkstra giải bài toán đường đi ngắn nhất từ một đỉnh trên một dồ thị định hướng G = (V,E) cho trường hợp trong đó mọi cạnh của đồ thị đều có trọng số không âm. Trong phần này, chúng ta sẽ giả sử w(u,v) 0 với mọi cạnh (u,v) E.

Thuật toán Dijkstra duy trì một tập S các đỉnh mà trọng số đường đi ngắn nhất của nó từ đỉnh s đã được xác định. Thuật toán lặp lại việc chọn một đỉnh u V - S

với đánh giá đường đi ngắn nhất nhỏ nhất, bổ sung u vào S, và giãn tất cả các cạnh đi ra khỏi u. Trong cài đặt sau đây, chúng ta sử dụng một hàng đợi ưu tiên nhỏ nhất

Số hóa bởi Trung tâm Học liệu - ĐHTN http://www.lrc-tnu.edu.vn/ DIJKSTRA(G, w, s) 1. INITIALIZE-SINGLE-SOURCE(G, s) 2. S 3. QV[G] 4. While Q 5. do u EXTRACT-MIN(Q) 6. SS {u}

7. for mỗi đỉnh u Adj[u] 8. do RELAX(u,v,w)

Thuật toán Dijkstra giãn tất cả các cạnh như minh hoạ trong hình 2.6. Dòng 1 thực hiện thủ tục khởi tạo các giá trị của d và  như thường lệ và dòng 2 khởi tạo tập S rỗng. Thuật toán duy trì một bất biến là Q = V – S tại điểm bắt đầu của mỗi vòng lặp while từ dòng 4 đến 8. Dòng 3 khởi tạo hàng đợi ưu tiên nhỏ nhất Q để chứa tất cả các đỉnh của V; do S =  tại thời điểm đó, bất biến là đúng sau dòng 3. Mỗi lần chạy qua vòng lặp while từ dòng 4 đến 8, một đỉnh u được lấy ra từ Q = V - S và được bổ sung vào S, và do vậy duy trì được tính bất biến. (Lần đầu tiên chạy qua vòng lặp này, u = s.) Đỉnh u, do vậy là đỉnh có đánh giá đường đi nhỏ nhất trong tập V – S. Sau đó, các dòng 7-8 giãn mỗi cạnh (u,v) đi ra khỏi u, cập nhật đánh giá d[v] và đỉnh trước [v] nếu như đường đi ngắn nhất đến v có thể được cải thiện bằng cách đi qua u. Quan sát ta sẽ thấy các đỉnh không bao giờ được chèn vào Q

sau dòng 3 và một đỉnh được lấy ra từ Q và bổ sung vào S đúng một lần. Do vậy vòng lặp while trên các dòng 4-8 lặp chính xác |V| lần.

Số hóa bởi Trung tâm Học liệu - ĐHTN http://www.lrc-tnu.edu.vn/

Hình 2.6 Thực hiện thuật toán Dijkstra.Đỉnh nguồn s là đỉnh bên trái nhất. Các đánh giá đường đi ngắn nhất được ghi bên trong các đỉnh, và các cạnh tô đậm chỉ ra các giá trị đỉnh trước. Các đỉnh màu đen là các đỉnh trong tập S, các đỉnh màu trắng là các đỉnh trong hàng đợi ưu tiên nhỏ nhất Q = V – S. (a) Tình huống ngay trước lần lặp đầu tiên của vòng lặp while ở các dòng 4-8. Các đỉnh tô đậm có giá trị d nhỏ nhất và được chọn làm đỉnh u trong dòng 5. (b)-(f) Tình huống sau mỗi lần lặp liên tiếp của vòng lặp while. Các đỉnh tô đậm trong mỗi phần được chọn làm đỉnh u trong dòng 5 của lần lặp tiếp theo. Các giá trị d và  trong phần (f) là các giá trị cuối cùng.

Vì thuật toán Dijkstra luôn chọn đỉnh gần s nhất trong tập V - S để bổ sung vào tập S, thuật toán Dijkstra thực chất sẽ tạo ra các đường đi ngắn nhất. Điểm mấu chốt là chỉ mỗi khi đỉnh u được bổ sung vào đỉnh S, ta có d[u] = (s,u).

Hình 2.7Chứng minh định lý 2.6. Tập S là không rỗng ngay trước khi đỉnh u được

Số hóa bởi Trung tâm Học liệu - ĐHTN http://www.lrc-tnu.edu.vn/

thành sp1 xyp2 u

, trong đó y là đỉnh đầu tiên trên đường đi không nằm trong S và x S là đỉnh nằm ngay trước y. Các đỉnh x và y là phân biệt, nhưng chúng ta có thể có s = x hoặc y = u. Đường đi p2 có thể quay trở vào tập S hoặc không.

Định lý 2.6 (Tính đúng đắn của thuật toán Dijkstra)

Thuật toán Dijkstra, chạy trên một đồ thị định hướng, có trọng số G = (V,E) với hàm trọng số không âm w và đỉnh nguồn s, kết thúc với d = (s,v) với mọi đỉnh

uV.

Chứng minh

Chúng ta sử dụng bất biến sau đây của vòng lặp.

Tại thời điểm bắt đầu vòng lặp while từ dòng 4 đến 8, d[v] =(s,v) với mỗi đỉnh

v S.

- Khởi tạo: đầu tiên, S= , và do đó bất biến là đúng.

- Duy trì: chúng ta sẽ chứng minh rằng trong mỗi lần lặp, d[u] =(s,u) đối với đỉnh u được bổ sung vào S. Giả thiết phản chứng, giả sử u là đỉnh đầu tiên mà đối với nó d[u] (s,u) khi nó được bổ sung vào S. Chúng ta sẽ tập trung vào tình huống tại thời điểm khởi đầu của một lần lặp của vòng lặp while trong đó u được bổ sung vào S và suy ra một điều mâu thuẫn rằng d[u] =(s,u) trong thời điểm bằng cách xét một đường đi ngắn nhất từ s đến u. Chúng ta phải có uss là đỉnh đầu tiên được bổ sung vào Sd(s) = (s,s) = 0 tại thời điểm đó. Vì u khác s, chúng ta cũng có

S ngay trước khi u được bổ sung vào S. Như vậy phải có một đường đi nào đó từ

s đến u, vì nếu ngược lại d[u] = (s,u) =  theo tính chất không tồn tại đường đi, điều đó mâu thuẫn với giả thiết d[u] (s,u). Vì có ít nhất một đường đi nên sẽ có một đường đi ngắn nhất p từ s đến u. Trước khi bổ sung u vào S, đường đi u nối một đỉnh trong S, (đỉnh s), và một đỉnh trong V - S (đỉnh u). Chúng ta hãy xét đỉnh đầu tiên y trong p sao cho yVS, và gọi xS là đỉnh trước của nó. Như minh hoạ trong hình vẽ 2.7, đường đi p có thể được tách ra thành sp1 xyp2 u (Các đường đi p1 và p2 đều có thể không có cạnh nào).

Số hóa bởi Trung tâm Học liệu - ĐHTN http://www.lrc-tnu.edu.vn/

Chúng ta sẽ chứng tỏ rằng d[y] = (s,y) khi u được bổ sung vào S. Để chứng minh điều này, chú ý rằng xS. và u được chọn là đỉnh đầu tiên sao cho d[u] (s,u) khi nó được bổ sung vào s, chúng ta có d[x] = (s,x) khi x được bổ sung vào S. Cạnh (x,y) được giãn vào thời điểm đó, nên ta suy ra d[y] = (s,y) khi u được bổ sung vào S theo tính chất hội tụ.

Bây giờ chúng ta có thể suy ra được d[u] = (s,u). Vì y xuất hiện trước u trên đường đi ngắn nhất từ s đến u và mọi cạnh của đồ thị đều có trọng số không âm, nên đường đi p2 cũng có trọng số không âm, và do vậy suy ra (s,y) (s,u), và do vậy ta có

d[y] = (s,y) (s,u) d[u] (theo tính chất chặn trên) (2.2)

Nhưng vì cả hai đỉnh u và y đều nằm trong V - S khi u được chọn trong dòng 5, chúng ta có d[u] d[y]. Như vậy từ hai bất đẳng thức trong (2.2) thực ra là các đẳng thức và do vậy ta có d[y] = (s,y) = (s,u) = d[u]

Do vậy, d[u] = (s,u), mâu thuẫn với cách chọn u của chúng ta. Chúng ta có thể kết luận rằng d[u] = (s,u) khi u được bổ sung vào S, và đẳng thức này được duy trì tại mọi thời điểm sau đó khi một đỉnh u nào đó được bổ sung vào S.

Kết thúc: Tại thời điểm kết thúc, Q= , cùng với bất biến chúng ta đã thấy rằng Q

= VS, điều đó có nghĩa là S = V. Do vậy, d[u] = (s,u) với mọi đỉnh uV.

Hệ quả 2.7

Nếu chúng ta chạy thuật toán Dijkstra trên một đồ thị định hướng có trọng số G

= (V,E) với một hàm trọng số không âm w và một đỉnh nguồn s, khi kết thúc thuật toán, đồ thị đỉnh trước G sẽ là một cây đường đi ngắn nhất có gốc là s.

Chứng minh

Trực tiếp suy ra từ định lý 2.6 và tính chất đồ thị con đỉnh trước.

Phân tích

Thuật toán Dijkstra . Nó duy trì hàng đợi ưu tiên nhỏ nhất bằng cách gọi 3 toán tử hàng đợi ưu tiên: INSERT (ẩn trong dòng 3), EXTRACT-MIN (dòng 5), và DECREASE-KEY (ẩn trong RELAX, được gọi trong dòng 8). INSERT được gọi một lần cho mỗi đỉnh, cũng giống như với EXTRACT-MIN. Vì mỗi đỉnh vV được

Số hóa bởi Trung tâm Học liệu - ĐHTN http://www.lrc-tnu.edu.vn/

bổ sung vào tập S đúng một lần, mỗi cạnh trong danh sách kề của nó Adj[v] được duyệt (trong vòng lặp for ở các dòng 7-8) đúng một lần trong quá trình thực hiện thuật toán. Vì tổng số cạnh trong tất cả các danh sách kề là |E|, vậy có tổng số |E| lần lặp trong vòng lặp for, và như vậy có tổng số tối đa |E| thao tác DECREASE-KEY. (Lưu ý ở đây một lần nữa chúng ta lại sử dụng lập luận theo tập hợp).

Thời gian chạy của thuật toán Dijkstra phụ thuộc vào việc hàng đợi ưu tiên nhỏ nhất được cài đặt như thế nào.Đầu tiên xét trường hợp chúng ta duy trì hàng đợi ưu tiên nhỏ nhất bằng cách tận dụng các đỉnh được đánh số từ 1 đến |V|. Chúng ta đơn giản lưu trữ d[v] trong ô thứ v của một mảng. Mỗi thao tác INSERT và DECREASE-KEY cần O(1) thời gian, và mỗi thao tác EXTRACT-MIN cần O(V)

thời gian (vì chúng ta phải tìm kiếm trên toàn bộ mảng), như vậy toàn bộ thời gian cần thiết sẽ là O(𝑉2 + E) = O(V)

Nếu đồ thị khá thưa trong trường hợp riêng E = O(𝑉2/𝑙𝑔𝑉) thực tế chúng ta nên cài đặt hàng đợi ưu tiên nhỏ nhất với một min-heap nhị phân. Mỗi thao tác EXTRACT-MIN cần O(lgV). Như trước, có |V| thao tác như vậy. Thời gian để xây dựng một min-heap là O(V). Mỗi thao tác DECREASE-KEY cần O(lgV), và vẫn có tối đa |E| thao tác như vậy. Tổng thời gian chạy là O((V+E)lgV),O(ElgV) nếu như mọi đỉnh đều đến được từ đỉnh nguồn. Thời gian chạy này là một cải tiến trường hợp chung O(𝑉2) khi trường hợp E = O(𝑉2/lgV).

Trong thực tế, chúng ta có thể đạt đến một thời gian chạy O(VlgV + E) bằng cách cài đặt hàng đợi ưu tiên nhỏ nhất với một Fibonaci heap. Các giá cho mỗi một trong |V| thao tác EXTRACT-MIN là O(lgV) và mỗi lời gọi DECREASE-KEY (tối đa |E| lời gọi) chỉ cần O(1). Trong lịch sử, cấu trúc heap Fibonaci được phát triển dựa trên việc quan sát rằng thuật toán Dijkstra thường có nhiều lời gọi đến DECREASE-KEY hơn lời gọi đến EXTRACT-MIN, nên bất kì việc giảm thời gian của mỗi thao tác DECREASE-KEY nào về O(lgV) mà không tăng thời gian ETRACT-MIN đều dẫn đến một thuật toán về mặt tiệm cận nhanh hơn heap nhị phân.

Số hóa bởi Trung tâm Học liệu - ĐHTN http://www.lrc-tnu.edu.vn/

Một phần của tài liệu Các thuật toán tìm đường đi ngắn nhất trong đồ thị lý thuyết, thuật toán và ứng dụng (Trang 34 - 40)