Complete the Sequence!
今年四月份的时候,我开始重新学起了 Haskell。按照往年的惯例,开始在 SPOJ 上刷题(严格来说不能叫刷,以我卑微的算法能力,一天能做掉一道题目已经是万幸了)。其实,我主要是把以前用别的语言做过的题目,再用 Haskell 做一遍。其中,有一道题 CMPLS - Complete the Sequence! 虽然,不是很难,但是其解题过程着实有趣,尤其是用 Haskell 的时候。
题目的大概意思是给定一个数列 P(n)以及前 S 项目, 求这个数列的后 C 项。其中数列的通项公式可表示为:
\begin{equation} P(n)=a_D.n^D+a_{D-1}.n^{D-1}+...+a_1.n+a_0 \end{equation}上一次做这道题目的时候,我还在学 ruby 也就是 6 年前的样子。我重新翻看自己的解法的时候简直生不如死。代码是这样的。
1: class Array 2: def const? n 3: 1.upto(n) do |i| 4: return false if self[i]!=first 5: end 6: return true 7: end 8: def diff! n 9: 0.upto(n-1) do |i| 10: self[i]=self[i+1]-self[i] 11: end 12: end 13: def computer n 14: return Array.new(n,first) if const? size-1 15: count=size-1 16: while true 17: diff!count 18: count-=1 19: break if const?count 20: end 21: n.times.inject(Array.new(n)) do |res,j| 22: count.upto(size-2) do |i| 23: self[i+1]+=self[i] 24: end 25: res[j]=last 26: res 27: end 28: end 29: end 30: 31: ss= gets.to_i.times.inject([]) do |s,j| 32: n=gets.split[1].to_i 33: s<<gets.split.map{|s| s.to_i}.computer(n).join(" ") 34: end 35: 36: puts ss
在经过一段时间的读码之后,我开始有了一点思路。举个例子,输入序列是:1, 8, 27, 64, 125。也就是正整数的立方的数列。 \(P(n)=n^3\) 。后一个数减去前一个数得到一个新数列 7, 19, 37, 61。继续做前面的操作,获得数列 12, 18, 24。继续操作,获得数列 6, 6。终于我们得到了一个常数列。回看一下,倒数第二个数列是等差数列其通项公式为 \(P(n)=6+6n\) 。
1 8 27 64 125 7 19 37 61 12 18 24 6 6
我把原来的数列命名为 \(A(n)\) ,相邻两项的差的数列设为 \(D(n)\) 。则有定义很容易得到以下结论:
\begin{equation} A(n)=A(n-1)+D(n-1) \end{equation}而经过刚刚的操作我们发现似乎经过操作之后,最后能得到一个常数列。其实这点很好证明。假设 \(A(n)\) 的通项公式的最高次幂为 \(x\) ,那么相邻两数相减就可以把这个最高次幂消去,于是 \(D(n)\) 的通项公式就是最高次为 \(x-1\) 的多项式。如此操作下去就能将 \(n\) 的次方消去,最后剩下常数了。然后,在从常数列算出等差数列,从等差数列继续推算出高次数列。上面的 ruby 代码就是这两个过程。我这里把这两个过程分别叫做 seeds 和 series。seeds 算出从原始数列到各个差数列的首项,而 series 则反向从差数列推算出原始数列。这次我用 Haskell 来解这道题目。 首先解决几个小问题。在 Haskell 中,如何表示一个常数列。这个很简单,在 GHC.List 包中有一个函数 repeat :: a -> [a],比如 repeat 1, 就是 1,1,1,1...常数列。由于 Haskell 的懒惰特性,repeat 1 不会立即求值,只有等到需要的时候才会求值,比如 take 10 (repeat 1),这个表达式只会求出数列的前 10 项,因为从第 11 项开始数列暂时不被使用,所以停止求值。然而我觉得使用 repeat 函数并不过瘾。可以用下面的表达式定义常数列。
ones :: [Int] ones = 1 : ones
等差数列如何表示
odds :: [Int] odds = 1 : map (+2) odds -- or odds = let twos = 2 : twos in 1 : zipWith (+) odds twos
第一种方法比较直白,用语言来表述就是,后一项等于前一项加 2。而第二种表达看上去有些啰嗦,我也用语言解释一下,twos 是一个常数列,odds 数列的每一项等于其前一项和在 twos 数列对应的项的和。把 twos 数列当成差数列,那么 odds 数列就是原数列。也就是说:
dn :: [Int] dn = undefined an :: [Int] an = a1 : zipWith (+) an dn
有了这个两个表达式,就可以写 seeds 和 series 函数了。
1: {-# OPTIONS_GHC -optc-O2 #-} 2: 3: import Control.Monad 4: 5: series :: [Int] -> [Int] 6: series [x] = let s = x : s in s 7: series (x:xs) = let s = x : zipWith (+) (series xs) s in s 8: 9: seeds :: [Int] -> [Int] 10: seeds xs@(x:ts) = if all (==x) ts 11: then [x] 12: else x : seeds (zipWith (-) ts xs) 13: 14: cmpls :: Int -> Int -> [Int] -> [Int] 15: cmpls s c = take c . drop s . series . seeds 16: 17: solveProblems :: [String] -> IO () 18: solveProblems = foldM_ f [] 19: where f [] l = return . map read . words $ l 20: f [s, c] l = putStrLn (unwords . map show $ cmpls s c (map read . words $ l)) 21: >> return [] 22: 23: main :: IO () 24: main = getContents >>= solveProblems . tail . lines