【问题标题】:Filtering lists which have the same number of different elements in them in Haskell在 Haskell 中过滤具有相同数量的不同元素的列表
【发布时间】:2020-12-10 11:53:41
【问题描述】:

我是 Haskell 的新手,我有数据 data Instruction = Add | Sub | Mul | Div | Dup | Pop deriving (Eq,Ord,Show,Generic),我正在生成列表,其中包含 MulDup 的所有可能组合以及大小为 n 的 mapM (const [Mul, Dup]) [1..n])

我只想要以Dup 开头并以Mul 结尾的列表,所以我使用了filter((== Mul) . last)(filter((== Dup) . head) (mapM (const [Mul, Dup]) [1..n])),但我也只想要其中具有相同数量的MulDup 的列表,但我不能t 似乎想出一种方法来做到这一点。我该如何过滤?有没有更有效的方法来做这件事,因为随着列表变大,可能会有大量的组合?

示例列表如下所示:[Dup,Mul,Dup,Mul][Dup,Dup,Mul,Mul] 用于大小为 4 的列表。

【问题讨论】:

  • 我必须说,我真的很喜欢mapM (const [Mul, Dup]) [1..n] 的做法——这是一种非常优雅的生成这些列表的方式,而且我自己也找不到!也许sequenceA (replicate 3 [Mul, Dup]) 可能更容易阅读,但我当然喜欢你的做法。
  • @bradrn replicateM 3 [Mul, Dup] 也可以工作。尽管如此,与其生成所有列表然后过滤掉错误的列表,不如一开始只生成想要的列表会更快。
  • 谢谢@chi,不知怎的我完全忘记了replicateM!这确实是一种更好的方法。

标签: list haskell filter


【解决方案1】:

虽然您的方法是正确的,但我认为这不是最有效的方法。您生成2^N 列表,然后过滤掉其中的许多。忘记保持计数简单的其他要求,通过要求我们有与Dups 一样多的Muls,我们最终只得到choose(N, N/2) 列表(1..N 的大小为N/2 的子集的数量),这是一个小得多的数字。

我们可以尝试避免过滤并生成想要的列表,只是,首先。我建议以下方法,您可以根据需要对其进行修改以满足其他要求。

我们定义了一个函数sameMulDup,它接受两个整数md,并生成带有m Muls和d Dups的所有列表。

sameMulDup :: Int -> Int -> [[Instruction]]
sameMulDup 0 d = [replicate d Dup]
sameMulDup m 0 = [replicate d Mul]
sameMulDup m d = do
    -- generate the first element
    x <- [Dup, Mul]
    -- compute how many m and d we have left
    let (m', d') = case x of
           Dup -> (m  , d-1)
           Mul -> (m-1, d  )
    -- generate the other elements
    xs <- sameMulDup m' d'
    return (x:xs)

直观地说,如果d=0m=0 只有一个可能的列表包含在列表列表结果中。否则,我们不确定地选择第一个元素,递减对应的计数器dm,然后生成其余的。

或者,最后一个等式可以替换为以下更基本的等式:

sameMulDup m d =
    map (Dup:) (sameMulDup m (d-1))
    ++
    map (Mul:) (sameMulDup (m-1) d)

无论如何,鉴于sameMuldup,您应该能够解决您的全部任务。

【讨论】:

  • 感谢您的回答,这对于创建具有相同数量不同元素的列表非常有用。我只需要弄清楚如何编辑它,以便只生成第一个元素 Dup 和最后一个元素 Mul 的列表。
  • @GeorgiSpasov 从所需的md 中减去1,然后在已知位置添加两个缺失的元素。
  • 以下观察结果让我被书呆子狠狠地狙击了:sameMulDup 在运行过程中会被多次调用,并使用相同的参数。例如,在生成前缀Mul:Dup:...Dup:Mul:... 之后,可以想象这两个... 可以共享,但在此实现中将重新计算。生成具有建议共享的版本相当简单,但书呆子狙击是这样的:完全共享的版本实际上更好吗?可能如果您打算只对列表进行一次迭代,分享更多会更糟糕,但两次或更多会更好。
  • @DanielWagner 很好的观察。事实上,这取决于消费者。简而言之:动态编程被认为是有害的(嗯,有时至少)。
  • @chi 在 ghci 或没有优化的情况下编译,事实证明共享版本更快,内存占用更少!但是对于-O2,这和我们都猜到的差不多。有关详细信息,请参阅我的答案。
【解决方案2】:

