Các thuật toán tìm đƣờng

Một phần của tài liệu (LUẬN VĂN THẠC SĨ) Nghiên cứu phát triển trên hệ thống dịch vụ dựa trên vị trí địa lý và thử nghiệm (Trang 37 - 44)

Chƣơng 2 : ỨNG DỤNG LOGIC MỜ TRONG TÌM ĐƢỜNG

2.2. Các thuật toán tìm đƣờng

Trong các ứng dụng thực tế, nhƣ trong các mạng lƣới giao thông đƣờng bộ, đƣờng thuỷ hay đƣờng không. Ngƣời ta không chỉ quan tâm đến việc tìm đƣờng đi giữa hai địa điểm mà còn phải lựa chọn một hành trình “tốt nhất” (theo tiêu chuẩn nào đó nhƣ không gian, thời gian hay chi phí). Khi đó phát sinh yêu cầu tìm đƣờng đi ngắn nhất giữa hai đỉnh của đồ thị. Trong đó, các đỉnh của đồ thị tƣơng ứng với các điểm nút giao thông, các cạnh của đồ thị tƣơng ứng với các tuyến đƣờng nối các điểm nút giao thông với nhau, độ dài tuyến đƣờng hay kết hợp với các thông số khác nhƣ độ rộng, tốc độ cho phép,… đóng vai trò là trọng số của cạnh tƣơng ứng trên đồ thị. Bài toán này phát biểu dƣới dạng tổng quát nhƣ sau: Cho đồ thị có trọng số G = (V, E), hãy tìm một đƣờng đi ngắn nhất từ đỉnh xuất phát S ∈ V đến đỉnh đích F ∈ V. Độ dài của đƣờng đi này đƣợc ký hiệu là d[S, F] và gọi là khoảng cách từ S đến F. Nếu nhƣ không tồn tại đƣờng đi từ S tới F thì ta sẽ đặt khoảng cách đó = +∞.

theo chu trình này một số lần đủ lớn, ta có thể chỉ ra đƣờng đi giữa hai đỉnh nào đó trong chu trình này nhỏ hơn bất kỳ một số 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 đi không có đỉnh lặp lại) ngắn nhất. Tuy nhiên đây là một vấn đề phức tạp, không nằm trong phạm vi nghiên cứu của đề tài nên không đƣợc trình bày cụ thể trong luận văn này.

Nếu nhƣ đồ thị không có chu trình âm thì ta có thể chứng minh đƣợc rằng một trong những đƣờng đi ngắn nhất là đƣờng đi cơ bản. Và nếu nhƣ biết đƣợc khoảng cách từ S tới tất cả những đỉnh khác thì đƣờng đi ngắn nhất từ S tới F có thể tìm đƣợc một cách dễ dàng qua thuật toán sau:

Gọi c[u, v] là trọng số của cạnh [u, v]. Qui ƣớc c[v, v] = 0 với mọi v ∈ V và c[u, v] = +∞ nếu nhƣ (u, v) ∉ E. Đặt d[S, v] là khoảng cách từ S tới v. Để tìm đƣờng đi từ S tới F, ta có thể nhận thấy rằng luôn tồn tại đỉnh F1 ≠ F sao cho:

d[S, F] = d[S, F1] + c[F1, F]

(Độ dài đƣờng đi ngắn nhất S->F = Độ dài đƣờng đi ngắn nhất S->F1 + Chi phí đi từ F1 tới F)

Đỉnh F1 đó là đỉnh liền trƣớc F trong đƣờng đi ngắn nhất từ S tới F. Nếu F1≡S thì đƣờng đi ngắn nhất là đƣờng đi trực tiếp theo cung (S, F). Nếu không thì vấn đề trở thành tìm đƣờng đi ngắn nhất từ S tới F1. Và ta lại tìm đƣợc một đỉnh F2 khác F và F1 để:

d[S, F1] = d[S, F2] + c[F2, F1]

