【问题标题】:Two pointers method in haskellhaskell中的两个指针方法
【发布时间】:2021-08-29 15:22:19
【问题描述】:

如何在haskell中实现两个指针的方法?

例如,在子数组求和问题中,我们要找到和为 x 的子数组。

示例:[ 1, 3, 5, 18 ] 求和为 8 的子数组 Ans: [3,5]

在命令式编程中,解决方案是通过两个指针方法通过维护指向子数组的第一个和最后一个值的指针来给出的。当结果子数组和为 lteq 到 x 时,右指针移动。

我想出的最接近的想法是通过 scanltakeWhile 的组合,但是当我们扫描解决方案时,我无法想象如何滑动“蠕虫”。

我对所描述算法的幼稚实现

go' :: (Ord a, Num a) => a -> [a] -> Bool
go' x l 
    | null l = False
    | x `elem` takeWhile (<=x) (scanl' (+) 0 l) = True 
    | otherwise = go' x (tail l)

【问题讨论】:

  • 你能发布一个链接或一些代码来展示两个指针方法在命令式语言中是如何工作的吗?
  • @Noughtmare 在竞争性编程手册中,cses.fi/book.pdf。子数组和问题在第 77 页的两个指针方法部分中进行了描述。

标签: haskell functional-programming


【解决方案1】:

这是解决问题的相对简单的方法:

getSubarrayIndices :: Int -> [Int] -> Maybe (Int, Int)
getSubarrayIndices target list = 
   let prefixSumsWithIndices = zip [0..] $ scanl' (+) 0 list
       loop begin@((beginIdx, beginSum):restBegin) end@((endIdx, endSum):restEnd) = 
            case compare (endSum - beginSum) target of
                LT -> loop begin restEnd
                EQ -> Just (beginIdx, endIdx)
                GT -> loop restBegin end
       loop _ _ = Nothing
   in loop prefixSumsWithIndices prefixSumsWithIndices

如果getSubarrayIndices target list = Just (beginIdx, endIdx)target = sum (drop beginIdx (take endIdx list))。如果getSubarrayIndices target list = Nothing 则没有listtarget 相加的连续子序列。

请务必注意,很少使用列表作为存储数据的一种方式。如果您正在寻找高性能,Haskell 中的列表确实应该被视为命令式语言中控制流机制的替代品,而不是存储将多次循环的数据的手段。在这种情况下,使用列表和使用数组的渐近时间复杂度将相等,但常数因素将有利于使用数组。在这种情况下,我们实际上可以通过创建prefixSumsWithIndices 的两个副本来提高算法的内存复杂度,如下所示:

getSubarrayIndices :: Int -> [Int] -> Maybe (Int, Int)
getSubarrayIndices target list = 
   let prefixSumsWithIndices () = zip [0..] $ scanl' (+) 0 list
       loop begin@((beginIdx, beginSum):restBegin) end@((endIdx, endSum):restEnd) = 
            case compare (endSum - beginSum) target of
                LT -> loop begin restEnd
                EQ -> Just (beginIdx, endIdx)
                GT -> loop restBegin end
       loop _ _ = Nothing
   in loop (prefixSumsWithIndices ()) (prefixSumsWithIndices ())

这是因为在原始算法中,当end 领先于begin 时,所有中间前缀和都存储在内存中的某个位置。在创建两个列表的算法的第二个版本中,两个列表都将在使用时生成,因此没有中间前缀和要存储,因为中间前缀和由begin 重新计算。因此,我们的新代码使用了O(1) 辅助内存。

【讨论】:

  • 如果启用优化,您节省内存的尝试可能会失败。
  • @dfeuer GHC 即使在-O2 也不会应用任何“危险”优化,这些优化会增加空间或时间复杂度。这些优化必须手动打开。所以 GHC 应该不能在这里进行常见的子表达式消除,因为它会增加空间复杂度。
  • this Core 中查看lvl_s2Ki
【解决方案2】:

你总是可以做的一件事是将命令式代码直接翻译成 Haskell:

import qualified Data.Vector as V

twoPointers :: Int -> [Int] -> Maybe [Int]
twoPointers x xs = go 0 0 0 where
  arr = V.fromList xs
  n = V.length arr
  go s l r
    | r  == n = Nothing
    | s' == x = Just (V.toList (V.slice l (r + 1 - l) arr))
    | s' <= x = go s' l (r + 1)
    | otherwise = go (s - arr V.! l) (l + 1) r
    where
      s' = s + arr V.! r

编辑:以下函数式解决方案还不是 O(n),抱歉。

对于更实用的解决方案,我将推导出如下。您已经建议了scanltakeWhile,我会将它与tails 结合起来以获取每个子序列的所有总和的列表:

ghci> map (takeWhile (<= 5) . scanl (+) 0) (tails [1,2,3])
[[0,1,3],[0,2,5],[0,3],[0]]

(请注意,您可以改用一元绑定来编写它:takeWHile (&lt;= 5) . scanl (+) 0 =&lt;&lt; tails [1,2,3],我认为它看起来更好)

有了这个,我们可以很容易地解决我们只想确定是否存在具有特定总和的子序列的决策问题版本:

subseqSumDecision :: Int -> [Int] -> Bool
subseqSumDecision x xs = x `elem` concatMap (takeWhile (<= x) . scanl (+) 0) (tails xs)

要获得实际的子序列,我认为最简单的方法是更改​​scanl 的参数以同时构建子序列:

ghci> concatMap (takeWhile ((<= 5) . fst) . scanl (\(s, xs) x -> (s + x, x : xs)) (0, [])) (tails [1,2,3])
[(0,[]),(1,[1]),(3,[2,1]),(0,[]),(2,[2]),(5,[3,2]),(0,[]),(3,[3]),(0,[])]

现在我们可以简单地使用lookup 函数来查找子序列:

subseqSumReversed :: Int -> [Int] -> Maybe [Int]
subseqSumReversed x xs = lookup x 
  $ concatMap
    (takeWhile ((<= x) . fst) . scanl (\(s, xs) x -> (s + x, x : xs))
    (0, []))
  $ tails xs

你可能会注意到这会返回反转的子序列,所以你可以简单地在最后再次反转它:

subseqSum :: Int -> [Int] -> Maybe [Int]
subseqSum x xs = fmap reverse
  $ lookup x 
  $ concatMap
    (takeWhile ((<= x) . fst) . scanl (\(s, xs) x -> (s + x, x : xs))
    (0, []))
  $ tails xs

【讨论】:

  • 关于时间复杂度,我在一个大型测试集上尝试了你的第一个和第二个解决方案,该测试集来自我最近的一次采访,它甚至无法完成第一个案例。我的 scanl 解决方案与您的类似,因此也没有帮助。我现在正在尝试将 scanl' 传递给 scanr 并对其进行推理,看看这是否有意义。该算法看起来很简单,可以用类似 C 的语言来实现,但我花了一整天的时间试图弄清楚如何用 Haskell 来描述它。
  • 我已经更新了第一个解决方案几次。也许您使用的是旧版本。我认为它现在应该具有正确的复杂性。
  • 还要确保使用优化进行编译 (-O)。
  • 第一个解决方案似乎不正确。 twoPointers 4 [1,2,3,4] 崩溃了
  • 糟糕,切片应该是开始和长度,而不是开始和结束。我已经修好了。我想我正在遭受integer blindness的痛苦。
【解决方案3】:

既然你提到了两个指针的方法,我们实际上可以使用一个。我们将保留一个指向子序列第一个元素的指针,以及一个刚好超过最后一个元素的指针。我们还将跟踪子序列的大小及其总和。

import Numeric.Natural

getSubarray :: Natural -> [Natural] -> Maybe [Natural]
getSubarray goal = \xs -> go 0 0 xs xs
  where
    go sz total start []
      | total == goal
        -- These shenanigans avoid leaking the remainer
        -- of the list.
      , (taken, !_) <- splitAt sz start
      = Just taken
      | otherwise = Nothing
    go sz total ~start@(a:as) end@(x : xs) = case compare total goal of
      EQ
        | (taken, !_) <- splitAt sz start
        -> Just taken
      LT -> go (sz + 1) (total + x) start xs
      GT -> go (sz - 1) (total - a) as end

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-01-06
    • 1970-01-01
    • 1970-01-01
    • 2010-09-14
    • 1970-01-01
    • 1970-01-01
    • 2015-11-14
    • 1970-01-01
    相关资源
    最近更新 更多