具有补码集的快速powerset实现

时间:2017-12-11 14:37:03

标签: haskell combinatorics powerset

我想要一个功能

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

例如:

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],[])]

很容易获得一些实现,例如

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

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

或者

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

但我估计这两者的表现都会很差。什么是最佳方法?可以使用与[]列表不同的数据结构。

3 个答案:

答案 0 :(得分:10)

那么你应该看到这样的一个powerset:你枚举集合的项目,然后你决定是否把它们放在“选择”(元组的第一项)中,或者不是(元组的第二项) 。通过详尽地列举这些选择,我们得到了powerset。

所以我们可以这样做,例如使用递归:

import Control.Arrow(first, second)

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

所以这里map (second (x:)rec的元组的所有第二项前置x,而map (second (x:)对元组的第一项进行相同的操作rec。其中rec是项目尾部的递归。

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],[])]

这种方法的优点是我们为我们生成的每个列表生成补充列表:我们同时构建选择和补充。此外,我们可以重用我们在递归中构造的列表,这将减少内存占用。

在时间复杂度和内存复杂性方面,powersetWithComplements函数都是相同的(注意这是复杂度,当然在处理时间方面它需要更多时间,因为我们像powerset函数一样做额外的工作,因为预先列出一个列表通常是在 O(1)中完成的,现在我们为每个列表构建两个列表(和一个元组)原始清单。

答案 1 :(得分:2)

由于您正在寻找“快速”实施,我想我会与Willem的解决方案分享一些benchmark experiments

我认为使用DList而不是普通列表将是一个很大的改进,因为DLists具有常量时间附加,而附加列表在左参数的大小上是线性的。

psetDL :: [a] -> [([a],[a])]
psetDL = toList . go
    where
    go [] = DList.singleton ([],[])
    go (x:xs) = (second (x:) <$> rec) <> (first (x:) <$> rec)
        where
        rec = go xs

但这并没有产生重大影响。

我怀疑这是因为我们正在遍历两个子列表,因为fmap(<$>)。我们可以通过执行类似于CPS转换函数的操作来避免遍历,将累积的集合作为参数传递而不是返回它们。

psetTail :: [a] -> [([a],[a])]
psetTail = go [] []
    where
    go a b [] = [(a,b)]
    go a b (x:xs) = go a (x:b) xs <> go (x:a) b xs

这对大小为20的列表产生了220%的改进。既然我们没有遍历fmapping中的列表,我们可以通过使用DList来消除追加遍历:

psetTailDL :: [a] -> [([a],[a])]
psetTailDL = toList . go [] []
    where
    go a b [] = DList.singleton (a,b)
    go a b (x:xs) = go a (x:b) xs <> go (x:a) b xs

这会带来额外20%的改善。

答案 2 :(得分:0)

我想最好的灵感来自你的reverse发现

partitions s=filterM(const[False,True])s
        `zip`filterM(const[True,False])s

而非可能的stackoverflower

partitions[]=[([],[])]
partitions(x:xs)=[p|(f,t)<-partitions xs,p<-[(l,x:r),(x:l,r)]]

或节省空间和时间效率的有限列表索引器

import Data.Array
import Data.Bits
import Data.List
partitions s=[(map(a!)f,map(a!)t)
             |n<-[length s],a<-[listArray(0,n-1)s],
              m<-[0..2^n-1],(f,t)<-[partition(testBit m)[0..n-1]]]