My Opera is closing 1st of March

QUANG HOANG'S BLOG

Welcome to everybody !

Tìm đường đi ngắn nhất A* trên đồ thị có thông tin định hướng

,

Tác giả: Trương Hải Nam

Trong lập trình, có lẽ các bạn đã được làm quen với khái niệm đồ thị và bài toán tìm đường đi ngắn nhất trên đồ thị nói chung. Có nhiều giải thuật liên quan đến bài toán loại này như thuật toán Dijkstra cho đồ thị đơn có trọng số của các cung (cạnh) là không âm; hoặc thuật toán Floy-Bellman cho đồ thị không có chu trình có tổng trọng số là âm. Trong bài viết này, tôi muốn giới thiệu thuật toán A* (đọc là “a sao” hoặc “a-star” trong tiếng Anh) – cách thức tìm đường heuristic dựa trên thuật toán Dijkstra và được ứng dụng rất rộng rãi trong thực tế. Cũng xin chú ý rằng một số bạn cho là heuristic đồng nghĩa với việc tìm nghiệm gần đúng, nhưng thuật toán A* lại cho chúng ta nghiệm chính xác đúng với một vài ràng buộc về hàm lượng giá.

Tại sao phải cần đến A*?

Các thuật toán cổ điển như Dijkstra được xây dựng trên các mô hình thuần tuý lý thuyết, trong đó đồ thị được xem là tập hợp các đỉnh và cung nối giữa các đỉnh đó, ngoài ra, không có bất kì thông tin gì bổ sung cho bài toán. Với mô hình lý thuyết thuần tuý như vậy, trên một đồ thị có thể tồn tại những đặc điểm ít khi xảy ra trong thực tế, chẳng hạn: có thể tồn tại một cung có trọng số lớn hơn tổng trọng số của tất cả các cung còn lại trên đồ thị, hoặc một đồ thị chẳng có cung nào. Quay lại với câu hỏi của chúng ta “Tại sao phải cần đến A*?”, câu trả lời thật đơn giản: “để giải quyết các bài toán tìm đường trong thực tế!”.

Thuật toán tìm đường Dijkstra đòi hỏi chi phí về thời gian là O(n2) cho việc tìm đường đi giữa hai cặp đỉnh bất kì; thuật toán Floy-Bellman đòi hỏi chi phí O(n) nhưng lại bắt buộc phải tìm đường đi giữa mọi cặp đỉnh trong đồ thị, dẫn đến tổng chi phí thời gian của cả thuật toán là O(n3), như vậy những thuật toán này thích hợp với các đồ thị không quá lớn (từ vài trăm tới không quá vài ngàn đỉnh). Trong khi đó, bài toán tìm đường trong thực tế thường làm việc với đồ thị có vài chục ngàn đỉnh; bù lại, các bài toán như vậy lại có thêm thông tin phụ giúp chúng ta định hướng tốt hơn trong quá trình tìm lời giải. Vấn đề ở đây là sử dụng các thông tin định hướng đó như thế nào. Ví dụ trên bài toán tìm đường, chúng ta cần tìm đường đi từ Hà Nội vào TP. Hồ Chí Minh, mọi người đều có xu hướng đi về phía Nam, vì TP. Hồ Chí Minh ở phía Nam Hà Nội, không mấy ai nghĩ đến chuyện đi lên Lạng Sơn rồi tìm đường về TP. Hồ Chí Minh (theo cách tìm kiếm mù của thuật toán Dijkstra). Một bài toán khác, trong các trò chơi chiến thuật thời gian thực, cần đi từ điểm A đến điểm B. Ở đây không thể áp dụng các thuật toán tìm đường thông thường vì bản đồ của trò chơi đôi khi có kích thước lên đến 512 x 512 ô, tương đương với một đồ thị thưa có 262.144 đỉnh; nhóm quân thường có xu hướng đi thẳng về hướng B và nếu gặp chướng ngại vật thì men theo chướng ngại vật.

Thông tin định hướng không nhất thiết chỉ là thông tin về “hướng” như hai ví dụ trên. Trong từng bài toán, thông tin này ở những hình thức khác nhau, chúng thay đổi muôn hình muôn vẻ, bản thân việc sử dụng các thông tin này như thế nào để xây dựng hàm lượng giá cũng là vấn đề lớn và thú vị. Chúng ta sẽ trở lại vấn đề này sau, còn tiếp sau đây là nội dung thuật toán A*.

Thuật toán A*