Cứ tiếp tục nhƣ vậy, sau một số hữu hạn bƣớc, ta suy ra rằng dãy F, F1, F2, … không chứa đỉnh lặp lại và kết thúc ở S. Lật ngƣợc thứ tự dãy cho ta đƣờng đi ngắn nhất từ S tới F. Tuy nhiên, ngƣời ta thƣờng không sử dụng phƣơng pháp này mà sẽ kết hợp lƣu vết đƣờng đi ngay trong quá trình tìm kiếm.

Sau đây là một số thuật toán tìm đƣờng đi ngắn nhất từ đỉnh S tới đỉnh F trên đơn đồ thị có hƣớng G = (V, E) có n đỉnh và m cung.

Thuật toán Ford-Bellman

Thuật toán Ford-Bellman áp dụng trên đồ thị không có chu trình âm và đƣợc phát biểu nhƣ sau:

Với đỉnh xuất phát S. Gọi d[v] là khoảng cách từ S tới v với các giá trị khởi tạo là:

d[S] = 0

Sau đó ta tối ƣu hoá dần các 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]+ c[u, v] thì ta đặt lại d[v] := d[u] + c[u, v].

Tức là nếu độ dài đƣờng đi từ S tới v lại lớn hơn tổng độ dài đƣờng đi từ S tới u cộng với chi phí đi từ u tới v thì ta sẽ huỷ bỏ đƣờng đi từ S tới v đang có và coi đƣờng đi từ S tới v chính là đƣờng đi từ S tới u sau đó đi tiếp từ u tới v.

Chú ý rằng ta đặt c[u, v] = +∞ nếu (u, v) không là cung. Thuật toán sẽ kết thúc khi không thể tối ƣu thêm bất kỳ một nhãn d[v] nào nữa.

Tính đúng đắn 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 qua 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 qua 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 qua không quá i - 1 cạnh

Độ dài đƣờng đi ngắn nhất từ S tới u qua không quá i - 1 cạnh cộng với trọng 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.

Do vậy số bƣớc lặp tối ƣu hoá sẽ không quá n - 1 bƣớc.

Trong cài đặt chƣơng trình, nếu mỗi bƣớc lặp đƣợc mô tả dƣới dạng: for u := 1 to n do

for v := 1 to n do

Do sự tối ƣu bắc cầu (dùng d[u] tối ƣu d[v] rồi lại có thể dùng d[v] tối ƣu d[w] nữa…) chỉ làm tốc độ tối ƣu nhãn d[v] tăng nhanh hơn nên số bƣớc lặp tối ƣu nhãn vẫn sẽ không quá n - 1 bƣớc.

Thuật toán Dijkstra

Trong trƣờng hợp trọng số trên các cung không âm, thuật toán do Dijkstra đề xuất dƣới đây hoạt động hiệu quả hơn nhiều so với thuật toán Ford-Bellman. Ta hãy xem trong trƣờng hợp này, thuật toán Ford-Bellman thiếu hiệu quả ở chỗ nào:

Với đỉnh v ∈ V, Gọi d[v] là độ dài đƣờng đi ngắn nhất từ S tới v. Thuật toán Ford-Bellman khởi gán d[S] = 0 và d[v] = +∞ với ∀v ≠ S, sau đó tối ƣu hoá dần các nhãn d[v] bằng cách sửa nhãn theo công thức: d[v] := min(d[v], d[u] + c[u, v]) với ∀u, v ∈ V. Nhƣ vậy nếu nhƣ ta dùng đỉnh u sửa nhãn đỉnh v, sau đó nếu ta lại tối ƣu đƣợc d[u] thêm nữa thì ta cũng phải sửa lại nhãn d[v] dẫn tới việc d[v] có thể phải chỉnh đi chỉnh lại rất nhiều lần. Vậy nên chăng, tại mỗi bƣớc không phải ta xét mọi cặp đỉnh (u, v) để dùng đỉnh u sửa nhãn đỉnh v mà sẽ chọn đỉnh u là đỉnh mà không thể tối ƣu nhãn d[u] thêm đƣợc nữa.

