Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Efficient functional algorithm for computing closure under an operator

I'm interested in efficient functional algorithms (preferably in Haskell, and even more preferably already implemented as part of a library!) for computing the closure of a container under a unary operator.

A basic and inefficient example of what I have in mind, for lists, is:

closure :: Ord a => (a -> a) -> [a] -> [a]
closure f xs = first_dup (iterate (\xs -> nub $ sort $ xs ++ map f xs) xs) where
    first_dup (xs:ys:rest) = if xs == ys then xs else first_dup (ys:rest)

A more efficient implementation keeps tracks of the new elements generated at each stage (the "fringe") and doesn't apply the function to elements to which it has already been applied:

closure' :: Ord a => (a -> a) -> [a] -> [a]
closure' f xs = stable (iterate close (xs, [])) where
    -- return list when it stabilizes, i.e., when fringe is empty
    stable ((fringe,xs):iterates) = if null fringe then xs else stable iterates

    -- one iteration of closure on (fringe, rest);  key invariants:
    -- (1) fringe and rest are disjoint; (2) (map f rest) subset (fringe ++ rest)
    close (fringe, xs) = (fringe', xs') where
        xs' = sort (fringe ++ xs)
        fringe' = filter (`notElem` xs') (map f fringe)

As an example, if xs is a nonempty sublist of [0..19], then closure' (\x->(x+3)`mod`20) xs is [0..19], and the iteration stabilizes in 20 steps for [0], 13 steps for [0,1], and 4 steps for [0,4,8,12,16].

Even more efficiency could be gotten using a tree-based ordered-set implementation. Has this been done already? What about the related but harder question of closure under binary (or higher-arity) operators?

like image 282
lambdacalculator Avatar asked Oct 21 '13 01:10

lambdacalculator


1 Answers

How about something like this which uses the Hash Array Mapped Trie data structures in unordered-containers. For unordered-containers member and insert are O(min(n,W)) where W is the length of the hash.

module Closed where

import Data.HashSet (HashSet)
import Data.Hashable
import qualified Data.HashSet as Set

data Closed a = Closed { seen :: HashSet a, iter :: a -> a } 

insert :: (Hashable a, Eq a) => a -> Closed a -> Closed a
insert a c@(Closed set iter)
  | Set.member a set = c
  | otherwise        = insert (iter a) $ Closed (Set.insert a set) iter

empty :: (a -> a) -> Closed a
empty = Closed Set.empty

close :: (Hashable a, Eq a) => (a -> a) -> [a] -> Closed a
close iter = foldr insert (empty iter)

Here's a variation on the above that generates the solution set more lazily, in a breadth-first manner.

data Closed' a = Unchanging | Closed' (a -> a) (HashSet a) (Closed' a)

close' :: (Hashable a, Eq a) => (a -> a) -> [a] -> Closed' a
close' iter = build Set.empty where
  inserter :: (Hashable a, Eq a) => a -> (HashSet a, [a]) -> (HashSet a, [a])
  inserter a (set, fresh) | Set.member a set = (set, fresh)
                          | otherwise        = (Set.insert a set, a:fresh)
  build curr [] = Unchanging
  build curr as =
    Closed' iter curr $ step (foldr inserter (curr, []) as)
  step (set, added) = build set (map iter added)

-- Only computes enough iterations of the closure to 
-- determine whether a particular element has been generated yet
-- 
-- Returns both a boolean and a new 'Closed'' value which will 
-- will be more precisely defined and thus be faster to query
member :: (Hashable a, Eq a) => a -> Closed' a -> (Bool, Closed' a)
member _ Unchanging = False
member a c@(Closed' _ set next) | Set.member a set = (True, c)
                                | otherwise        = member a next

improve :: Closed' a -> Maybe ([a], Closed' a)
improve Unchanging = Nothing
improve (Closed' _ set next) = Just (Set.toList set, next)

seen' :: Closed' a -> HashSet a
seen' Unchanging = Set.empty
seen' (Closed' _ set Unchanging) = set
seen' (Closed' _ set next)       = seen' next

And to check

>>> member 6 $ close (+1) [0]
...

>>> fst . member 6 $ close' (+1) [0]
True
like image 177
J. Abrahamson Avatar answered Nov 16 '22 13:11

J. Abrahamson