Như đã đề cập ở trên, A* là thuật toán dựa trên Dijkstra, vì vậy cũng như Dijkstra, tư tưởng tìm đường của A* dựa trên chiến lược tìm kiếm theo chiều rộng. Gần như có sự tương đương 1-1 giữa các bước thực hiện của cả hai thuật toán. Trước khi xem xét thuật toán, ta quy ước cho bài toán tìm đường đi ngắn nhất trên đồ thị G:

- u0 = đỉnh xuất phát.

- goal = đỉnh kết thúc.

- close = tập các đỉnh đã được tính toán chính xác đường đi ngắn nhất.

- open = tập các đỉnh còn lại.

- l[i,j] = trọng số của cung (i,j).

- d(i) = khoảng cách min từ i đến u0.

- v(i) = khoảng cách min ước lượng từ i đến u0.

Như vậy, nhiệm vụ đầu tiên của các thuật toán tìm đường đi ngắn nhất là phải tìm được giá trị d[goal], chúng ta hãy xem chi tiết thể hiện của hai thuật toán như dưới đây:

Thuật toán A*

d(i) = +VôCực i:[1..n]

close = [u0]

open = [1..n] - [u0]

k = u0

repeat

  { sửa đổi ước lượng min}

  i: open

    d(i) = min {d(i), d[k]+l[k,i]}

  { mở rộng tập close }

  chọn k :open để i:open

    có (d[k]+v[k])  (d(i)+v(i))

  open = open - [k]

  close = close + [k]

until goal Thuộcclose;



Thuật toán Dijkstra

d(i) = +VôCực i:[1..n]

close = [u0]

open = [1..n] - [u0]

k = u0

repeat

  { sửa đổi ước lượng min}

  i: open

    d(i) = min {d(i), d[k]+l[k,i]}

  { mở rộng tập close }

  chọn k :open để i:open

    có d[k]  d(i)

  open = open - [k]

  close = close + [k]

until goal Thuộcclose;


Sự khác biệt duy nhất của hai thuật toán ở điểm chọn đỉnh k để mở rộng tập close. Trong chiến lược tìm kiếm theo chiều rộng, việc lựa chọn trạng thái để mở rộng tìm kiếm đóng vai trò tối quan trọng. Với một trạng thái được lựa chọn tốt, có thể tìm thấy lời giải của bài toán chỉ sau số bước rất ít so với việc lựa chọn trạng thái mở rộng một cách ngẫu nhiên (lựa chọn mù). Theo chứng minh lý thuyết, nếu các trọng số của đồ thị G đều là dương (l[i,j] > 0 với mọi i¹j) và v(i) là lượng giá dương thấp hơn đường đi ngắn nhất từ đỉnh i đến u0 (0 < v(i) < d(i) với mọi i), thì thuật toán A* luôn cho kết quả đúng và không bao giờ yêu cầu nhiều thời gian hơn thuật toán Dijkstra. Có hai kết quả dễ thấy; thứ nhất, thuật toán Dijkstra có thể xem là trường hợp “suy biến” của A* trong trường hợp v(i) = 0; thứ hai, nếu v(i) là ước lượng đúng thì thuật toán A* có độ phức tạp thời gian là tuyến tính.


Vài cách xây dựng ước lượng cho bài toán ứng dụng thuật toán A*

Xây dựng hàm ước lượng cho các bài toán ứng dụng thuật toán A* là vấn đề thú vị và đôi khi rất khó. Sau đây chúng ta hãy xem xét một vài ví dụ khá đơn giản, có thể dễ dàng lý giải. Hãy xét việc phải xây dựng các ước lượng cho thuật toán A* đối với bài toán chỉ ra đường đi ngắn nhất của một quân cờ di chuyển trên bàn cờ vô tận, kẻ ô vuông và có một số ô đánh dấu không được đi vào. Chúng ta cần xây dựng v(x1,y1,x2,y2) là ước lượng của đường đi ngắn nhất từ ô (x1,y1) đến ô (x2,y2). Việc xây dựng hàm này phụ thuộc vào việc quân cờ mà chúng ta xét tới ở đây là loại gì. Sau đây tôi sẽ xây dựng kiểu lượng giá cho một vài loại quân, việc chứng minh lượng giá này là lượng giá thấp (tiêu chuẩn để thuật toán A* cho nghiệm đúng) khó dễ dàng, được dành cho bạn đọc như là bài tập:

