Search code examples
haskellcombinatoricspowerset

Fast powerset implementation with complement set


I would like to have a function

powersetWithComplements :: [a] -> [([a], [a])]

Such that for example:

powersetWithComplements [1,2,3] = [([],[1,2,3]),([3],[1,2]),([2],[1,3]),([2,3],[1]),([1],[2,3]),([1,3],[2]),([1,2],[3]),([1,2,3],[])]

It is easy to obtain some implementation, for example

powerset :: [a] -> [[a]]
powerset = filterM (const [False, True])

powersetWithComplements s = let p = powerset s in zip p (reverse p)

Or

powersetWithComplements s = [ (x, s \\ x) | x <- powerset s]

But I estimate that the performance of both these would be really poor. What would be an optimal approach? It is possible to use different data structure than the [] list.


Solution

  • Well you should see a powerset like this: you enumerate over the items of the set, and you decide whether you put these in the "selection" (first item of the tuple), or not (second item of the tuple). By enumerating over these selections exhaustively, we get the powerset.

    So we can do the same, for instance using recursion:

    import Control.Arrow(first, second)
    
    powersetWithComplements [] = [([],[])]
    powersetWithComplements (x:xs) = map (second (x:)) rec ++ map (first (x:)) rec
        where rec = powersetWithComplements xs
    

    So here the map (second (x:) prepends all the second items of the tuples of the rec with x, and the map (second (x:) does the same for the first item of the tuples of rec. where rec is the recursion on the tail of the items.

    Prelude Control.Arrow> powersetWithComplements [1,2,3]
    [([],[1,2,3]),([3],[1,2]),([2],[1,3]),([2,3],[1]),([1],[2,3]),([1,3],[2]),([1,2],[3]),([1,2,3],[])]
    

    The advantage of this approach is that we do not generate a complement list for every list we generate: we concurrently build the selection, and complement. Furthermore we can reuse the lists we construct in the recursion, which will reduce the memory footprint.

    In both time complexity and memory complexity, the powersetWithComplements function will be equal (note that this is complexity, of course in terms of processing time it will require more time, since we do an extra amount of work) like the powerset function, since prepending a list is usually done in O(1)), and we now build two lists (and a tuple) for every original list.