Critique my generic A* pathfinding algorithm

Started by
27 comments, last by Narf the Mouse 14 years ago
Quote:Original post by Narf the Mouse
Unfortunately, the only one I found in .Net 4.0 is SortedSet, which does almost what I need. The problem is, it doesn't allow multiple inserts of the same data - Or rather, it simply doesn't insert them and returns false on an add.

I've thought it before and I'll think it again - .Net is a leetle too light on self-sorting collections.

Looks like it's time to build one...Unless someone's found one I missed?


Hm, yeah, I can't find such a container either. I'm coming from C++ (multiset) and Java (PriorityQueue) so I didn't know C# didn't have a good equivalent.

This looks like a good piece of code for a C# multiset: http://www.ikriv.com/en/prog/code/Set/Set.html
Advertisement
Thanks - Yeah, my brain is exhausted, but I can't sleep, so...

In any case, I ran it in the following testbed and got this result:
Loops: 1000, Seconds: 86.5001775, Loops per Second: 11.5606699188565

Seems kinda slow to me.

Testbed code:
        static System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();        static Random random = new Random();        static void Main(string[] args)        {            stopwatch.Start();            Int32 t = 0,                count = 1000;            Int32 x, y,                sizeX = 20, sizeY = 20;            Boolean[,] pathabilityMap = new Boolean[sizeX, sizeY];            Double seconds = 0.0;            for (t = 0; t < count; ++t)            {                for (y = 0; y < sizeY; ++y)                {                    for (x = 0; x < sizeX; ++x)                    {                        pathabilityMap[x, y] = random.NextDouble() >= 0.4;                    }                }                AIExtensions.AStarNS.GraphMap2D graph = new AIExtensions.AStarNS.GraphMap2D(passabilityMap: pathabilityMap);                graph.SetStart(new NetExtensions.VariablesNS.Point(0, 0));                graph.SetGoal(new NetExtensions.VariablesNS.Point(19, 19));                AIExtensions.AStarNS.AStar.AStarData data = new AIExtensions.AStarNS.AStar.AStarData(graph: graph);                Double startTime = stopwatch.Elapsed.TotalSeconds;                AIExtensions.AStarNS.AStar.Pathfind(data: ref data);                seconds += stopwatch.Elapsed.TotalSeconds - startTime;            }            Console.WriteLine(Handy.LoopString(count, seconds));            Console.ReadKey(true);        }
Don't time the whole thing, just time how long the path-find call takes.

Compare it to your original path-find call.
Quote:Original post by nullsquared
Don't time the whole thing, just time how long the path-find call takes.

Compare it to your original path-find call.

Yeah...That's exactly what I'm doing.

Will do that.

[Edited by - Narf the Mouse on April 1, 2010 10:25:57 PM]
Quote:Original post by Narf the Mouse
Quote:Original post by nullsquared
Don't time the whole thing, just time how long the path-find call takes.

Compare it to your original path-find call.

Yeah...That's exactly what I'm doing.

Woops, sorry, didn't notice you explicitly subtract the start time.
Testbed is up to:
Loops: 1000, Seconds: 8.2598843, Loops per Second: 121.067071121081

Although it goes down to 12 LPS on 40x40.

My AStar class: The current version is the result of iterative tests - Yes, the List.Sort works, at worse, no worse than the HeapSorted. So I'm using the List.