a. Với loại quân cờ chỉ có thể dịch chuyển bước một sang một trong 4 ô có chung cạnh với ô mà nó đang đứng, có thể xây dựng hàm lượng giá đơn giản như sau:

v(x1,y1,x2,y2) = |x1-x2| + |y1-y2|

b. Với loại quân cờ là quân Vua (của cờ quốc tế), có thể dịch chuyển bước một sang một trong 8 ô có chung cạnh hoặc đỉnh với ô nó đang đứng, chúng ta có thể xây dựng hàm lượng giá như sau:

v(x1,y1,x2,y2) = max(|x1-x2|,|y1-y2|)

c. Phức tạp hơn một chút, nếu quân cờ là quân Mã, có thể dịch chuyển bước một đến một trong 8 ô chéo 1/2 với ô mà nó đang đứng, có thể xây dựng hàm lượng giá như sau (ước lượng này có thể làm chặt hơn rất nhiều một cách dễ dàng):

v(x1,y1,x2,y2) = (|x1-x2| + |y1-y2|)/3

Các ứng dụng của A*

Là thuật toán có tính định hướng cao, hàm lượng giá của thuật toán sẽ dễ dàng xây dựng nếu bản chất bài toán có tính định hướng, như trong các bài toán ví dụ ở phần trên, tất cả đều là ước lượng đường đi trên bản đồ 2 chiều. Hàm ước lượng sẽ khó xây dựng hơn rất nhiều nếu thông tin về “chiều” của bài toán là trừu tượng. Để thấy rõ được điều này, các bạn hãy thử sức với các bài tập 2 và 3 được nêu trong phần cuối của bài viết này. Trong thực tế, thuật toán A* và các biến thể của nó được sử dụng rất rộng rãi. Một trong những lĩnh vực ứng dụng thuật toán này nhiều nhất là các trò chơi chiến lược/xây dựng, tiêu biểu là “Age of Empires” của Microsoft, hậu duệ của trò chơi này là “Age of Kings” và một loạt các trò chơi cạnh tranh cùng kiểu được xây dựng trên nền một biến thể tinh vi của thuật toán A*, đó là thuật toán RA* - thuật toán A* dùng cho các ứng dụng thời gian thực. A* không phải chỉ dành cho các trò chơi ứng dụng mà còn là một thay thế hiệu quả cho thuật toán Dijsktra trong nhiều trường hợp. Nó cũng được xem như một thuật toán tìm kiếm theo chiều rộng hiệu quả. Các thuật toán tìm đường song song trên mạng (intranet/internet), bài toán tìm kiếm thông tin trên cây tri thức, cây trò chơi... đều là các biến thể hoặc dựa trên tư tưởng của thuật toán A*.



Lời kết

Việc nắm bắt các giải thuật là rất quan trọng, nhưng áp dụng các giải thuật đó như thế nào trong các bài toán thực tế là điều quan trọng hơn. Các bài toán thực tế không bao giờ trọn vẹn như trong mô hình lý thuyết, bù lại, nó lại có tính “thực tế”; sử dụng các yếu tố thực tế đó vào ứng dụng của bạn sẽ đem lại một sức sống mới cho các ứng dụng đó; chương trình của bạn sẽ “cư xử” có tính người hơn, có tính định hướng tốt hơn và qua đó sẽ thân thiện hơn với người sử dụng. Nhưng trước khi xây dựng các ứng dụng như vậy, bạn hãy thử sức mình với các bài tập dưới đây. Có thể có nhiều phương pháp giải, nhưng các bạn hãy sử dụng thuật toán A*; xây dựng một vài kiểu hàm lượng giá khác nhau và phân tích xem với từng bài toán thì kiểu lượng giá nào là tốt nhất. Chúc các bạn thành công.

1. Cho một ma trận nhị phân (các số 0 hoặc 1) kích thước m x n, với m và n trong khoảng [8, 9,..., 512]. Hai ô gọi là kề nhau nếu chúng cùng chứa số 0 và chung một cạnh. Hai ô gọi là đi được đến nhau nếu chúng có thể đi được đến nhau qua một số bước dịch chuyển qua các ô kề nhau, số bước dịch chuyển là độ dài của đường đi.

Vấn đề: Tìm đường đi có độ dài ngắn nhất từ ô [x,y] đến ô [p,q].