Thuật toán Dijkstra có thể mô tả như sau:

Bƣớc 1: Khởi tạo

Với đỉnh v ∈ V, gọi nhãn d[v] là độ dài đƣờng đi ngắn nhất từ S tới v. Ta sẽ tính các d[v]. Ban đầu d[v] đƣợc khởi gán nhƣ trong thuật toán Ford-Bellman (d[S] = 0 và d[v] = ∞ với ∀v ≠ S). Nhãn của mỗi đỉnh có hai trạng thái tự do hay cố định, nhãn tự do có nghĩa là có thể còn tối ƣu hơn đƣợc nữa và nhãn cố định tức là d[v] đã bằng độ dài đƣờng đi ngắn nhất từ S tới v nên không thể tối ƣu thêm.

Để làm điều này ta có thể sử dụng kỹ thuật đánh dấu: Free[v] = TRUE hay FALSE tuỳ theo d[v] tự do hay cố định. Ban đầu các nhãn đều tự do.

Bƣớc 2: Lặp

Bƣớc lặp gồm có hai thao tác:

1. 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 u.

2. Sửa nhãn: Dùng đỉnh u, xét tất cả những đỉnh v và sửa lại các d[v] theo công thức:

Bƣớc lặp sẽ kết thúc khi mà đỉnh đích F đƣợc cố định nhãn (tìm đƣợc đƣờng đi ngắn nhất từ S tới F); hoặc tại thao tác cố định nhãn, tất cả các đỉnh tự do đều có nhãn là +∞ (không tồn tại đƣờng đi).