My HeapSorted class is in the other thread.
    public static class AStar    {        public struct AStarData : IAnytime<AStarNode, List<GraphNode>, Double>        {            public AStarData(Graph graph, Double maxRuntime = -1.0, Int32 maxSteps = -1)            {                this.graph = graph;                this.runtime = 0.0;                this.steps = 0;                this.current = graph.Start;                this.maxRuntime = maxRuntime;                this.maxSteps = maxSteps;                // open = new HeapSorted<AStarNode>(this.current, false);                open = new List<AStarNode>();                closed = new HashSet<AStarNode>();            }            private Graph graph;            public Graph Graph { get { return graph; } }            private List<AStarNode> open;            public List<AStarNode> Open { get { return open; } }            private HashSet<AStarNode> closed;            public HashSet<AStarNode> Closed { get { return closed; } }            private Double maxRuntime;            public Double MaxRuntime { get { return maxRuntime; } set { maxRuntime = value; } }            private Int32 maxSteps;            public Int32 MaxSteps { get { return maxSteps; } set { maxSteps = value; } }            private Double runtime;            public Double Runtime { get { return runtime; } set { runtime = value; } }            private Int32 steps;            public Int32 Steps { get { return steps; } set { steps = value; } }            private AStarNode current;            public AStarNode Current { get { return current; } set { current = value; } }            public List<GraphNode> Result { get { return current.Path(); } }            public double Accuracy { get { return current.F; } }        }        /// <summary>        /// Pathfinds on any mappable graph.        /// </summary>        /// <typeparam name="T">The type of data the graph contains.</typeparam>        /// <param name="graph">An abstract Graph class.</param>        /// <param name="start">The start data.</param>        /// <param name="goal">The end data.</param>        /// <param name="path">The output path.</param>        /// <returns>Returns true if a path is found.</returns>        public static Boolean Pathfind(Graph graph, dynamic start, dynamic goal, out List<GraphNode> path)        {            graph.SetStart(start);            graph.SetGoal(goal);            return Pathfind(graph, out path);        }        /// <summary>        /// Pathfinds on any mappable graph.        /// </summary>        /// <typeparam name="T">The type of data the graph contains.</typeparam>        /// <param name="graph">An abstract Graph class.</param>        /// <param name="path">The output path.</param>        /// <returns>Returns true if a path is found.</returns>        public static Boolean Pathfind(Graph graph, out List<GraphNode> path)        {            AStar.AStarData data = new AStarData(graph);            Boolean check = Pathfind(ref data);            path = new List<GraphNode>();            if (!check)            {                if (data.Closed.Count > 0)                {                    path = data.Current.Path();                }            }            else                path = data.Current.Path();            return check;        }        public static Boolean Pathfind(ref AStar.AStarData data)        {            // Get the graph's Start node.            AStarNode S = data.Graph.Start;            // Add the Start node to the Open list.            data.Open.Add(item: S);            // Continue until we have no more Open nodes.            bool check = false;            data.Runtime = 0.0;            data.Steps = 0;            while (data.Open.Count > 0 && !check                && (data.MaxRuntime == -1 || data.Runtime < data.MaxRuntime)                && (data.MaxSteps == -1 || data.Steps < data.MaxSteps))            {                check = PathFindInner(ref data);            }            if (!check && data.Open.Count == 0)            {                data.Closed.RemoveWhere(a => { return Object.Equals(a.Parent, null); });                if (data.Closed.Count > 0)                {                    data.Current = data.Closed.OrderBy<AStarNode, Double>(a => { return a.H; }).ElementAt(0);                }                return false;            }            // If the While loop ends, we have no more Open nodes to check, so failure.            return check;        }        private static bool PathFindInner(ref AStar.AStarData data)        {            Double startRuntime = NetExtensions.GeneralData.Stopwatch.Elapsed.TotalSeconds;            // Sort from lowest F-value (Estimated distance to solution)            // to highest F-value.            data.Open.Sort((a, b) => { if (a.F < b.F) return -1; else if (a.F > b.F)return 1; else return 0; });            // SortedSet<AStarNode> test = new SortedSet<AStarNode>(data.Current);            // Get the best node in the open list and remove it from            // that list.           do           {                // If there's nothing left in the open list, we've checked                 // everywhere and there's no solution. Exit, returning false.                if (data.Open.Count == 0)                {                    data.Closed.RemoveWhere(a => { return Object.Equals(a.Parent, null); });                    if (data.Closed.Count > 0)                    {                        data.Current = data.Closed.OrderBy<AStarNode, Double>(a => { return a.H; }).ElementAt(0);                    }                    return false;                }                data.Current = data.Open[0];                data.Open.RemoveAt(0);                // data.Current = data.Open.RemoveLowest();            } while (data.Closed.Contains(data.Current));            data.Closed.Add(data.Current);            // If it's the destination,            if (data.Current.Data == data.Graph.Goal.Data)            {                // Get the path to this node and return true.                return true;            }            // Parse through all of the graph nodes linked to B.            foreach (AStarNode C in data.Current.Links)            {                if (!data.Closed.Contains(C))                {                    // Make it's parent B,                    C.Parent = data.Current;                    // Calculate it's values                    C.Recalculate();                    // and add it to the Open list.                    // Int32 n = data.Open.FindIndex(a => { return a.F >= C.F; });                    data.Open.Add(C);                    /* if (n > -1)                        data.Open.Insert(n, C);                    else                        data.Open.Insert(0, C); */                }            }            data.Runtime += NetExtensions.GeneralData.Stopwatch.Elapsed.TotalSeconds - startRuntime;            ++data.Steps;            return false;        }    }
Okay, something is definitely wrong with your heap code then.

There is no way that running a sort, which is *at least* O(nlogn) (and quicksort in particular will perform like O(n^2) on mostly-sorted-already data) will be faster than inserting into a heap, which is usually O(logn).
Yeah - I found out I don't need to Heapify on an Add, just Sift. That brings it to 200/second. Properly implementing it as a binary heap should allow me to do something similar on GetNext (Formerly RemoveLowest), which, according to preliminary tests, will bring it to 400/second.

I suspect the code I got was not a proper binary heap.
Well, this looks as good as I could reasonably get it. I could maybe trim the Closed list based on wether all the links are closed, but that's rather context-dependent. The only optimization left that I can see is the binary tree. (Rhyme not intended)

Thanks. :)

        public static Boolean Pathfind(ref AStar.AStarData data)        {            // Get the graph's Start node.            AStarNode S = data.Graph.Start;            // Add the Start node to the Open list.            data.Open.Add(item: S);            // Continue until we have no more Open nodes.            bool check = false;            data.Runtime = 0.0;            data.Steps = 0;            while (data.Open.Count > 0 && !check                && (data.MaxRuntime == -1 || data.Runtime < data.MaxRuntime)                && (data.MaxSteps == -1 || data.Steps < data.MaxSteps))            {                check = PathFindInner(ref data);            }            if (!check && data.Open.Count == 0)            {                data.Closed.RemoveWhere(a => { return Object.Equals(a.Parent, null); });                if (data.Closed.Count > 0)                {                    data.Current = data.Closed.OrderBy<AStarNode, Double>(a => { return a.H; }).ElementAt(0);                }                return false;            }            // If the While loop ends, we have no more Open nodes to check, so failure.            return check;        }        private static bool PathFindInner(ref AStar.AStarData data)        {            Double startRuntime = NetExtensions.GeneralData.Stopwatch.Elapsed.TotalSeconds;            // Sort from lowest F-value (Estimated distance to solution)            // to highest F-value.            // data.Open.Sort((a, b) => { if (a.F < b.F) return -1; else if (a.F > b.F)return 1; else return 0; });            // SortedSet<AStarNode> test = new SortedSet<AStarNode>(data.Current);            // Get the best node in the open list and remove it from            // that list.           do           {                // If there's nothing left in the open list, we've checked                 // everywhere and there's no solution. Exit, returning false.                if (data.Open.Count == 0)                {                    data.Closed.RemoveWhere(a => { return Object.Equals(a.Parent, null); });                    if (data.Closed.Count > 0)                    {                        data.Current = data.Closed.OrderBy<AStarNode, Double>(a => { return a.H; }).ElementAt(0);                    }                    return false;                }                // data.Current = data.Open[0];                // data.Open.RemoveAt(0);                data.Current = data.Open.GetNext();            } while (data.Closed.Contains(data.Current));            data.Closed.Add(data.Current);            // If it's the destination,            if (data.Current.Data == data.Graph.Goal.Data)            {                // Get the path to this node and return true.                return true;            }            // Parse through all of the graph nodes linked to B.            foreach (AStarNode C in data.Current.Links)            {                if (!data.Closed.Contains(C))                {                    // Make it's parent B,                    C.Parent = data.Current;                    // Calculate it's values                    C.Recalculate();                    // and add it to the Open list.                    // Int32 n = data.Open.FindIndex(a => { return a.F >= C.F; });                    data.Open.Add(C);                    /* if (n > -1)                        data.Open.Insert(n, C);                    else                        data.Open.Insert(0, C); */                }            }            data.Runtime += NetExtensions.GeneralData.Stopwatch.Elapsed.TotalSeconds - startRuntime;            ++data.Steps;            return false;        }

This topic is closed to new replies.

Advertisement