数组索引触发元素的重复评估?

时间:2019-04-13 11:41:25

标签: haskell

我正在编写一个程序来生成形式的前k个数字

enter image description here

其中 a b 是非负整数。

我正在使用的算法利用了以下事实:该系列中的每个数字都是通过将1或sqrt(2)与该系列中的前一个数字相加而生成的,该系列从0开始。我们将在数组中的序列,其中每个元素均应按需延迟评估。系列中的第一个数字为0,因此我们将数组的第一个元素初始化为0。我们将维护两个指针 i j ,它们均从索引0开始(系列中的第一个数字)。系列中的下一个数字是 min(A [i] +1,A [j] + sqrt(2))。如果系列中的下一个数字来自指针 i ,则指针 i 将递增(因为我们从不希望添加 再次从1到A [i]),并且类似地,如果系列中的下一个数字来自指针 j ,则 j 将递增以指向下一个索引,即 j +1 。如果 A [i] + 1 = A [j] + sqrt(2),则 i j 都将增加。

我认为下面的代码片段捕获了上述算法,但是,在ghci中运行代码时,似乎函数f的参数1 0 0被重复调用,程序永不结束。尽管好像是v A.! i触发了该调用,但我不知道如何进行此调用。

import qualified Data.Array as A 
import Debug.Trace 

compute :: Int -> Int -> Double
compute x y = (fromIntegral x) + (fromIntegral y) * (sqrt 2)

firstK :: Int -> [(Int,Int)]
firstK k = A.elems v where 
    v = A.listArray (0,k-1) $ (0,0):f 1 0 0
    f c i j 
        | c == k = [] 
        | otherwise = traceShow (c,i,j) $
            let (ix,iy) = v A.! i 
                (jx,jy) = v A.! j 
                iv      = compute (ix+1) iy 
                jv      = compute jx (jy+1)
            in  if iv < jv 
                    then (ix+1,iy):f (c+1) (i+1) j 
                else if iv > jv 
                    then (jx,jy+1):f (c+1) i (j+1)
                    else (ix+1,iy):f (c+1) (i+1) (j+1)

输出-

λ> firstK 50
(1,0,0)
(1,0,0)
(1,0,0)
(1,0,0)
(1,0,0)
(1,0,0)
(1,0,0)
(1,0,0)
(1,0,0)
(1,0,0)
(1,0,0)
(1,0,0)
(1,0,0)
(1,0,0)
...
...

3 个答案:

答案 0 :(得分:4)

chi的答案详细讨论了代码的确切问题,以及可以使您保留实现想法但得到希望的答案的修复程序。但是,我想看看母语为Haskell的讲者将如何处理相同的算法也很有趣:我将只使用本地列表,而不是使用数组指针,而是使用列表指针本身来跟踪我们在哪里。看起来像这样:

vs :: [(Int,Int)]
vs = (0,0) : merge [(a+1, b) | (a, b) <- vs] [(a, b+1) | (a, b) <- vs] where
    compute (a,b) = fromIntegral a + fromIntegral b * sqrt 2
    merge xs@(xh:xt) ys@(yh:yt) = case compare (compute xh) (compute yh) of
        LT -> xh:merge xt ys
        EQ -> xh:merge xt yt
        GT -> yh:merge xs yt

在对merge的递归调用中,使用xt(而不是xs)类似于递增i指针;使用yt(而不是ys)类似于增加j指针。

关于vs的一件好事是,您不必提前声明所需的对-您只需获得一个懒惰的无穷列表即可(使用{{1 }} Double!)。该代码也大大缩短了。这是在ghci中运行的示例:

compute

答案 1 :(得分:1)

问题在于arrayList在实际构建数组之前过早强制列表的所有书脊。这使得递归循环永远存在。

但是,我们可以使用以下辅助功能来解决此问题。

-- A spine-lazy version of arrayList
arrayListLazy :: (Int, Int) -> [a] -> A.Array Int a
arrayListLazy (lo,hi) xs = A.array (lo,hi) $ go lo xs
   where
   go i _ | i > hi = []
   go i ~(e:ys) = (i, e) : go (succ i) ys

firstK :: Int -> [(Int,Int)]
firstK k = A.elems v where 
    v = arrayListLazy (0,k-1) $ (0,0):f 1 0 0
    f c i j 
        | c == k = [] 
        | otherwise = traceShow (c,i,j) $
            let (ix,iy) = v A.! i 
                (jx,jy) = v A.! j 
                iv      = compute (ix+1) iy 
                jv      = compute jx (jy+1)
            in  if iv < jv 
                    then (ix+1,iy):f (c+1) (i+1) j 
                else if iv > jv 
                    then (jx,jy+1):f (c+1) i (j+1)
                    else (ix+1,iy):f (c+1) (i+1) (j+1)

小测试:

> firstK 10
[(0,0),(1,0),(0,1),(2,0),(1,1),(0,2),(3,0),(2,1),(1,2),(4,0)]

答案 2 :(得分:1)

使用相同想法的另一种方法,

firstK k = take k $ l
  where l = (0,0) : unfoldr (\(i,j) -> Just $ case compare (uncurry compute $ first (1+) $ l!!i) (uncurry compute $ second (1+) $ l!!j) of
          LT -> (first (1+) $ l!!i, (i+1,j))
          EQ -> (first (1+) $ l!!i, (i+1,j+1))
          GT -> (second (1+) $ l!!j, (i,j+1))) (0,0)

并不是特别整洁,但是您可以看到它的去向。特别是,与case compare _ _ of ...匹配的模式比if a > b else if a < b else ..更好。

编辑:尽管时间复杂度较差。