Có thể đặt câu hỏi, ở thao tác 1, 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] + c[t, u]. Do trọng số c[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 lặp đầu tiên thì S là đỉnh đƣợc cố định nhãn do d[S] = 0.

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 cho biết không tồn tại đƣờng đi (d[F] = +∞).

Thuật toán Dijkstra sử dụng cấu trúc Heap

Nếu đồ thị có nhiều đỉnh, ít cạnh, ta có thể sử dụng danh sách kề kèm trọng số để biểu diễn đồ thị, tuy nhiên tốc độ của thuật toán Dijkstra vẫn khá chậm vì trong trƣờng hợp xấu nhất, nó cần n lần cố định nhãn và mỗi lần tìm đỉnh để cố định nhãn sẽ mất một đoạn chƣơng trình với độ phức tạp O(n). Để tăng tốc độ, ngƣời ta thƣờng sử dụng cấu trúc dữ liệu Heap để lƣu các đỉnh chƣa cố định nhãn. Heap ở đây là một cây nhị phân hoàn chỉnh thoả mãn: Nếu u là đỉnh lƣu ở nút cha và v là đỉnh lƣu ở nút con thì d[u] ≤ d[v]. (Đỉnh r lƣu ở gốc Heap là đỉnh có d[r] nhỏ nhất).

Tại mỗi bƣớc lặp của thuật toán Dijkstra có hai thao tác: Tìm đỉnh cố định nhãn và Sửa nhãn.

Với mỗi đỉnh v, gọi Pos[v] là vị trí đỉnh v trong Heap, quy ƣớc Pos[v] = 0 nếu v chƣa bị đẩy vào Heap. Mỗi lần có thao tác sửa đổi vị trí các đỉnh trên cấu trúc Heap, ta lƣu ý cập nhập lại mảng Pos này.

Thao tác tìm đỉnh cố định nhãn sẽ lấy đỉnh lƣu ở gốc Heap, cố định nhãn, đƣa phần tử cuối Heap vào thế chỗ và thực hiện việc vun đống (Adjust).

Thao tác sửa nhãn, sẽ duyệt danh sách kề của đỉnh vừa cố định nhãn và sửa nhãn những đỉnh tự do kề với đỉnh này, mỗi lần sửa nhãn một đỉnh nào đó, ta xác định đỉnh này nằm ở đâu trong Heap (dựa vào mảng Pos) và thực hiện việc chuyển đỉnh đó lên (UpHeap) phía gốc Heap nếu cần để bảo toàn cấu trúc Heap.

Thuật toán A*

thấy một đƣờng đi chạm tới đích. Tuy nhiên, cũng nhƣ tất cả các thuật toán tìm kiếm có thông tin, nó chỉ xây dựng các tuyến đƣờng "có vẻ" dẫn về phía đích.

Để biết những tuyến đƣờng nào có khả năng sẽ dẫn tới đích, A* sử dụng một "đánh giá heuristic" về khoảng cách từ điểm bất kỳ cho trƣớc tới đích. Trong trƣờng hợp tìm đƣờng đi, đánh giá này có thể là khoảng cách đƣờng chim bay - một đánh giá xấp xỉ thƣờng dùng cho khoảng cách của đƣờng giao thông.

Điểm khác biệt của A* đối với tìm kiếm theo lựa chọn tốt nhất là nó còn tính đến khoảng cách đã đi qua. Điều đó làm cho A* "đầy đủ" và "tối ƣu", nghĩa là, A* sẽ luôn luôn tìm thấy đƣờng đi ngắn nhất nếu tồn tại một đƣờng đi nhƣ thế. A* không đảm bảo sẽ chạy nhanh hơn các thuật toán tìm kiếm đơn giản hơn. Trong một môi trƣờng dạng mê cung, cách duy nhất để đến đích có thể là trƣớc hết phải đi về phía xa đích và cuối cùng mới quay lại. Trong trƣờng hợp đó, việc thử các nút theo thứ tự "gần đích hơn thì đƣợc thử trƣớc" có thể gây tốn thời gian.

Thuật toán có thể mô tả như sau:

A* lƣu giữ một tập các lời giải chƣa hoàn chỉnh, nghĩa là các đƣờng đi qua đồ thị, bắt đầu từ nút xuất phát. Tập lời giải này đƣợc lƣu trong một hàng đợi ƣu tiên (priority queue). Thứ tự ƣu tiên gán cho một đƣờng đi x đƣợc quyết định bởi hàm f(x) = g(x) + h(x).

Trong đó, g(x) là chi phí của đƣờng đi cho đến thời điểm hiện tại, nghĩa là tổng trọng số của các cạnh đã đi qua. h(x) là hàm đánh giá heuristic về chi phí nhỏ nhất để đến đích từ x. Ví dụ, nếu "chi phí" đƣợc tính là khoảng cách đã đi qua, khoảng cách đƣờng chim bay giữa hai điểm trên một bản đồ là một đánh giá heuristic cho khoảng cách còn phải đi tiếp.

Hàm f(x) có giá trị càng thấp thì độ ƣu tiên của x càng cao (do đó có thể sử dụng một cấu trúc heap tối thiểu để cài đặt hàng đợi ƣu tiên này).

function A*(điểm_xuất_phát,đích)

var đóng := tập rỗng

var q := tạo_hàng_đợi(tạo_đƣờng_đi(điểm_xuất_phát)) while q không phải tập rỗng

var p := lấy_phần_tử_đầu_tiên(q) var x := nút cuối cùng của p if x in đóng

bổ sung x vào tập đóng

for each y in các_đƣờng_đi_tiếp_theo(p) đƣa_vào_hàng_đợi(q, y)

return failure

Trong đó, các_đƣờng_đi_tiếp_theo(p) trả về tập hợp các đƣờng đi tạo bởi việc kéo dài p thêm một nút kề cạnh. Giả thiết rằng hàng đợi đƣợc sắp xếp tự động bởi giá trị của hàm f.

"Tập hợp đóng" (đóng) lƣu giữ tất cả các nút cuối cùng của p (các nút mà các đƣờng đi mới đã đƣợc mở rộng tại đó) để tránh việc lặp lại các chu trình (việc này cho ra thuật toán tìm kiếm theo đồ thị). Đôi khi hàng đợi đƣợc gọi một cách tƣơng ứng là "tập mở". Tập đóng có thể đƣợc bỏ qua (ta thu đƣợc thuật toán tìm kiếm theo cây) nếu ta đảm bảo đƣợc rằng tồn tại một lời giải hoặc nếu hàm các_đƣờng_đi_tiếp_theo đƣợc chỉnh để loại bỏ các chu trình.

Giống nhƣ tìm kiếm theo chiều rộng (breadth-first search), A* là thuật toán đầy đủ theo nghĩa nó sẽ luôn luôn tìm thấy một lời giải nếu bài toán có lời giải.

Nếu hàm heuristic h có tính chất thu nạp đƣợc, nghĩa là nó không bao giờ đánh giá cao hơn chi phí nhỏ nhất thực sự của việc đi tới đích, thì bản thân A* có tính chất thu nạp đƣợc (hay tối ƣu) nếu sử dụng một tập đóng. Nếu không sử dụng tập đóng thì hàm h phải có tính chất đơn điệu (hay nhất quán) thì A* mới có tính chất tối ƣu. Nghĩa là nó không bao giờ đánh giá chi phí đi từ một nút tới một nút kề nó cao hơn chi phí thực. Phát biểu một cách hình thức, với mọi nút x, y trong đó y là nút tiếp theo của x:

h(x) ≤ g(y) – g(x) + h(y)

A* còn có tính chất hiệu quả một cách tối ƣu với mọi hàm heuristic h, có nghĩa là không có thuật toán nào cũng sử dụng hàm heuristic đó mà chỉ phải mở rộng ít nút hơn A*, trừ khi có một số lời giải chƣa đầy đủ mà tại đó h dự đoán chính xác chi phí của đƣờng đi tối ƣu.

Độ phức tạp thuật toán:

Độ phức tạp thời gian của A* phụ thuộc vào đánh giá heuristic. Trong trƣờng hợp xấu nhất, số nút đƣợc mở rộng theo hàm mũ của độ dài lời giải, nhƣng nó sẽ là hàm đa thức khi hàm heuristic h thỏa mãn điều kiện sau:

trong đó h * là heuristic tối ƣu, nghĩa là hàm cho kết quả là chi phí chính xác để đi từ x tới đích. Nói cách khác, sai số của h không nên tăng nhanh hơn lôgarit của "heuristic hoàn hảo" h * - hàm trả về khoảng cách thực từ x tới đích.

Vấn đề sử dụng bộ nhớ của A* còn rắc rối hơn độ phức tạp thời gian. Trong trƣờng hợp xấu nhất, A* phải ghi nhớ số lƣợng nút tăng theo hàm mũ. Một số biến thể của A* đã đƣợc phát triển để đối phó với hiện tƣợng này, một trong số đó là A* lặp sâu dần (iterative deepening A*), A* bộ nhớ giới hạn (memory-bounded A* - MA*) và A* bộ nhớ giới hạn đơn giản (simplified memory bounded A*).

Thuật toán Dijkstra là một trƣờng hợp đặc biệt của A* trong đó đánh giá heuristic là một hàm hằng h(x) = 0 với mọi x.

Một thuật toán tìm kiếm có thông tin khác cũng có tính chất tối ƣu và đầy đủ nếu đánh giá heuristic của nó là thu nạp đƣợc. Đó là tìm kiếm đệ quy theo lựa chọn tốt nhất (recursive best-first search - RBFS).

Một phần của tài liệu (LUẬN VĂN THẠC SĨ) Nghiên cứu phát triển trên hệ thống dịch vụ dựa trên vị trí địa lý và thử nghiệm (Trang 37 - 44)