应该可以定义一个函数countPred :: a -&gt; [a] -&gt; Int,它计算列表中等于第一个参数的项目数;然后你可以做filter (\l -&gt; countPred Mul l == countPred Dup l)(或者filter ((==) &lt;$&gt; countPred Mul &lt;*&gt; countPred Dup),如果你喜欢无点形式)。我认为另一种方法可能是使用(==0) . sum . map (\case { Mul -&gt; 1, Dup -&gt; (-1) }),但这让我觉得它比必要的稍微复杂一些。

【讨论】:

  • 感谢您的回答。这可以完美地过滤列表,但我也会尝试提高效率并仅生成所需的列表。
  • @GeorgiSpasov 我建议您现在可能只想解决问题而不必担心效率。如果您担心这一点,您可以稍后随时对其进行基准测试,以尝试优化最有效的方法。
【解决方案3】:

我喜欢 chi 的回答,但在评论中,我提到它没有实现尽可能多的共享。我推测如果您多次迭代指令列表,共享将是有益的,但如果您只迭代一次则更糟。根据经验,无论您迭代多少次,共享版本似乎都更快,但内存权衡正如预测的那样:一次迭代更糟,多次迭代更好。所以我认为展示它可能会很有趣。

这是它的外观。我们将列出一个无限的答案列表。第一个索引是指令列表的长度;第二个是有多少Muls(尽管我将使用TrueFalse 而不是MulDup)。所以:

bits :: [[[[Bool]]]]
bits = iterate extend [[[]]] where
    extend bsss = zipWith (++)
        (map (map (False:)) bsss ++ [[]])
        ([[]] ++ map (map (True:)) bsss)

为了完整起见,下面是你如何编写一个与 chi 的 sameMulDup 具有相同签名的函数,并计算相同的答案(直到交换到 Bool):

sameMulDup' :: Int -> Int -> [[Bool]]
sameMulDup' m d = bits !! (m+d) !! m

我机器上的一些计时,对于m=d=12,编译时-O2

sameMulDup , one iteration  1.35s    6480Kb
sameMulDup', one iteration  1.11s  226476Kb
sameMulDup , two iterations 4.26s 2135368Kb
sameMulDup', two iterations 1.97s  620880Kb

这是我用于获取这些数字的驱动程序代码:

main :: IO ()
main = do
    [sharing, twice, m, d] <- getArgs
    let answer = (if read sharing then sameMulDup' else sameMulDup) (read m) (read d)
    if read twice
       then do
           print . sum . map (sum . map fromEnum) $ answer
           print . sum . map (sum . map (fromEnum . not)) $ answer
        else print . sum . map (sum . map fromEnum) $ answer

这里有一些微妙的地方:

  1. 要对列表进行两次迭代,我们必须有一种方法可以在两次迭代中引用同一个列表。这是上面代码中的answer
  2. 我们必须使用实际上强制所有值使其有用的迭代。我通过计算有多少 Trues 来做到这一点,但还有其他方法。 (仅打印整个列表效果不佳:计算的运行时间与生成要打印的 String 以及将其传输到终端所做的工作相形见绌。)
  3. 虽然第一次迭代在if 的两个分支中使用相同的代码,但重要的是不要共享此代码并将其移出if。我们希望编译器在else 分支中知道answer 将不会被再次使用,以便它可能会进行垃圾收集。如果你写print answer &gt;&gt; if twice then print answer else pure (),那么answer的前缀可能被垃圾回收的时候静态就不那么明显了。
  4. then 分支中,我在两个循环中使用了两种不同的计算,这样编译器就不会变得聪明,只计算一次,然后打印两次计算结果。

【讨论】:

  • chi 的回答中有两个不同的sameMulDups,你用哪一个测试过这个?
  • @WillNess 我现在已经对两者进行了测试。在-O2,结果与任一版本基本相同。
  • 尚不清楚您究竟是如何迭代结果列表。你将它绑定到变量还是以某种方式直接使用?也许提供一些代码 sn-p 来澄清?细节可能很重要。
  • @WillNess 此编辑是否解决了您的问题?
  • 好的,谢谢。但实际上,如果我们需要对这样的列表进行两次迭代,最好将其生成两次,以尽可能降低内存使用量(因此,不惜一切代价避免共享)。这将这个问题带到了前台——用 C 或 Lisp 这样的命令式语言编写这个东西在恒定(或多或少)内存中运行是很自然的,而在高级 Haskell 中似乎不太可能这样做。 . 两种 chi 的变体似乎都没有实现这一点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-18
  • 1970-01-01
  • 2015-10-29
  • 1970-01-01
  • 2017-06-26
相关资源
最近更新 更多