【问题标题】:Fast obtention of all the subsets of size N in Haskell在 Haskell 中快速获取所有大小为 N 的子集
【发布时间】:2013-01-10 21:01:12
【问题描述】:

以下(非最优)代码为某个子集生成所有大小为 N 的子集。

此代码有效,但正如我所说,它非常不理想。使用中间列表来避免 Set.insert 的 O(log(n)) 似乎没有帮助,因为稍后将列表重新转换为 Set 的成本很高

谁能建议如何优化代码?

import qualified Data.Set as Set


subsetsOfSizeN :: Ord a => Int -> Set.Set a -> Set.Set (Set.Set a)
subsetsOfSizeN n s
  | Set.size s < n || n < 0 = error "subsetOfSizeN: wrong parameters"
  | otherwise = doSubsetsOfSizeN n s
 where doSubsetsOfSizeN n s
        | n == 0 = Set.singleton Set.empty
        | Set.size s == n = Set.singleton s
        | otherwise =
           case Set.minView s of
             Nothing -> Set.empty
             Just (firstS, restS) ->
               let partialN n = doSubsetsOfSizeN n restS in
               Set.map (Set.insert firstS) (partialN (n-1)) `Set.union` partialN n

【问题讨论】:

    标签: algorithm haskell optimization complexity-theory set


    【解决方案1】:

    这是受到帕斯卡三角形的启发。

    choose :: [b] -> Int -> [[b]]
    _      `choose` 0       = [[]]
    []     `choose` _       =  []
    (x:xs) `choose` k       =  (x:) `fmap` (xs `choose` (k-1)) ++ xs `choose` k
    

    【讨论】:

    • 很不错,我喜欢这个
    • 非常优雅,恭喜:)
    【解决方案2】:

    此代码有效,但正如我所说,它非常不理想。

    对我来说似乎并没有那么糟糕。大小为n 的集合中大小为k 的子集的数量是n `choose` k,对于k ~ n/2,它增长得相当快。因此,创建所有子集的规模必须很差。

    使用中间列表来避免 Set.insertO(log(n)) 似乎没有帮助,因为稍后将列表重新转换为 Set 的成本很高。

    嗯,我发现使用列表可以提供更好的性能。我认为不是渐近的,而是一个不可忽略的或多或少的常数因子。

    但首先,您的代码效率低下很容易修复:

    Set.map (Set.insert firstS) (partialN (n-1))
    

    请注意,Set.map 必须从头开始重建树。但是我们知道firstS总是小于partialN (n-1)中任何集合中的任何元素,所以我们可以使用Set.mapMonotonic,它可以重用集合的脊椎。

    这个原则也是列表吸引人的原因,子集是按字典顺序生成的,所以我们可以使用更有效的Set.fromDistinctAscList来代替Set.fromList。转录算法产生

    onlyLists :: Ord a => Int -> Set.Set a -> Set.Set (Set.Set a)
    onlyLists n s
        | n == 0                    = Set.singleton Set.empty
        | Set.size s < n || n < 0   = error "onlyLists: out of range n"
        | Set.size s == n           = Set.singleton s
        | otherwise                 = Set.fromDistinctAscList . map Set.fromDistinctAscList $
                                                             go n (Set.size s) (Set.toList s)
          where
            go 1 _ xs = map return xs
            go k l (x:xs)
                | k == l = [x:xs]
                | otherwise = map (x:) (go (k-1) (l-1) xs) ++ go k (l-1) xs
    

    在我运行的几个基准测试中,它比使用 Sets 的修正算法快 1.5 到 2 倍。

    反过来,在我的标准基准测试中,速度几乎是 dave4420 的两倍。

    【讨论】:

      【解决方案3】:
      subsets :: Int -> [a] -> [[a]]
      subsets 0 _ = [[]]
      subsets _ [] = []
      subsets k (x:xs) = map (x:) (subsets (k - 1) xs) ++ subsets k xs
      

      【讨论】:

        【解决方案4】:

        首先,使用更好的算法。

        看看你的最后一行:

                   Set.map (Set.insert firstS) (partialN (n-1)) `Set.union` partialN n
        

        评估doSubsetsOfSizeN k (Set.fromList $ 1:2:xs) 将涉及评估doSubsetsOfSizeN (k-1) (Set.fromList xs) 两次(插入1 时一次,插入2 时一次)。这种重复很浪费。

        输入一个更好的算法。

        mine :: Ord a => Int -> Set.Set a -> Set.Set (Set.Set a)
        mine n s | Set.size s < n || n < 0 = Set.empty
                 | otherwise               = Set.foldr cons nil s !! n
            where
                nil :: Ord a => [Set.Set (Set.Set a)]
                nil = Set.singleton Set.empty : repeat Set.empty
                cons :: Ord a => a -> [Set.Set (Set.Set a)] -> [Set.Set (Set.Set a)]
                cons x sets = zipWith Set.union sets
                                       (Set.empty : map (Set.map $ Set.insert x) sets)
        

        mine 9 (Data.Set.fromList [0..18]) `seq` ()subsetsOfSizeN 9 (Data.Set.fromList [0..18]) `seq` () 快,应该有更好的渐近性能。

        我没有尝试进一步优化这个。可能还有更好的算法。

        (如果insertfromList 的成本有问题,您应该考虑返回一个列表列表而不是一组集合。)

        【讨论】:

          【解决方案5】:

          我找到了这个,可能对你有帮助

          f []  = [[1]]
          f l   = (:) [u] l'
              where 
                  u  = succ (head (head l))
                  l' = (++) l (map(\x->(:) u x) l)
          
          fix f n = if (n==0) then [] else f (fix f (n-1)) 
          

          测试一下

          $ length $ (fix f 10) => 1023 -- The empty set is always include then == 1024
          

          【讨论】:

          • (:) [u] l' 而不仅仅是[u]:l' 有什么特别的原因吗? (与l' 定义中的(++)(:) 类似。)
          • 是的,我更喜欢使用这种形式,因为我可以在 where 子句中写 cons = uncurry (:), concat = uncurry (++) 并在将 [u]:l' 替换为之后缺点 [u] l.
          猜你喜欢
          • 1970-01-01
          • 2018-08-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-02-21
          相关资源
          最近更新 更多