Two pointers method in haskell

Viewed 241

How I can implement the two pointers method in haskell?

For example, in the subarray sum problem where we want to find a subarray whose sum is x.

Example : [ 1, 3, 5, 18 ] Find subarray whose sum is 8. Ans: [3,5]

In imperative programming, the solution is given by the two pointers method by maintaining pointers to first and last value of subarray. And the right pointer moves when the resulting subarray sum is lteq to x.

The closest idea I've came up with is through a combination of scanl and takeWhile, but I wasn't able to visualize how to slide the 'worm' as we scan for the solution.

My naive implementation of the algo described

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)
3 Answers

One thing you can always do is write a direct translation of the imperative code into 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

Edit: The following functional solution is not yet O(n), sorry.

For a more functional solution, I would derive it as follows. You already suggested scanl and takeWhile, I would combine it with tails to get a list of all the sums of each subsequence:

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

(Note you can write it with the monadic bind instead: takeWHile (<= 5) . scanl (+) 0 =<< tails [1,2,3] which I think looks better)

With this we can easily solve the decision problem version where we just want to determine if there is a subsequence with a specific sum:

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

To get the actual subsequence, I think the easiest way is to change the arguments of scanl to also build up the subsequences:

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,[])]

Now we can simply use the lookup function to find the subsequence:

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

And you might notice that this returns the subsequence reversed, so you can simply reverse it again at the end:

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

Here's a relatively straightforward way to solve the problem:

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

If getSubarrayIndices target list = Just (beginIdx, endIdx) then target = sum (drop beginIdx (take endIdx list)). If getSubarrayIndices target list = Nothing then there is no contiguous subsequence of list summing to target.

It's important to note that one should rarely use lists as a means for storing data. If you're looking for high performance, lists in Haskell should really be viewed as a replacement for control flow mechanisms in imperative languages, not as a means to store data that will be looped over multiple times. The asymptotic time complexity of using lists and using an array in this case will be equal, but the constant factors will favour the use of an array. In this case, we can actually improve the memory complexity of the algorithm by creating two copies of prefixSumsWithIndices as follows:

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 ())

This is because in the original algorithm, when end gets ahead of begin, all the intermediate prefix sums are stored in memory somewhere. In the second version of the algorithm, which creates two lists, both lists will be produced as they are consumed, so there are no intermediate prefix sums to store as the intermediate prefix sums are recalculated by begin. Thus, our new code uses O(1) auxiliary memory.

Since you mention a two-pointer method, we can actually use one. We'll keep one pointer to the first element of the subsequence, and one pointer just past the last. We'll also keep track of the size of the subsequence and its sum.

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
Related