具有补集的快速幂集实现
Fast powerset implementation with complement set
我想要一个功能
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]
但是我估计这两个性能真的很差。什么是最佳方法?可以使用与 []
列表不同的数据结构。
你应该看到这样的幂集:你枚举集合的项目,然后决定是否将它们放在 "selection"(元组的第一项)中,或者不放在(第二项)中元组)。通过详尽地枚举这些选择,我们得到了幂集。
所以我们可以做同样的事情,例如使用递归:
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) 中完成),我们现在构建两个每个原始列表的列表(和元组)。
由于您正在寻找 "fast" 实现,我想我会分享一些 benchmark experiments 我用 Willem 的解决方案所做的。
我认为使用 DList 而不是普通列表将是一个很大的改进,因为 DList 具有恒定时间追加,而追加列表与左参数的大小成线性关系。
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% 的改进。
我想最好的是你的reverse
发现
partitions s=filterM(const[False,True])s
`zip`filterM(const[True,False])s
而不是可能的 Whosebuger
partitions[]=[([],[])]
partitions(x:xs)=[p|(f,t)<-partitions xs,p<-[(l,x:r),(x:l,r)]]
或 space 且时间高效的有限列表索引器
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]]]
我想要一个功能
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]
但是我估计这两个性能真的很差。什么是最佳方法?可以使用与 []
列表不同的数据结构。
你应该看到这样的幂集:你枚举集合的项目,然后决定是否将它们放在 "selection"(元组的第一项)中,或者不放在(第二项)中元组)。通过详尽地枚举这些选择,我们得到了幂集。
所以我们可以做同样的事情,例如使用递归:
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) 中完成),我们现在构建两个每个原始列表的列表(和元组)。
由于您正在寻找 "fast" 实现,我想我会分享一些 benchmark experiments 我用 Willem 的解决方案所做的。
我认为使用 DList 而不是普通列表将是一个很大的改进,因为 DList 具有恒定时间追加,而追加列表与左参数的大小成线性关系。
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% 的改进。
我想最好的是你的reverse
发现
partitions s=filterM(const[False,True])s
`zip`filterM(const[True,False])s
而不是可能的 Whosebuger
partitions[]=[([],[])]
partitions(x:xs)=[p|(f,t)<-partitions xs,p<-[(l,x:r),(x:l,r)]]
或 space 且时间高效的有限列表索引器
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]]]