Các thuật toán tìm đường trong lĩnh vực đồ họa máy tính theo ý nghĩa toán học, là một tập hợp các đỉnh được kết nối với nhau bởi các cạnh, một bản đồ được chia lưới trong thực tại ảo có thể được coi như một đồ thị với đỉnh là các ô, và các cạnh của đồ thị là thể hiện cho những ô liền kề nhau.
Hình 1.21. Lưới đồ thị trong thuật toán tìm đường
Bây giờ ta giả định như là đã có một lưới hai chiều. Sau này ta sẽ đề cập cụ thể tới cách thức xây dựng các lưới đồ thị khác nhau trong ứng dụng thực tại ảo.
a b b
Phần lớn các thuật toán tìm đường của lĩnh vực trí tuệ nhân tạo lẫn, lĩnh vực nghiên cứu thuật toán đều được xây dựng cho một đồ thị bất kì chứ không chỉ riêng cho các ứng dụng trong thực tại ảo. Và cũng có một số quy ước mà ta coi đó là quy ước chung, tuy nhiên thuật toán sẽ không bao giờ có thể hiểu được những quy ước này. Ví dụ như: các quy ước về hướng: như là hai điểm càng xa nhau thì nói chung là sẽ phải tốn nhiều thời gian hơn để có thể từ điểm này tới điểm kia, và sẽ không có bất cứ hố đen nào có thể giúp ta dịch chuyển tức thời từ vị trí này tới vị trí khác (đây chỉ là những giả định chung của ta, còn nếu chúng tồn tại ngược với những quy ước này của ta thì sẽ rất khó để có thể xây dựng được thuật toán hoàn chỉnh).
1.1.2.1. Thuật toán Dijkstra [3]
Hình 1.22. Đồ thị trong thuật toán tìm đường Dijkstra
Thuật toán Dijkstra thực hiện bằng cách đi qua mỗi đỉnh trong đồ thị bắt đầu từ điểm đầu của đối tượng. Nó sẽ lặp đi lặp lại việc kiểm tra các đỉnh gần nhất mà chưa được kiểm tra, và đưa nó vào danh sách các đỉnh đã được kiểm tra. Nó sẽ lan rộng dần ra ngoài với tâm là điểm đầu cho tới khi gặp được điểm đích. Thuật toán này sẽ đảm bảo cho ta việc chỉ ra được đường đi ngắn nhất từ điểm đầu tới điểm đích miễn là không có cạnh nào có giá trị âm. Trong hình trên ta có thể quan sát
thấy ô màu hồng là điểm đầu, ô màu lam là điểm đích và các ô màu lam nhạt sẽ thể hiện cho các ô mà thuật toán đã quét tới. Những ô màu lam nhạt nhất sẽ là ô xa điểm đầu nhất, và vì thế sẽ hình thành nên biên của khu vực được mở.
1.1.2.2. Thuật toán BFS (Best first search) [3]
Thuật toán này cũng hoạt động tương tự, trừ việc nó sẽ sử dụng một số ước lượng về khoảng cách từ một đỉnh nào đó tới điểm đích. Thay vì việc chọn đỉnh có khoảng cách gần nhất tới điểm đầu thì ta sẽ chọn ô có khoảng cách gần nhất tới điểm đích theo khoảng cách được ước lượng. Thuật toán này không đảm bảo cho ta là sẽ chỉ ra được đường đi ngắn nhất tuy nhiên thời gian thực hiện chương trình là nhanh hơn rất nhiều so với thuật toán Dijkstra bới chức năng tự tìm kiếm trong thuật toán này sẽ tập trung hướng đi của đối tượng luôn về phía điểm đích, giảm thiểu được khá nhiều số ô phải mở. Lấy ví dụ nếu như điểm đích nằm về phía Nam của điểm đầu thì khi đó thuật toán sẽ có khuynh hướng tập trung đường đi về
hướng Nam. Trong hình 1.23 màu vàng thể hiện cho những đỉnh có chi phí ước
lượng cao để di chuyển tới điểm đích, còn các ô màu đen thể hiện cho những đỉnh có chi phí ước lương thấp để di chuyển tới điểm đích. Nó cũng cho ta thấy được tốc độ ưu việt hơn hẳn của nó so với thuật toán Dijkstra.
Tuy vậy cả hai ví dụ mà ta vừa xem xét đều chỉ là những ví dụ hết sức đơn giản, khi mà không hề tồn tại bất kì vật cản nào trên đường đi, và đường đi ngắn nhất chỉ là một đường thẳng. Hãy cùng xem xét tới trường hợp vật cản có dạng
hình chữ “U” như trong hình 1.24, thuật toán Dijkstra sẽ thực hiện lượng tính toán
nhiều hơn nhưng nó sẽ đảm bảo cho chỉ ra được cho ta đường đi ngắn nhất.
Hình 1.24. Một tình huống vật cản có hình dạng chữ "U" đối với Dijkstra Còn trong trường hợp này, thuật toán BFS thì ngược lại thực hiện tính toán ít hơn tuy nhiên kết quả thì lại rất không tốt.
Vấn đề là ở chỗ thuật toán BFS dựa trên chính tính chất tập trung hướng đi của mình về phía điểm đích để xây dựng đường đi, và cố gắng di chuyển về hướng điểm đích mặc dù đó không phải là hướng đi đúng đắn. Khi mà thuật toán này chỉ quan tâm tới chi phí còn phải trả để đi tới điểm đích mà không quan tâm tới chi phí đã phải trả trên đường, và nó vẫn tiếp tục thực hiện đường đi kể cả khi đường đi này đã trở nên quá dài.
Từ ý tưởng liệu có thể tìm ra được một thuật toán khác kết hợp được các ưu điểm của hai thuật toán trên không? Từ suy nghĩ này thì thuật toán A* đã được đề xuất, thuật toán này sử dụng phép ước lượng của thuật toán BFS và thực hiện tính toán tương tự như thuật toán Dijkstra. Hơi khác một chút so với cách thức ước lượng của thuật toán BFS khi mà thuật toán này luôn đưa ra một đường đi xấp xỉ để giải quyết vấn đề mà không đảm bảo đuợc chắc chắn đường đi đó có phải là tốt nhất hay không, còn trong thuật toán A* thì phép ước lượng được xây dựng một cách chính xác hơn nên, mặc dù phép ước lựơng không thể tự nó chứng minh được sự đúng đắn của thuật toán nhưng trong cả thuật toán A* thì nó có thể đảm bảo được sự đúng đắn của mình.
1.1.2.3. Thuật toán A*
Trong phần này tôi sẽ tập trung vào phân tích thuật toán A*. Đây là thuật toán thông dụng nhất để giải quyết bài toán tìm đường bởi vì nó khá linh hoạt và có thể giải quyết được khá nhiều trường hợp.
A* cũng giống như những thuật toán tìm kiếm bằng đồ thị khác, nó có thể thực hiện tìm kiếm trong một bản đồ địa hình rộng lớn. Giống như thuật toán Dijkstra nó có thể chỉ ra được chính xác đường đi ngắn nhất, và cũng giống với thuật toán “BFS” nó sử dụng phương pháp ước lựơng làm tiêu chí cho việc tìm đường.
A* là phương pháp tìm kiếm tối ưu, là một phiên bản đặc biệt của thuật toán AKT áp dụng cho trường hợp đồ thị tổng quát ( AT hay AKT chỉ áp dụng cho đồ thị dạng cây).
Việc lựa chọn trạng thái tiếp theo được quyết định dựa trên 1 hàm 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 đó.
* Một số khái niệm liên quan đến thuật toán A*
MO: tập chứa các trạng thái đã được sinh ra nhưng chưa được xét đến (vì ta đã chọn một trạng thái khác). Thực ra MO là một loại hàng đợi ưu tiên (priority queue) mà trong đó, phần tử có độ ưu tiên cao nhất là phần tử tốt nhất. Người ta thường cài đặt hàng đợi ưu tiên bằng Heap.
DONG: tập chứa các trạng thái đã được xét đến. Cần lưu trữ những trạng thái này trong bộ nhớ để đề phòng trường hợp khi một trạng thái mới được tạo ra lại trùng với một trạng thái mà ta đã xét đến trước đó. Trong trường hợp không gian tìm kiếm có dạng cây thì không cần dùng tập này.
Cost(S-Si): Giá trị từ đỉnh S đến đỉnh Si .
B(n): Tập hợp các đỉnh có thể đến từ đỉnh n đang xét
Cha(ni): Trạng thái cha của trạng thái ni: Cho biết trạng thái dẫn đến trạng thái ni. Trong trường hợp có nhiều trạng thái dẫn đến nithì chọn Cha(ni) sao cho chi phí đi từ trạng thái khởi đầu đến ni là thấp nhất.
* Độ tốt của lời giải trong thuật toán A* f(n)=g(n)+h(n)
Độ tốt f của một trạng thái được tính dựa theo 2 hai giá trị mà ta gọi là g và h. h(n) là một ước lượng về chi phí từ trạng thái hiện hành cho đến trạng thái đích. Còn g(n) là "chiều dài quãng đường" đã đi từ trạng thái ban đầu cho đến trạng thái hiện tại. Lưu ý rằng g là chi phí thực sự (không phải chi phí ước lượng).
Thuật toán:
Input: Dữ liệu chứa các thông tin về bản đồ, MO = { phần tử khởi đầu}, DONG = {}
Hoặc không tìm được đường đi nếu thuật toán không tìm được đường đi và danh sách MO = {}.
Bước 1: Mở đỉnh đầu tiên:
S0 = E; {Trang thái ban đầu} g(S0) = 0;
f(S0) = g(S0)+ h(S0) ;
MO = {S0}; {Gán S0 cho tập đỉnh mở} DONG={}; {Gán tập đóng bằng rỗng} while MO {} do
Bước 2: Chọn một S trong MO với f(S) nhỏ nhất: MO= MO - {S}
DONG= DONG+ {S} {Đóng đỉnh S} Nếu S là đích thì dừng.
Ngược lại qua bước 3.
Bước 3: Xây dựng các đỉnh Si có thể đến từ S nhờ các hành động có thể chọn để thực hiện.Si sau S:
Tính g(Si) ứng với mỗi i: g(Si) = g(S) + cost(S->Si). Ước lượng h(Si)
Gán f(Si)=g(Si)+h(Si)
Bước 4: Đặt vào trong MO những Si không có trong MO lẫn trong DONG. Với các Si đã có trong MO hoặc trong DONG thì gán:
f(gi) = Min( gcũ(Si), gmới(Si) ).
If Si có trong DONG and (gcũ(Si)< gmới(Si) )then +DONG := DONG– {Si}
+MO:= MO + {Si} {Mở Si} End A
Yêu cầu :Tìm đường đi từ AG theo thuật giải A* Dựa vào thuật toán, ta tạo bảng các bước thực hiện :
n B(n) MO A: g=0, f=40. DONG A C: g=4; f=19; Cha(C)=A B: g=6; f=36; Cha(B)=A C: g=4; f=19; Cha(C)=A (min) B: g=6; f=36; Cha(B)=A A: g=0, f=40. C B: g=14; f=44; Cha(B)=C D: g=11; f=39; Cha(D)=C E: g=10; f=18; Cha(E)=C F: g=13; f=17; Cha(F)=C B: g=6; f=36; Cha(B)=A D: g=11; f=39; Cha(D)=C E: g=10; f=18; Cha(E)=C F: g=13; f=17; Cha(F)=C(min) A: g=0, f=40. C: g=4; f=19; Cha(C)=A
F E: g=18; f=26; Cha(E)=F D: g=18; f=46; Cha(D)=F G: g=15; f=15; Cha(G)=F B: g=6; f=36; Cha(B)=A D: g=11; f=39; Cha(D)=C E: g=10; f=18; Cha(E)=C G: g=15; f=15; Cha(G)=F (min) A: g=0, f=40. C: g=4; f=19; Cha(C)=A F: g=13; f=17; Cha(F)=C
Đến bước này ta chọn tiếp G, nhưng G đã thuộc tập ĐICH nên dừng chương trình
Lật ngược lại các đỉnh là Cha của đỉnh được chọn có :GFCA Vậy đường đi là : ACFG.
Dưới đây là hai ví dụ giải quyết hai bài toán đã được đề xuất ở trên nhưng với thuật toán A*.
Hình 1.26. Trường hợp đơn giản khi áp dụng A*
Trong trường hợp đơn giản này khi đường đi chỉ là đường thẳng từ điểm đầu tới điểm đích thì thuật toán có thể đưa ra được kết quả nhanh như thuật toán “BFS”.
Hình 1.27. Thuật toán A* với trường hợp vật cản có hình dạng chữ “U”
Còn trong trường hợp này, vật cản là có hình dạng chữ “U” chắn ngang đường đi. Thuật toán cũng có thể chỉ ra được chính xác đường đi ngắn nhất như thuật toán Dijkstra.
Bí mật của sự ưu việt trong thuật toán A* là sự kết hợp của hai loại thông tin trong hai thuật toán trên, sử dụng sự ưu tiên đối với các đỉnh có khoảng cách gần nhất với điểm đầu trong thuật toán Dijkstra, và sử dụng sự ưu tiên đối với các đỉnh có khoảng cách gần nhất đối với điểm đích (sử dụng các phép ước lượng). Khi nhắc tới thuật toán A* ta thường đề cập tới các thuật ngữ sau:
G(n) : thể hiện cho chi phí để đi từ điểm đầu tới một đỉnh n nào đó. H(n) : thể hiện cho chi phí ước tính để đi từ đỉnh n tới điểm đích.
Trong hình trên màu vàng thể hiện cho khoảng cách của một điểm đối với điểm đích, còn màu xanh nhạt thể hiện cho khoảng cách của ô đó với điểm đầu. Thuật toán A* sẽ cân bằng 2 giá trị đó khi nó di chuyển từ điểm đầu tới điểm đích. Qua mỗi bước di chuyển nó sẽ kiểm tra những ô có giá trị F(n) = G(n) + H(n) nhỏ nhất.