【问题标题】:Generating subsets of set. Laziness?生成集合的子集。懒惰?
【发布时间】:2016-03-19 11:20:12
【问题描述】:

我编写了一个生成子集子集的函数。当我以以下方式使用subsets [1..]时,它会导致堆栈溢出。对于“正常”(非惰性)语言,这是“正常”行为。现在,我想改进我的懒惰功能。

附:我不理解懒惰(我试图理解它)所以也许我的问题对你来说很奇怪 - 请解释一下。 :)

附: 2 请随意谈谈我在 Haskell 中的残疾情况;)

subsets :: [a] -> [[a]]
subsets (x:xs) = (map (\ e -> x:e) (subsets xs)) ++ (subsets xs)
subsets [] = [[]]

【问题讨论】:

    标签: haskell


    【解决方案1】:

    该功能存在两个问题。首先,它递归两次,这使得它的效率比必要的指数低(如果我们忽略结果的指数数量......),因为每个子树每次都针对所有重叠的子集重新计算;这可以通过let将递归调用设为相同的值来解决:

    subsets' :: [a] -> [[a]]
    subsets' [] = [[]]
    subsets' (x:xs) = let s = subsets' xs
                      in map (x:) s ++ s
    

    这已经允许您在几秒钟内计算出length $ subsets' [1..25],而length $ subsets [1..25] 需要......好吧,我没有等;)

    另一个问题是,在你的版本中,当你给它一个无限列表时,它会首先在该列表的无限尾上递归。为了以有意义的方式生成所有有限子集,我们需要确保两件事:首先,我们必须从较小的集合中构建每个集合(以确保终止),其次,我们应该确保公平顺序(即,不要先生成列表[[1], [2], ...],然后再不去其他地方)。为此,我们从[[]] 开始,递归地将当前元素添加到我们已经生成的所有内容中,然后为下一步记住新列表:

    subsets'' :: [a] -> [[a]]
    subsets'' l = [[]] ++ subs [[]] l
      where subs previous (x:xs) = let next = map (x:) previous
                                   in next ++ subs (previous ++ next) xs
            subs _ [] = []
    

    按此顺序排列:

    *Main> take 100 $ subsets'' [1..]
    [[],[1],[2],[2,1],[3],[3,1],[3,2],[3,2,1],[4],[4,1],[4,2],[4,2,1],[4,3],[4,3,1],[4,3,2],[4,3,2,1],[5],[5,1],[5,2],[5,2,1],[5,3],[5,3,1],[5,3,2],[5,3,2,1],[5,4],[5,4,1],[5,4,2],[5,4,2,1],[5,4,3],[5,4,3,1],[5,4,3,2],[5,4,3,2,1],[6],[6,1],[6,2],[6,2,1],[6,3],[6,3,1],[6,3,2],[6,3,2,1],[6,4],[6,4,1],[6,4,2],[6,4,2,1],[6,4,3],[6,4,3,1],[6,4,3,2],[6,4,3,2,1],[6,5],[6,5,1],[6,5,2],[6,5,2,1],[6,5,3],[6,5,3,1],[6,5,3,2],[6,5,3,2,1],[6,5,4],[6,5,4,1],[6,5,4,2],[6,5,4,2,1],[6,5,4,3],[6,5,4,3,1],[6,5,4,3,2],[6,5,4,3,2,1],[7],[7,1],[7,2],[7,2,1],[7,3],[7,3,1],[7,3,2],[7,3,2,1],[7,4],[7,4,1],[7,4,2],[7,4,2,1],[7,4,3],[7,4,3,1],[7,4,3,2],[7,4,3,2,1],[7,5],[7,5,1],[7,5,2],[7,5,2,1],[7,5,3],[7,5,3,1],[7,5,3,2],[7,5,3,2,1],[7,5,4],[7,5,4,1],[7,5,4,2],[7,5,4,2,1],[7,5,4,3],[7,5,4,3,1],[7,5,4,3,2],[7,5,4,3,2,1],[7,6],[7,6,1],[7,6,2],[7,6,2,1]]
    

    【讨论】:

    • 非常感谢!这是一个带有单子的解决方案。没有他们怎么办?
    • @Gilgamesz 原来甚至不需要 monad。无论如何,以前的版本是错误的——现在它适用于普通列表,并且希望是正确的。
    • @Gilgamesz 懒惰是 Haskell 的核心,也是最高级的主题之一。例如,phg 的第一点是 Haskell 对惰性的具体实现的结果,它使用图结构来表示正在进行的评估:IIRC,在某些有限的情况下,编译器捕获重复,并共享两种情况下的数据结构相同——但并非总是如此。这是我们为懒惰付出的代价之一。这是编译器的一个限制,我们在关注性能或处理无穷大时必须注意这一点。
    • 好点。具体来说,从graph reduction 的角度思考语言很有帮助。
    【解决方案2】:

    您无法生成所有无限集的子集:它们形成了一个不可数集。基数使它不可能。

    您最多可以尝试生成所有有限子集。为此,从[] 开始,您无法通过归纳继续进行,因为您永远无法到达[]。您需要从列表的开头而不是结尾进行归纳。

    【讨论】:

    • 好的,你是对的。你让我想起了集合论中的一些事实:D。好的,所以让我们只考虑有限集。如何提高我的懒惰功能?
    【解决方案3】:

    正确的折叠解决方案是:

    powerset :: Foldable t => t a -> [[a]]
    powerset xs = []: foldr go (const []) xs [[]]
        where go x f a = let b = (x:) <$> a in b ++ f (a ++ b)
    

    然后:

    \> take 8 $ powerset [1..]
    [[],[1],[2],[2,1],[3],[3,1],[3,2],[3,2,1]]
    

    【讨论】:

    • +1 为简洁起见,但需要更多解释。尤其是因为它使用了“使用 四个 参数调用foldr”这种不太惯用的技术,这很容易让新手感到困惑。我还是更喜欢简单的递归,我觉得它更具可读性。
    • @chi 与递归解决方案相比,它使用的内存更少,执行速度更快!在此解决方案上尝试length $ powerset [1..24]ghc --make -O2,而不是递归解决方案。
    • 非常感谢!但是,如果您可以用一些话来解释您的解决方案,那就太好了。特别是我不明白(const [])&lt;$&gt; :)
    • @Gilgamesz const 被简单地定义为const x _ = x,也就是说它只返回它的第一个参数而忽略第二个参数。这里的(x:) &lt;$&gt; a 等价于map (x:) a
    • @behzad.nouri 非常简洁。有谁知道为什么这最终表现得更好?因为在内联foldr和一些currying之后,它和我的第二个版本完全一样,只是subs/go的参数颠倒了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-12-31
    • 2010-09-26
    • 2011-07-21
    • 2017-03-11
    • 2019-01-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多