2. Bài thi tin học quốc tế năm 1990, tại Belorussia. Cho một bảng kích thước 4 x 4 có ghi đủ các số từ 1 đến 14, hai ô còn lại được để trống. Quy tắc chuyển hình trạng: Mỗi số đều có thể chuyển sang 1 trong 4 ô liền kề (có chung cạnh) miễn là ô đó trống. Khi đã chuyển đi, ô trước đó thành ô trống.

Vấn đề: Cho hình trạng ban đầu (ví dụ như bảng A), hãy chuyển về hình trạng đích như được cho trong bảng B sau ít bước dịch chuyển nhất.

3. Tương tự như bài số 2, nhưng bảng có 15 số từ 1 đến 15 và chỉ có 1 ô trống (chú ý rằng bài số 2 luôn tồn tại phương án giải, còn bài này thì không, vì vậy, không chỉ đơn thuần sử dụng các hàm lượng giá).

Hướng dẫn thực hành môn C++ for WindowsThuật giải Alpha-Beta

Comments

Quang Hoanghodawa Wednesday, October 4, 2006 7:16:14 AM

Giải thuật tìm kiếm A*

Trong khoa học máy tính, A* (đọc là A sao) là một thuật toán tìm kiếm trong đồ thị. Thuật toán này tìm một đường đi từ một nút khởi đầu tới một nút đích cho trước (hoặc tới một nút thỏa mãn một điều kiện đích). Thuật toán này sử dụng một "đánh giá heuristic" để xếp loại từng nút theo ước lượng về tuyến đường tốt nhất đi qua nút đó. Thuật toán này duyệt các nút theo thứ tự của đánh giá heuristic này. Do đó, thuật toán A* là một ví dụ của tìm kiếm theo lựa chọn tốt nhất (best-first search).

Thuật toán A* được mô tả lần đầu vào năm 1968 bởi Peter Hart, Nils Nilsson, và Bertram Raphael. Trong bài báo của họ, thuật toán được gọi là thuật toán A; khi sử dụng thuật toán này với một đánh giá heuristic thích hợp sẽ thu được hoạt động tối ưu, do đó mà có tên A*.



Ý tưởng trực quan

Xét bài toán tìm đường - bài toán mà A* thường được dùng để giải. A* xây dựng tăng dần tất cả các tuyến đường từ điểm xuất phát cho tới khi nó tìm 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 (informed tìm kiếm thuật toán), 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ìn 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.


Mô tả thuật toán

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
            continue
        if x = đích
            return p
        bổ sung x vào tập đóng
        foreach 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 và 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.


Các tính chất

Cũng như tìm kiếm theo chiều rộng (breadth-first search), A* là thuật toán đầy đủ (complete) theo nghĩa rằng 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 (admissible), 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(x) - g(x) + h(y)

A* còn có tính chất hiệu quả một cách tối ưu (optimally efficient) 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.
[sửa]

Quan hệ với tìm kiếm chi phí đều (uniform-cost search)

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ằng hàm h(x) = 0- với mọi x.


Độ 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:

|h(x) - h^*(x)| <= O(log h^*(x))

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 (Russell và Norvig 2003, tr. 101).

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*).

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 (admissible). Đó là tìm kiếm đệ quy theo lựa chọn tốt nhất (recursive best-first search - RBFS).


Tham khảo

* Hart, P. E.; Nilsson, N. J.; Raphael, B. (1968). “A Formal Basis for the Heuristic Determination of Minimum Cost Paths”. IEEE Transactions on Systems Science and Cybernetics SSC4 (2): pp. 100–107.
* Hart, P. E.; Nilsson, N. J.; Raphael, B. (1972). “Correction to "A Formal Basis for the Heuristic Determination of Minimum Cost Paths"”. SIGART Newsletter 37: pp. 28–29.
* Nilsson, N. J. (1980). Principles of Artificial Intelligence, Palo Alto, California: Tioga Publishing Company.
* Russell, S. J.; Norvig, P. (2003). Artificial Intelligence: A Modern Approach, pp. 97-104.


Unregistered user Friday, October 6, 2006 10:50:49 AM

TROCHI writes: Bài viết hay lắm. Xin cảm ơn. TROCHI

Unregistered user Wednesday, November 1, 2006 12:30:28 PM

U writes: Bài viết hay quá ... Tiện đay xin hỏi tác giả có tài liệu nào về bài toán tô mầu đồ thị ko ... nếu có xin share cho mình nhé ... mình cần lắm ... Cảm ơn rất nhiều ...

Write a comment

New comments have been disabled for this post.

February 2014
S M T W T F S
January 2014March 2014
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28