C#實現A*尋路

網上有關A*算法的文章已經非常豐富了,各種語言各種思路都有,本來我實在不想再寫一篇,,但因爲最近工作動盪因此專門抽空又看了一下,然後就想寫個文章防止以後印象模糊,好歹看自己寫的東西可以很快回憶起來。如果是初次接觸A*算法的朋友可以先看看這篇參考文章,我這邊只是做一個總結,然後先貼上我之前的筆記吧:

二維數組map數據: 

0=可通過的格子,S=起點,E=終點,B=障礙物
基礎定義:G=目標點到起點的步長總和 H=目標點到終點的步長總和(曼哈頓距離),F=兩總和之和
定義:OpenList,CloseList,current,adjacentCube,g(步長,這個例子假定爲1)

  1.將起點A加入到OpenList中,遍歷OpenList,每次取最小F值的item,將其賦值給current,隨後將這個item從OpenList中刪除,加入到CloseList(例子中在這裏遍歷CloseList中是否存在目標點,有的話則立刻break,我個人覺得沒必要每次都遍歷,直接判斷取到的current是否目標路徑即可)。
  2.獲取到該item四周的item(需要判斷map該位置是否爲"0"或"E",否則過濾,因爲障礙物和邊緣不能加入尋路OpenList中),設置X和Y並insert入adjacentCube中。
  3.遍歷adjacentCube,檢查每個item是否已存在於OpenList中,兩種情況:
  (1.)已存在OpenList中,則判斷如果current的G值+g步長和大於這個item的G值的話,則將這個item的parent置爲current,同時將這個item的G值更新爲current父物體的G值+g步長(主要是找一個相對最短的路徑,因爲最短路徑最終需要從終點通過parent回溯,而不是正向尋找一遍就結束了)。
   (2.)未存在OpenList中,則設置這個item的各種屬性值(G,H,F)以及parent,然後將其insert入OpenList中。
  4.重複以上步驟,直到找到目標點則break隨後開始回溯輸出。回溯的過程比較簡單,因爲之前有記錄parent,所以當前current的parent已經記錄了一條最短的路徑。

然後如果你看過原文章的代碼的話可能會發現那個變量g會有問題,源碼每次OpenList遍歷一次g值就會+1,但是有可能會有一種情況就是順着一個格子C的周圍的其中一條路徑走了一段發現是錯誤的(即F值不再是OpenList中最小的),這時需要繼續從OpenList中重新找最小F值的item繼續找找新的路徑,假如此時找到了之前C格子周圍的另一個格子的F值最小,此時就需要從這裏重新走新的路徑,新找到的這個格子的G值當然也應該重新變回之前C格子的G+g步長(這個例子中步長爲1),而不是一直自加。下面我會上我修改後的代碼,在修改的地方都有加註釋,你可以去掉源碼的註釋然後註釋掉我的代碼測試一下兩者的區別,我這裏上一下圖,在我之前說的這個例子中兩個代碼的運行結果:

源碼:修改後:

可以看到搜索的路徑差距很大,我的這個執行結果我能看懂,因爲從起點到終點最大的F值只有80,而周圍檢索不到的點F值都達到了100所以不遍歷,但是源碼的這個。。遍歷了這麼大的範圍說實話我是有點蒙圈的,可能是因爲原文章作者的那個路徑比較簡單看不太出問題,我大概想了一會沒想通,就先不糾結啦,反正肯定是和g值這個問題脫不了干係的。

