Last active
November 24, 2022 06:36
-
-
Save abhin4v/8172534 to your computer and use it in GitHub Desktop.
A* (A star) search in haskell
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import qualified Data.PQueue.Prio.Min as PQ | |
import qualified Data.HashSet as Set | |
import qualified Data.HashMap.Strict as Map | |
import Data.Hashable (Hashable) | |
import Data.List (foldl') | |
import Data.Maybe (fromJust) | |
astarSearch :: (Eq a, Hashable a) => a -> (a -> Bool) -> (a -> [(a, Int)]) -> (a -> Int) -> Maybe (Int, [a]) | |
astarSearch startNode isGoalNode nextNodeFn heuristic = | |
astar (PQ.singleton (heuristic startNode) (startNode, 0)) | |
Set.empty (Map.singleton startNode 0) Map.empty | |
where | |
astar pq seen gscore tracks | |
| PQ.null pq = Nothing | |
| isGoalNode node = Just (gcost, findPath tracks node) | |
| Set.member node seen = astar pq' seen gscore tracks | |
| otherwise = astar pq'' seen' gscore' tracks' | |
where | |
(node, gcost) = snd . PQ.findMin $ pq | |
pq' = PQ.deleteMin pq | |
seen' = Set.insert node seen | |
successors = | |
filter (\(s, g, _) -> not (Set.member s seen') && | |
(not (s `Map.member` gscore) || g < (fromJust . Map.lookup s $ gscore))) | |
$ successorsAndCosts node gcost | |
pq'' = foldl' (\q (s, g, h) -> PQ.insert (g + h) (s, g) q) pq' successors | |
gscore' = foldl' (\m (s, g, _) -> Map.insert s g m) gscore successors | |
tracks' = foldl' (\m (s, _, _) -> Map.insert s node m) tracks successors | |
successorsAndCosts node gcost = map (\(s, g) -> (s, gcost + g, heuristic s)) . nextNodeFn $ node | |
findPath tracks node = if Map.member node tracks | |
then findPath tracks (fromJust . Map.lookup node $ tracks) ++ [node] | |
else [node] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import qualified Data.PQueue.Prio.Min as PQ | |
import qualified Data.HashSet as Set | |
import qualified Data.HashMap.Strict as Map | |
import Data.Hashable (Hashable) | |
import Data.List (foldl') | |
import Data.Maybe (fromJust) | |
-- A* search: Finds the shortest path from a start node to a goal node using a heuristic function. | |
astarSearch :: (Eq a, Hashable a) => | |
a -- startNode: the node to start the search from | |
-> (a -> Bool) -- isGoalNode: a function to test if a node is the goal node | |
-> (a -> [(a, Int)]) -- nextNodeFn: a function which calculates the next nodes for a current node | |
-- along with the costs of moving from the current node to the next nodes | |
-> (a -> Int) -- heuristic: a function which calculates the (approximate) cost of moving | |
-- from a node to the nearest goal node | |
-> Maybe (Int, [a]) -- result: Nothing is no path is found else | |
-- Just (path cost, path as a list of nodes) | |
astarSearch startNode isGoalNode nextNodeFn heuristic = | |
astar (PQ.singleton (heuristic startNode) (startNode, 0)) | |
Set.empty (Map.singleton startNode 0) Map.empty | |
where | |
-- pq: open set, seen: closed set, tracks: tracks of states | |
astar pq seen gscore tracks | |
-- If open set is empty then search has failed. Return Nothing | |
| PQ.null pq = Nothing | |
-- If goal node reached then construct the path from the tracks and node | |
| isGoalNode node = Just (gcost, findPath tracks node) | |
-- If node has already been seen then discard it and continue | |
| Set.member node seen = astar pq' seen gscore tracks | |
-- Else expand the node and continue | |
| otherwise = astar pq'' seen' gscore' tracks' | |
where | |
-- Find the node with min f-cost | |
(node, gcost) = snd . PQ.findMin $ pq | |
-- Delete the node from open set | |
pq' = PQ.deleteMin pq | |
-- Add the node to the closed set | |
seen' = Set.insert node seen | |
-- Find the successors (with their g and h costs) of the node | |
-- which have not been seen yet | |
successors = | |
filter (\(s, g, _) -> | |
not (Set.member s seen') && | |
(not (s `Map.member` gscore) | |
|| g < (fromJust . Map.lookup s $ gscore))) | |
$ successorsAndCosts node gcost | |
-- Insert the successors in the open set | |
pq'' = foldl' (\q (s, g, h) -> PQ.insert (g + h) (s, g) q) pq' successors | |
gscore' = foldl' (\m (s, g, _) -> Map.insert s g m) gscore successors | |
-- Insert the tracks of the successors | |
tracks' = foldl' (\m (s, _, _) -> Map.insert s node m) tracks successors | |
-- Finds the successors of a given node and their costs | |
successorsAndCosts node gcost = | |
map (\(s, g) -> (s, gcost + g, heuristic s)) . nextNodeFn $ node | |
-- Constructs the path from the tracks and last node | |
findPath tracks node = | |
if Map.member node tracks | |
then findPath tracks (fromJust . Map.lookup node $ tracks) ++ [node] | |
else [node] |
Thanks a lot, this helped me complete Advent of Code 2021 day 15
https://github.com/ekenberg/AdventOfCode2021/tree/master/15
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
--Example of Use
import AStar
import Data.List
import Data.Hashable
import Data.Maybe
data Node = A | B | C | D | E | F | G deriving (Read,Show,Eq,Enum)
instance Hashable Node where
hashWithSalt s x = s + (fromJust $ elemIndex x (enumFrom A))
cost A = [(D,2),(C,3),(B,9)]
cost B = [(A,9),(C,2)]
cost C = [(A,3),(B,2),(G,5)]
cost D = [(A,2),(F,2),(E,4)]
cost E = [(D,4),(F,2),(G,4)]
cost F = [(D,3),(E,2),(G,9)]
cost _ = []
main = do
putStrLn $ show $ astarSearch B (==E) cost (const 0)