packages feed

search-algorithms 0.3.3 → 0.3.4

raw patch · 4 files changed

+145/−71 lines, 4 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Algorithm.Search: dijkstraAssocCost :: (Num cost, Ord cost, Ord state) => ((state, cost) -> [(state, cost)]) -> (state -> Bool) -> state -> Maybe (cost, [state])
+ Algorithm.Search: dijkstraAssocCostM :: (Monad m, Num cost, Ord cost, Ord state) => ((state, cost) -> m [(state, cost)]) -> (state -> m Bool) -> state -> m (Maybe (cost, [state]))

Files

CHANGELOG.md view
@@ -4,6 +4,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.3.4] - 2025-06-28+- Add `dijkstraAssocCost` and `dijstraAssocCostM`, which make it easier to+  handle path-dependent costs.+- Fix bug in `dijkstraAssocM`, where the prior cost was not propagated to new states.++## [0.3.3] - 2024-11-08+- Add `pruningAssoc` and `pruningAssocM`, which allow for easy pruning of states based on cost.+- Add monadic versions of `dijkstraAssoc` and `astarAssoc`+ ## [0.3.2] - 2021-12-27 - Add two new functions, `dijkstraAssoc` and `aStarAssoc`. These allow for the simultaneous   computation of neighboring states and their costs. (Thank you to@@ -29,6 +38,8 @@ ## 0.1.0 - 2017-03-07 - Initial release +[0.3.4]: https://github.com/devonhollowood/search-algorithms/compare/v0.3.3...v0.3.4+[0.3.3]: https://github.com/devonhollowood/search-algorithms/compare/v0.3.2...v0.3.3 [0.3.2]: https://github.com/devonhollowood/search-algorithms/compare/v0.3.1...v0.3.2 [0.3.1]: https://github.com/devonhollowood/search-algorithms/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/devonhollowood/search-algorithms/compare/v0.2.0...v0.3.0
search-algorithms.cabal view
@@ -1,5 +1,5 @@ name:                search-algorithms-version:             0.3.3+version:             0.3.4 synopsis:            Common graph search algorithms description:         Library containing common graph search algorithms,                      including depth-first and breadth-first searches,
src/Algorithm/Search.hs view
@@ -14,6 +14,7 @@   dfs,   dijkstra,   dijkstraAssoc,+  dijkstraAssocCost,   aStar,   aStarAssoc,   -- * Monadic Searches@@ -22,6 +23,7 @@   dfsM,   dijkstraM,   dijkstraAssocM,+  dijkstraAssocCostM,   aStarM,   aStarAssocM,   -- * Utility@@ -40,6 +42,7 @@ import qualified Data.Foldable as Foldable import Data.Functor.Identity (Identity(..)) import Control.Monad (filterM, zipWithM)+import Data.Tuple (swap)  -- | @bfs next found initial@ performs a breadth-first search over a set of -- states, starting with @initial@, and generating neighboring states with@@ -68,10 +71,10 @@   -> Maybe [state]   -- ^ First path found to a state matching the predicate, or 'Nothing' if no   -- such path exists.-bfs =+bfs next found  =   -- BFS is a generalized search using a queue, which directly compares states,   -- and which always uses the first path found to a state-  generalizedSearch Seq.empty id (\_ _ -> False)+  runIdentity . bfsM (Identity . next) (Identity . found)   -- | @dfs next found initial@ performs a depth-first search over a set@@ -100,10 +103,10 @@   -> Maybe [state]   -- ^ First path found to a state matching the predicate, or 'Nothing' if no   -- such path exists.-dfs =+dfs next found =   -- DFS is a generalized search using a stack, which directly compares states,   -- and which always uses the most recent path found to a state-  generalizedSearch [] id (\_ _ -> True)+  runIdentity . dfsM (Identity . next) (Identity . found)   -- | @dijkstra next cost found initial@ performs a shortest-path search over@@ -146,15 +149,12 @@   -> Maybe (cost, [state])   -- ^ (Total cost, list of steps) for the first path found which   -- satisfies the given predicate-dijkstra next cost found initial =+dijkstra next cost found =   -- This API to Dijkstra's algorithm is useful when the state transition   -- function and the cost function are logically separate.   -- It is implemented by using @dijkstraAssoc@ with appropriate mapping of   -- arguments.-  dijkstraAssoc next' found initial-  where-    next' st = map (\new_st -> (new_st, cost st new_st)) $-               Foldable.toList (next st)+  runIdentity . dijkstraM (Identity . next) (identity2 cost) (Identity . found)  -- | @dijkstraAssoc next found initial@ performs a shortest-path search over -- a set of states using Dijkstra's algorithm, starting with @initial@,@@ -174,7 +174,7 @@   -> Maybe (cost, [state])   -- ^ (Total cost, list of steps) for the first path found which   -- satisfies the given predicate-dijkstraAssoc next found initial =+dijkstraAssoc next found =   -- This API to Dijkstra's algoritm is useful in the common case when next   -- states and their associated transition costs are generated together.   --@@ -184,17 +184,41 @@   -- the stored state being (cost so far, state).   -- This implementation makes that transformation, then transforms that result   -- back into the desired result from @dijkstraAssoc@-  unpack <$>-    generalizedSearch emptyLIFOHeap snd leastCostly next' (found . snd)-      (0, initial)-  where-    next' (old_cost, st) =-      (\(new_st, new_cost) -> (new_cost + old_cost, new_st))-        <$> (next st)-    unpack [] = (0, [])-    unpack packed_states = (fst . last $ packed_states, map snd packed_states)+  runIdentity . dijkstraAssocM (Identity . next) (Identity . found) +-- | @dijkstraAssocCost next found initial@ performs a shortest-path search over+-- a set of states using Dijkstra's algorithm, starting with @initial@,+-- generating neighboring states with associated path costs with @next@ (this+-- means that the path of a cost is not the sum of the previous cost and the+-- cost of the current transition, @next@ can do arbitrary computations using+-- those two costs, like a weighted sum or a probability combination). This will+-- find the least-costly path from an initial state to a state for which @found@+-- returns 'True'. Returns 'Nothing' if no path to a solved state is possible.+dijkstraAssocCost :: (Num cost, Ord cost, Ord state)+  => ((state, cost) -> [(state, cost)])+  -- ^ function to generate list of neighboring states with associated+  -- transition costs given the current state+  -> (state -> Bool)+  -- ^ Predicate to determine if solution found. 'dijkstraAssoc' returns the+  -- shortest path to the first state for which this predicate returns 'True'.+  -> state+  -- ^ Initial state+  -> Maybe (cost, [state])+  -- ^ (Total cost, list of steps) for the first path found which+  -- satisfies the given predicate+dijkstraAssocCost next found =+  -- This API to Dijkstra's algoritm is useful in the common case when next+  -- states and their associated transition costs are generated together.+  --+  -- Dijkstra's algorithm can be viewed as a generalized search, with the search+  -- container being a heap, with the states being compared without regard to+  -- cost, with the shorter paths taking precedence over longer ones, and with+  -- the stored state being (cost so far, state).+  -- This implementation makes that transformation, then transforms that result+  -- back into the desired result from @dijkstraAssoc@+  runIdentity . dijkstraAssocCostM (Identity . next) (Identity . found) + -- | @aStar next cost remaining found initial@ performs a best-first search -- using the A* search algorithm, starting with the state @initial@, generating -- neighboring states with @next@, their cost with @cost@, and an estimate of@@ -232,16 +256,14 @@   -> Maybe (cost, [state])   -- ^ (Total cost, list of steps) for the first path found which satisfies the   -- given predicate-aStar next cost remaining found initial =+aStar next cost remaining found =   -- This API to A* search is useful when the state transition   -- function and the cost function are logically separate.   -- It is implemented by using @aStarAssoc@ with appropriate mapping of   -- arguments.-  aStarAssoc next' remaining found initial-  where-    next' st = map (\new_st -> (new_st, cost st new_st)) $-               Foldable.toList (next st)+  runIdentity . aStarM (Identity . next) (identity2 cost) (Identity . remaining) (Identity . found) + -- | @aStarAssoc next remaining found initial@ performs a best-first search -- using the A* search algorithm, starting with the state @initial@, generating -- neighboring states and their associated costs with @next@, and an estimate of@@ -263,7 +285,7 @@   -> Maybe (cost, [state])   -- ^ (Total cost, list of steps) for the first path found which satisfies the   -- given predicate-aStarAssoc next remaining found initial =+aStarAssoc next remaining found =   -- This API to A* search is useful in the common case when next   -- states and their associated transition costs are generated together.   --@@ -273,20 +295,7 @@   -- the stored state being (total cost estimate, (cost so far, state)).   -- This implementation makes that transformation, then transforms that result   -- back into the desired result from @aStarAssoc@-  unpack <$> generalizedSearch emptyLIFOHeap snd2 leastCostly next'-    (found . snd2) (remaining initial, (0, initial))-  where-    next' (_, (old_cost, old_st)) =-      update_state <$> (next old_st)-      where-        update_state (new_st, cost) =-          let new_cost = old_cost + cost-              new_est = new_cost + remaining new_st-          in (new_est, (new_cost, new_st))-    unpack [] = (0, [])-    unpack packed_states =-      (fst . snd . last $ packed_states, map snd2 packed_states)-    snd2 = snd . snd+  runIdentity . aStarAssocM (Identity . next) (Identity . remaining) (Identity . found)  -- $monadic -- Note that for all monadic searches, it is up to the user to ensure that@@ -376,11 +385,35 @@   where     nextM' (old_cost, old_st) = do       new_states <- nextM old_st-      return $ map (\(x, y) -> (y, x)) new_states+      return $ map (update_st old_cost) new_states+    update_st old_cost (new_st, new_cost) = (old_cost + new_cost, new_st)     unpack [] = (0, [])     unpack packed_states = (fst . last $ packed_states, map snd packed_states)  +-- | @dijkstraAssocCostM@ is a monadic version of 'dijkstraAssocCost': it has+-- support for monadic @next@ and @found@ parameters.+dijkstraAssocCostM :: (Monad m, Num cost, Ord cost, Ord state)+  => ((state, cost) -> m [(state, cost)])+  -- ^ Function to generate list of neighboring states with associated+  -- path costs given the current state+  -> (state -> m Bool)+  -- ^ Predicate to determine if solution found. 'dijkstraM' returns the+  -- shortest path to the first state for which this predicate returns 'True'.+  -> state+  -- ^ Initial state+  -> m (Maybe (cost, [state]))+  -- ^ (Total cost, list of steps) for the first path found which+  -- satisfies the given predicate+dijkstraAssocCostM nextM foundM initial =+  fmap2 unpack $ generalizedSearchM emptyLIFOHeap snd leastCostly nextM'+    (foundM . snd) (0, initial)+  where+    nextM' = fmap2 swap . nextM . swap+    unpack [] = (0, [])+    unpack packed_states = (fst . last $ packed_states, map snd packed_states)++ -- | @aStarM@ is a monadic version of 'aStar': it has support for monadic -- @next@, @cost@, @remaining@, and @found@ parameters. aStarM :: (Monad m, Foldable f, Num cost, Ord cost, Ord state)@@ -604,35 +637,6 @@   paths :: Map.Map stateKey [state]   } --- | Workhorse simple search algorithm, generalized over search container--- and path-choosing function. The idea here is that many search algorithms are--- at their core the same, with these details substituted. By writing these--- searches in terms of this function, we reduce the chances of errors sneaking--- into each separate implementation.-generalizedSearch ::-  (Foldable f, SearchContainer container, Ord stateKey, Elem container ~ state)-  => container-  -- ^ Empty @SearchContainer@-  -> (state -> stateKey)-  -- ^ Function to turn a @state@ into a key by which states will be compared-  -- when determining whether a state has be enqueued and / or visited-  -> ([state] -> [state] -> Bool)-  -- ^ Function @better old new@, which when given a choice between an @old@ and-  -- a @new@ path to a state, returns True when @new@ is a "better" path than-  -- old and should thus be inserted-  -> (state -> f state)-  -- ^ Function to generate "next" states given a current state-  -> (state -> Bool)-  -- ^ Predicate to determine if solution found. @generalizedSearch@ returns a-  -- path to the first state for which this predicate returns 'True'.-  -> state-  -- ^ Initial state-  -> Maybe [state]-  -- ^ First path found to a state matching the predicate, or 'Nothing' if no-  -- such path exists.-generalizedSearch empty mk_key better next found initial = runIdentity $-  generalizedSearchM empty mk_key better (Identity . next) (Identity . found) initial- -- | @nextSearchState@ moves from one @searchState@ to the next in the -- generalized search algorithm nextSearchStateM ::@@ -678,7 +682,11 @@           ps' = Map.insert (mk_key st) (st : steps_so_far) old_paths  --- | @generalizedSearchM@ is a monadic version of generalizedSearch+-- | Workhorse simple search algorithm, generalized over search container+-- and path-choosing function. The idea here is that many search algorithms are+-- at their core the same, with these details substituted. By writing these+-- searches in terms of this function, we reduce the chances of errors sneaking+-- into each separate implementation. generalizedSearchM ::   (Monad m, Foldable f, SearchContainer container, Ord stateKey,    Elem container ~ state)@@ -781,3 +789,7 @@ -- | This is just a convenience function which @fmap@s two deep fmap2 :: (Functor f1, Functor f2) => (a -> b) -> f1 (f2 a) -> f1 (f2 b) fmap2 = fmap . fmap++-- | This applies Identity to the result of a function of two arguments+identity2 :: (a -> b ->c) -> a -> b -> Identity c+identity2 f a b = Identity $ f a b
test/Algorithm/SearchSpec.hs view
@@ -105,6 +105,21 @@         `shouldBe` Nothing     it "handles zero-length solutions" $       dijkstra next cost (== 'd') 'd' `shouldBe` Just (0, [])+  describe "dijkstra with cost" $ do+    let new_cost old_cost (char, step_cost) = (char, old_cost * 10 + step_cost)+        next (char, cost) = map (new_cost cost) (cyclicWeightedGraph Map.! char)+        isChar char1 (char2, _cost) = char1 == char2+    it "performs dijkstra's algorithm" $+      dijkstraAssocCost next (== 'd') 'a'+        `shouldBe` Just (15, ['b', 'd'])+    it "handles pruning" $+      dijkstraAssocCost (next `pruning` isChar 'b') (== 'd') 'a'+        `shouldBe` Just (22, ['c', 'd'])+    it "returns Nothing when no path is possible" $+      dijkstraAssocCost (next `pruning` isChar 'b' `pruning` isChar 'c') (== 'd') 'a'+        `shouldBe` Nothing+    it "handles zero-length solutions" $+      dijkstraAssocCost next (== 'd') 'd' `shouldBe` Just (0, [])   describe "aStar" $ do     let start = (0, 0)         end = (2, 0)@@ -210,6 +225,42 @@       dijkstraM         (taxicabNeighborsBounded `pruningM` (return . isBigWall))         taxicabDistanceBounded+        (const Nothing)+        start+        `shouldBe` Nothing+  describe "dijkstraAssocCostM" $ do+    let start = (0, 0)+        end = (2, 0)+        taxicabNeighborsBounded' (from, cost1) = do+          tos <- taxicabNeighborsBounded from+          cost2s <- mapM (taxicabDistanceBounded from) tos+          pure $ zip tos $ (cost1 +) <$> cost2s+    it "performs monadic dijkstra's algorithm" $+      dijkstraAssocCostM+        taxicabNeighborsBounded'+        (return . (== end))+        start+        `shouldBe` Just (Just (2, [(1, 0), (2, 0)]))+    it "handles cyclic graphs" $ do+      let nextM (char, cost) = return $ fmap (cost +) <$> (cyclicWeightedGraph Map.! char)+          isChar char1 (char2, _cost) = char1 == char2+      dijkstraAssocCostM nextM (return . (== 'd')) 'a'+        `shouldBe` Just (Just (4, ['c', 'd']))+      dijkstraAssocCostM (nextM `pruningM` (return . isChar 'c'))+        (return . (== 'd')) 'a'+        `shouldBe` Just (Just (6, ['b', 'd']))+    it "handles zero-length solutions" $ do+      let nextM (char, cost) = return $ fmap (cost +) <$> (cyclicWeightedGraph Map.! char)+      dijkstraAssocCostM nextM  (return . (== 'd')) 'd'+        `shouldBe` Just (Just (0, []))+    it "correctly handles monadic behavior" $ do+      dijkstraAssocCostM+        (taxicabNeighborsBounded' `pruningM` (return . isBigWall . fst))+        (return . (== end))+        start+        `shouldBe` Nothing+      dijkstraAssocCostM+        (taxicabNeighborsBounded' `pruningM` (return . isBigWall . fst))         (const Nothing)         start         `shouldBe` Nothing