然後貼一下當時實驗的代碼,其實只是整理了一下那篇參考文章的代碼,這裏只關注算法所以並沒有考慮其他東西:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AStarPathFindingTest
{
    class Location
    {
        public int X;
        public int Y;
        public int F;
        public int G;
        public int H;
        public Location Parent;
    }

    class Program
    {

        static string[] map = new string[]
        {
            "+--------+",
            "|00000000|",
            "|00000000|",
            "|0000X000|",
            "|00S0X0E0|",
            "|0000X000|",
            "|00000000|",
            "|00000000|",
            "|00000000|",
            "+--------+",
        };

        static void DrawMapToConsole()
        {
            for (var i = 0; i < map.Length; ++i)
            {
                Console.WriteLine(map[i]);
            }

        }

        static void Main(string[] args)
        {
            DrawMapToConsole();
            Location current = null;
            var start = new Location { X = 3, Y = 4 };
            var target = new Location { X = 7, Y = 4 };
            var openList = new List<Location>();
            var closedList = new List<Location>();
            //修改處,g變爲只表示步長
            //int g = 0;
            int g = 1;

            // start by adding the original position to the open list
            openList.Add(start);

            while (openList.Count > 0)
            {
                // algorithm's logic goes here
                // get the square with the lowest F score
                var lowest = openList.Min(l => l.F);
                current = openList.First(l => l.F == lowest);

                // add the current square to the closed list
                closedList.Add(current);
                // remove it from the open list
                openList.Remove(current);

                // show current square on the map
                Console.SetCursorPosition(current.X, current.Y);
                Console.Write('.');
                Console.SetCursorPosition(current.X, current.Y);
                ///System.Threading.Thread.Sleep(1000);

                // if we added the destination to the closed list, we've found a path
                if (closedList.FirstOrDefault(l => l.X == target.X && l.Y == target.Y) != null)
                    break;

                var adjacentSquares = GetWalkableAdjacentSquares(current.X, current.Y, map);
                //修改處,不能這樣用g
                // g++;

                foreach (var adjacentSquare in adjacentSquares)
                {
                    // if this adjacent square is already in the closed list, ignore it
                    if (closedList.FirstOrDefault(l => l.X == adjacentSquare.X
                            && l.Y == adjacentSquare.Y) != null)
                        continue;

                    // if it's not in the open list...
                    if (openList.FirstOrDefault(l => l.X == adjacentSquare.X
                            && l.Y == adjacentSquare.Y) == null)
                    {
                        // compute its score, set the parent
                        //修改處,不能這樣用g
                        //adjacentSquare.G = g;
                        adjacentSquare.G = g + current.G;
                        adjacentSquare.H = ComputeHScore(adjacentSquare.X,
                        adjacentSquare.Y, target.X, target.Y);
                        adjacentSquare.F = adjacentSquare.G + adjacentSquare.H;
                        adjacentSquare.Parent = current;

                        // and add it to the open list
                        openList.Insert(0, adjacentSquare);
                    }
                    else
                    {
                        // test if using the current G score makes the adjacent square's F score
                        // lower, if yes update the parent because it means it's a better path
                        //修改處
                        if (/*g*/ current.G + g < adjacentSquare.G)
                        {
                            //修改處,不能這樣用g
                            //adjacentSquare.G = g;
                            adjacentSquare.G = g + current.G;
                            adjacentSquare.F = adjacentSquare.G + adjacentSquare.H;
                            adjacentSquare.Parent = current;
                        }
                    }
                }

            }

            // assume path was found; let's show it
            while (current != null)
            {
                Console.SetCursorPosition(current.X, current.Y);
                Console.Write('_');
                Console.SetCursorPosition(current.X, current.Y);
                current = current.Parent;
                //System.Threading.Thread.Sleep(1000);
            }

            Console.ReadKey();
        }

        static List<Location> GetWalkableAdjacentSquares(int x, int y, string[] map)
        {
            var proposedLocations = new List<Location>()
                {
                    new Location { X = x, Y = y - 1 },
                    new Location { X = x, Y = y + 1 },
                    new Location { X = x - 1, Y = y },
                    new Location { X = x + 1, Y = y },
                };

            return proposedLocations.Where(
                l => map[l.Y][l.X] == '0' || map[l.Y][l.X] == 'E').ToList();
        }

        static int ComputeHScore(int x, int y, int targetX, int targetY)
        {
            return Math.Abs(targetX - x) + Math.Abs(targetY - y);
        }
    }
}

 

最後說一下上面這些只是最簡單的用曼哈頓距離+4方向實現的A*算法,因爲之前項目也只有4方向,另外還有八方向,斜邊的g值可能會跟平行豎直移動的g值步長不同,例如平行豎直移動g值步長爲10,那斜方向可能爲15。然後還有地形的不同也會影響g步長的不同,這些都算是特殊需求,如果懂了A*算法本身的話做這些修改應該不會很難。還有H值得計算方法除了曼哈頓距離,還有對角線距離,歐幾里得距離等很多計算方法,尋路也分廣度優先和深度優先。還有二叉樹的a*尋路等等等等。。。這些如果到時候做項目的時候有需求我就再整理相關文章。另外還有一篇比較有意思的參考文章,這個系列主要說明和解決了一些使用a*尋路時遇到的一些坑,大概看了一下感覺對以後入坑時會很有幫助,所以一併貼出來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章