3

I have a directed graph that is cyclic in general, and I want to find relationships between nodes and sinks. Traversals over directed graphs seem to be well-suited for memoization with recursion, since the graph can be mapped to the results of the traversal at each node and queried, akin to the canonical Fibonacci memoization. However, I've found that detecting cycles thwarts the memoization effort, because cycle detection requires knowledge of the path, and there can be many paths leading to the same node, so the result mapped to the graph seems to depend on the path argument. However, when the cycle is ignored, the result is actually unambiguous, but I don't see any way to communicate this to the compiler.

As an simple but illustrative example, suppose I want to find all sinks reachable from a node in the graph. The naive algorithm with DFS looks something like this:

import qualified Data.Set as Set

type AdjList = [[Int]]
adj :: AdjList
adj = ...
sinks :: Int -> [Int]
sinks = sinks' Set.empty where
  sinks' :: Set.Set Int -> Int -> [Int]
  sinks' path node | node `Set.member` path = [] -- this forms a cycle
                   | null (adj !! node) = [node] -- this is a sink
                   | otherwise = concatMap (sinks' (Set.insert node path)) (adj !! node)

Whereas trying to write this for memoization clearly runs into a problem trying to fill in the path argument:

sinks = sinks' Set.empty where
  sinks' path = (map (sinks'' {- what goes here? -}) [0..(length adj - 1)] !!) where
    sinks'' path' node | node `Set.member` path' = [] -- this forms a cycle
                       | null (adj !! node) = [node] -- this is a sink
                       | otherwise = concatMap (sinks' (Set.insert node path')) (adj !! node)

Over a traversal on this graph for example, we can see how the paths to the cycle from A differ, but if the result from node D were memoized, the traversal past D would only have to be performed once:

Am I forced to write this memoization manually with an explicit table?

concat
  • 3,107
  • 16
  • 30
  • Is there something stopping you from reusing an [existing DFS](https://hackage.haskell.org/package/fgl-5.7.0.1/docs/Data-Graph-Inductive-Query-DFS.html)? I strongly recommend having a read through the [fgl paper](http://web.engr.oregonstate.edu/~erwig/fgl/haskell/old/fgl0103.pdf) -- it was quite an eye-opener for me about how to design a comfortable, idiomatic Haskell pattern-matching like API for a type which is at its core not the comfortable, idiomatic algebraic types we know and love. The paper never gives up on getting good performance in the presence of cycles, either. – Daniel Wagner Jul 13 '19 at 20:00
  • @DanielWagner I'm not sure I can see how fgl's DFS can work here, since the source suggests that all dfs functions remove visited nodes from the graph during the traversal. It might be workable for this example but only by querying the original graph (otherwise the edge cuts would create new sinks). The more general folds look promising though, thanks for showing me. – concat Jul 14 '19 at 02:38

0 Answers0