我刚遇到一个具有挑战性的问题(来自编程竞赛实践),其中包含递归序列,如下所示
给出3个数字 m n k 找到元素a [k]其中
a[0] = m
a[1] = n
a[i] = a[i-1] + a[i-2] ; if floor(i/2) mod 2 = 1
a[i] = a[i-1] - a[i-4] ; if floor(i/2) mod 2 = 0
示例案例:对于m = 2 n = 3 k = 6,答案为9
a[0] = 2
a[1] = 3
a[2] = 3 + 2 = 5
a[3] = 5 + 3 = 8
a[4] = 8 - 2 = 6
a[5] = 6 - 3 = 3
a[6] = 3 + 6 = 9
...
这就是我生成序列的方式(显然会消耗大量的堆栈,即使对于前100个元素也会超级慢)
1 fbm :: Int → Int → Int → Int
2 fbm m n 0 = m
3 fbm m n 1 = n
4 fbm m n x = let a = fbm m n (x-1)
5 b = fbm m n (x-2)
6 c = fbm m n (x-4)
7 in case (x `div` 2) `mod` 2 of
8 1 → a + b
9 0 → a - c
10
11 fbs m n = map (λx→fbm m n x) [0..]
由于在big(~1000 +)索引处找到元素所需的问题。我尝试通过尝试仅限于具有4个输入的函数来计算不同的方法,并在列表上递归地应用具有 4元素窗口的函数但是无法成功实现他们中的任何一个(某些东西意味着我无法弄明白该怎么做)
fs1 = map fst $ iterate next (a,b)
where next (a,b) = something
fs2 = m:n:scanl (gen) 2 fs2
where gen [a,b,c,d] = something
fs3 = scanl (genx m n 0 0) (repeat 0)
where genx a b c d = something
问题1:我的任何一种方法都是解决此问题的好方法吗? (+请给我一个如何做的例子)
问题2:如果我出错了,你会如何解决这类问题?
答案 0 :(得分:2)
我想提出两个解决方案,它们也基于dbaupp在这里介绍的memoisation概念。与现有答案不同,以下解决方案使用索引而不是先前元素的值来计算列表的新元素。
第一个想法是关注
fbs :: Int -> Int -> [Int]
fbs m n = m : n : map (fbMake m n) [2 ..]
fbMake :: Int -> Int -> Int -> Int
fbMake m n = f
where f i | (i `div` 2) `mod` 2 == 1 = (xs !! (i - 1)) + (xs !! (i - 2))
| otherwise = (xs !! (i - 1)) - (xs !! (i - 4))
xs = fbs m n
此解决方案从其备忘的前辈中构建fbs m n
列表的元素。不幸的是,由于列表的索引是O(n)
,因此它的执行效果相当差。
索引比列表更好?阵列开始发挥作用。这是第二个解决方案。
import Data.Array
fbs :: Int -> Int -> Int -> [Int]
fbs m n k = m : n : map (fbm m n k) [2 .. k]
fbsArr :: Int -> Int -> Int -> Array Int Int
fbsArr m n k = listArray (0, k) (fbs m n k)
fbm :: Int -> Int -> Int -> Int -> Int
fbm m n k i | (i `div` 2) `mod` 2 == 1 = (xs ! (i - 1)) + (xs ! (i - 2))
| otherwise = (xs ! (i - 1)) - (xs ! (i - 4))
where xs = fbsArr m n k
它与第一个几乎相同,但这次结果在数组中被记忆,并且其元素的索引明显更快。根据我的测试,它为(m, n, k) = (2, 3, 1000)
生成的答案比基于列表的方法快10倍。这种情况下的答案是fbsArr m n k ! k
。
答案 1 :(得分:2)
这个问题类似于“斐波纳契系列”,但在我看来,它们之间存在很大差异。
Memoization 是解决此类问题的常用技术
例如,我们可以用它来计算Fibonacci系列
以下是一个非常简单的说明。它不如zipWith
解决方案那么好,但它仍然是一个线性操作实现。
fib :: Int -> Integer
fib 0 = 1
fib 1 = 1
fib n = fibs !! (n-1) + fibs !! (n-2)
fibs :: [Integer]
fibs = map fib [0..]
如果我们尝试模仿上述fib
和fibs
,也许我们会编写以下代码。
fbm :: Int -> Int -> Int -> Int
fbm m n 0 = m
fbm m n 1 = n
fbm m n x = let a = fbs m n !! (x-1)
b = fbs m n !! (x-2)
c = fbs m n !! (x-4)
in case (x `div` 2) `mod` 2 of
1 -> a + b
0 -> a - c
fbs :: Int -> Int -> [Int]
fbs m n = map (fbm m n) [0..]
但上述fbs
也非常慢。按数组替换列表几乎没有区别。原因很简单,当我们调用fbs
时没有记忆。
如果我们比较fibs
和fbs
的类型签名,答案就会更清晰。
fibs :: [Integer]
fbs :: Int -> Int -> [Int]
其中一个是整数列表,另一个是函数 为了让memoization发生,我们必须以任何方式实现fbs e.g。
fbs m n = let xs = map fbm [0..]
fbm 0 = m
fbm 1 = n
fbm x = let a = xs !! (x-1)
b = xs !! (x-2)
c = xs !! (x-4)
in case (x `div` 2) `mod` 2 of
1 -> a + b
0 -> a - c
in xs
尾递归是解决此类问题的常用方法。
fbm :: Int -> Int -> Int -> (Int, Int, Int, Int)
-- a[0] = m
-- a[1] = n
-- a[2] = m + n
-- a[3] = m + 2 * n
fbm m n 3 = (m+2*n, m+n, n, m)
fbm m n x = case (x `div` 2) `mod` 2 of
1 -> (a+b, a, b, c)
0 -> (a-d, a, b, c)
where (a,b,c,d) = fbm m n (x-1)
最后但并非最不重要的是,这是一个数学解决方案。
a[0] = m
a[1] = n
a[2] = m + n
a[3] = m + 2n
a[4] = 2n
a[5] = n
a[6] = 3n
a[7] = 4n
a[8] = 2n
fbs m n = [m, n, m+n, m+2*n] ++ cycle [2*n, n, 3*n, 4*n]