【问题标题】:Undoing a prefix sum撤消前缀和
【发布时间】:2021-11-11 00:30:55
【问题描述】:

在 Haskell 中计算前缀和的简单方法是

scanl1 (+) [1,2,3,4,5,6]

产生输出

[1,3,6,10,15,21]

我需要写逆,这就是我想出的:

undo_prefix_sum :: (Num a) => [a] -> [a]
undo_prefix_sum s = init $ snd $ 
    foldr (\cur (tot, l) -> (cur, (tot-cur):l)) 
          (last s, []) 
          (0:s)

这似乎是正确的(但我可能错过了一些东西)。 有没有更简单或更有效的方法来做到这一点,可能使用扫描?

【问题讨论】:

  • 您在另一条评论中提到了zipWith (-) xs (0:xs),并“照亮了 Haskell 的有趣部分”。如果您还没有看到 zipWith (-) <*> (0 :) $ [1..6],您可能会有所启发。
  • 谢谢。我在这方面的经历很像我对"juxt" in haskell 的经历。在这种情况下,函数的<*> 定义使用了它的参数两次;在另一个中,函数的 Reader monad 实例被牵连,这与多次使用参数的效果相似!
  • scanl1 (+) 不计算前缀总和。它计算前缀总和。
  • @MichaWiedenmann 在 GHCI 中 scanl1 (+) [1..20] 之后尝试 zipWith (-) <*> (0 :) $ it。 (等待它...)然后尝试zipWith (-) <*> (0 :) $ it再次。然后再次。然后再次。 :) 然后再次。 :) 我不知道照明,但它很有趣。
  • @WillNess 感谢分享!确实很有趣。

标签: haskell prefix-sum


【解决方案1】:

在我看来,prefix_sum 自然地表达为折叠,undo_prefix_sum 更自然地表达为展开。

import Data.List

undo_prefix_sum  = unfoldr (\xs -> if null xs 
                                   then Nothing
                                   else let (h:t) = xs 
                                        in Just (h,  map (subtract h) t))

unfoldr 使用函数从种子值开始构建列表。在这种情况下,种子本身就是一个列表;我们在结果中使用种子的第一个元素,并将列表的其余部分(的修改版本)作为种子,以递归方式计算结果的其余部分。

【讨论】:

  • 这正是我正在寻找的答案。 undo_prefix_sum xs = zipWith (-) xs (0:xs) 也可以,但它并没有像您的回答那样阐明 Haskell 中任何更有趣的部分。
  • 重复的(subtract h)s 叠加起来,使其成为二次代码。 zipWith (-) <*> (0:) 计算是线性的。所以它更高效,也更简单。
  • (上面的意思是@TheoH)
  • @WillNess 这是一个不可逾越的障碍吗?想知道是否有办法对此进行调整,以便可以将减法部分的堆栈“融合”成单个函数。我确实喜欢将其计算为展开的对称性,但不以(被忽视的)二次行为为代价。
  • 当我们融合它们时,我们最终得到...zipWith ... tail 版本。 (我已经发布了一些重写的​​答案并对此发表了评论)
【解决方案2】:

你的函数应该写成

undo_prefix_sum' :: (Num a) => [a] -> [a]
undo_prefix_sum' xs = init $ snd $ 
    foldr (\x ~(y, ys) -> (x, (y-x):ys)) 
          (undefined, []) 
          (0:xs)

您代码中的tot 不是“总数”,它只是x 之后输入中的下一个 值。所以这里需要名称中的一些对称性(命名很重要)。由于init 调用,last xs 位永远不会被计算,所以可以只是undefined,以避免给人错误的印象。

由于它所做的只是计算每个元素与其前任元素之间的差异(或0 为第一个元素),我们不妨清楚这一点:

foo xs = [y-x | i <- [0..length xs-1], let y=xs!!i ; x = (0:xs)!!i ]

这当然是二次的,它会计算出length,这是一个禁忌;但至少它很简单,所以它很容易修复:

bar xs = [y-x | x <- 0:xs | y <- xs]
       = map (uncurry (-)) $ zip xs (0:xs)
       = zipWith (-) xs (0:xs)

你已经看到了,如 cmets 中所述。因此,它在计算上确实等同于您的代码,不像其他答案中的代码通过本质上不同的 essentially quadratic 计算相同的结果(并且不必要地如此),计算。

如果您真的想将zipWith (-) 过程表示为展开,则可以这样做

baz xs = [y-x | (x:y:_) <- tails (0:xs)]

因为tails ~= iterate tail,迭代和展开本质上是一回事(每个都可以表示为另一个,可能需要一些预处理和/或后处理步骤)。

最后一件事。您的代码确实存在问题。这是不必要的严格:

> take 2 $ undo_prefix_sum $ [1,2] ++ undefined
*** Exception: Prelude.undefined

> take 2 $ undo_prefix_sum' $ [1,2] ++ undefined
[1,1]

foo 上面也同样严格。此答案中的所有其他版本(以及其他答案中的代码)都成功且正确地计算了结果。此处的undo_prefix_sum' 与您的代码之间的唯一区别(除了变量重命名)非常小。

你能认出来吗?

除此之外,无论您的代码名义上使用foldr 还是unfoldr,重要的是它是否足够懒惰。 foldr 具有惰性组合功能,同样能够编写。像sum 这样的变质必然是严格的。要将您的函数编写为适当的变形,使其仍然应该是线性的,我们需要通过使用其tail 压缩输入来预处理输入,因为我们同时需要两个头部元素。对此进行编码的另一种方法是使用paramorphism,以便我们可以访问输入的当前尾部及其当前元素。并且通过折叠 tails(是的,就像 baz 正在做的那样),emulated 很容易实现。

【讨论】:

    猜你喜欢
    • 2016-02-12
    • 2021-07-12
    • 2014-04-12
    • 1970-01-01
    • 1970-01-01
    • 2018-01-19
    • 2010-12-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多