幾個 Haskell 小程式

黃志斌發表於2014-04-26

引言

《Haskell趣學指南》第20頁:

Haskell-p20

在 Haskell 中,最常用的型別是整數型別,共有兩種:

  • Int 是有界的,它的值一定界於最小值和最大值之間。在 64-bit 機器上,最小值一般為 -263,最大值為 263 - 1 。
  • Integer 是無界的,這就意味著可以用它存放非常非常大的數,不過它的效率不如 Int 高。

這兩個整數型別都屬於 Integral 型別類。由於Haskell 支援型別推導,我們在程式中可以不必指明具體的型別。

學習一門新的程式語言的最好方法是動手寫一些程式。那麼,讓我們來使用 Haskell 求解 ACM 題吧,這些題目均來源於 Sphere Online Judge (SPOJ) 網站。

Small factorials

這是一道求解 100 以內的數的階乘的題目,主要內容所下所示:

Small factorials: You are asked to calculate factorials of some small positive integers.

Input: An integer t, 1<=t<=100, denoting the number of testcases, followed by t lines, each containing a single integer n, 1<=n<=100.

Output: For each integer n given at input, display a line with the value of n!

Time limit: 1s

Source limit: 2000B

也就是說,要求在一秒之內計算出大約 100 個 100 以內的數的階乘。注意階乘增長是非常快的,100! 就有 158 位十進位制數字。下面就是 Haskell 程式:

1: main = do
2:   n <- getLine
3:   x <- sequence $ replicate (read n) getLine
4:   mapM_ (print . product . (flip take [1..]) . read) x

在該網站提交後,結果是“accepted”,執行時間是 0.01 秒,記憶體佔用為 3.6 MB。程式分析如下:

  • 第 2 行使用 getLine 函式讀入要計算的除乘的個數。
  • 第 3 行的 read 函式將字串 n 轉換為整數,然後使用 sequence 和 getLine 函式一次性讀入所有要計算的數到列表 x 中。
  • 第 4 行完成所有的計算工作。
  • 使用 mapM_ 函式逐個處理列表 x 中的元素。
  • read 函式將字串轉換為整數,假設為 z。
  • (flip take [1..]) 得到列表 [1..z] 。
  • product 函式將列表 [1..z] 中所有元素相乘,得到 z! 。
  • print 函式輸出計算結果。

Factorial

這道題目的主要內容所下所示:

Factorial:

The most important part of a GSM network is so called Base Transceiver Station (BTS). These transceivers form the areas called cells(this term gave the name to the cellular phone) and every phone connects to the BTS with the strongest signal (in a little simplified view). Of course, BTSes need some attention and technicians need to check their function periodically.

ACM technicians faced a very interesting problem recently. Given a set of BTSes to visit, they needed to find the shortest path to visit all of the given points and return back to the central company building. Programmers have spent several months studying this problem but with no results. They were unable to find the solution fast enough. After a long time, one of the programmers found this problem in a conference article. Unfortunately, he found that the problem is so called "Travelling Salesman Problem" and it is very hard to solve. If we have N BTSes to be visited, we can visit them in any order, giving us N! possibilities to examine. The function expressing that number is called factorial and can be computed as a product 1.2.3.4....N. The number is very high even for a relatively small N.

The programmers understood they had no chance to solve the problem. But because they have already received the research grant from the government, they needed to continue with their studies and produce at least someresults. So they started to study behaviour of the factorial function.

For example, they defined the function Z. For any positive integer N, Z(N) is the number of zeros at the end of the decimal form of number N!. They noticed that this function never decreases. If we have two numbers N1

Input: There is a single positive integer T on the first line of input (equal to about 100,000). It stands for the number of numbers to follow. Then there are T lines, each containing exactly one positive integer number N, 1 <= N <= 1,000,000,000.

Output: For every number N, output a single line containing the single non-negative integer Z(N).

Time limit: 6s

這道題目講述了一個在行動通訊網路中尋找通訊基站的最短路徑的故事,在這個故事中程式設計師發現該演算法的複雜度是 O(n!) 的,所以無法得到有效的解。由於他們已經收了政府的錢,所以必須至少給出一些結果。因此他們就轉而研究整數的階乘末尾有幾個零問題。這道題目要求計算指定的正整數的階乘末尾有多少個零。但是要計算的數大約有十萬個,而每個數可能達到十億,並且需要在六秒之內計算完畢。天哪,階乘函式可是增長得非常快的,十億的階乘有多少位十進位制數字?我暈了!其實,這是一個經典問題,並不需要用到大整數,使用普通的 Int 就足夠了:

 1: import Control.Monad
 2: 
 3: main = do
 4:   n <- getLine
 5:   forM_ [1 .. read n] (\_ -> do
 6:     x <- getLine
 7:     print $ (tailzeros . read) x
 8:     )
 9: 
10: tailzeros 0 = 0
11: tailzeros n = let m = div n 5 in m + tailzeros m

在該網站提交後,結果是“accepted”,執行時間是 3.80 秒,記憶體佔用為 3.6 MB。程式分析如下:

  • 第 1 行匯入 Control.Monad 模組是為了第 5 行的 forM_ 函式。
  • 第 4 行使用 getLine 函式讀入要計算的階乘的個數。
  • 第 5 行使用 forM_ 函式迴圈地進行處理。
  • 第 6 行使用 getLine 函式讀入要計算的數。
  • 第 7 行使用隨後定義的 tailzeros 函式計算某個數的階乘的末尾有多少個零。
  • 第 10 行處理 tailzeros 函式的遞迴的邊界條件,表明 0! 末尾有 0 個 0 。
  • 第 11 行處理 n > 0 的情況,遞迴地呼叫 tailzeros 函式本身來計算。

Not So Fast Multiplication

這是一道大整數乘法的題目,主要內容如下所示:

Not So Fast Multiplication: Multiply the given numbers.

Input:
n [the number of multiplications <= 1,000]
a b [numbers to multiply (at most 10,000 decimal digits each)]
Text grouped in [ ] does not appear in the input file.

Output: The results of multiplications.

Warning: large Input/Output data, be careful with certain languages

Time limit: 12s

也就是說,要求在十二秒之內計算大約一千道的整數乘法,而引數與運算的每個整數大約都有一萬個十進位制數字。Haskell 程式如下所示:

1: import Control.Monad
2: 
3: main = do
4:   n <- getLine
5:   forM_ [1 .. read n] (\_ -> do
6:     x <- getLine
7:     print $ ((\[a, b] -> a * b) . (map read) . words) x
8:     )

在該網站提交,結果是“accepted”。執行時間是 6.44 秒,記憶體佔用為 5.6 MB。該程式的主要部分是在第 7 行,分析如下:

  • 首先使用 words 函式將使用空格分隔的字串轉換為列表。
  • (map read) 將這個字串列表轉換為整數列表。
  • 然後使用 lambda 表示式將列表中的兩個元素相乘。
  • 最後使用 print 函式將乘積輸出。

Adding Reversed Numbers

這道題目的主要內容所下所示:

Adding Reversed Numbers:

The Antique Comedians of Malidinesia prefer comedies to tragedies. Unfortunately, most of the ancient plays are tragedies. Therefore the dramatic advisor of ACM has decided to transfigure some tragedies into comedies. Obviously, this work is very hard because the basic sense of the play must be kept intact, although all the things change to their opposites. For example the numbers: if any number appears in the tragedy, it must be converted to its reversed form before being accepted into the comedy play.

Reversed number is a number written in arabic numerals but the order of digits is reversed. The first digit becomes last and vice versa. For example, if the main hero had 1245 strawberries in the tragedy, he has 5421 of them now. Note that all the leading zeros are omitted. That means if the number ends with a zero, the zero is lost by reversing (e.g. 1200 gives 21). Also note that the reversed number never has any trailing zeros.

ACM needs to calculate with reversed numbers. Your task is to add two reversed numbers and output their reversed sum. Of course, the result is not unique because any particular number is a reversed form of several numbers (e.g. 21 could be 12, 120 or 1200 before reversing). Thus we must assume that no zeros were lost by reversing (e.g. assume that the original number was 12).

Input: The input consists of N cases (equal to about 10,000). The first line of the input contains only positive integer N. Then follow the cases. Each case consists of exactly one line with two positive integers separated by space. These are the reversed numbers you are to add.

Output: For each case, print exactly one line containing only one integer - the reversed sum of two reversed numbers. Omit any leading zeros in the output.

Time limit: 5s

講述古代的洗具和杯具的故事,為了把杯具轉換為洗具,需要把一些(很大的)整數倒轉過來。題目給出大約一萬對已經倒轉了整數,要求計算出每一對整數的和,然後再倒轉。好了,下面就是我們的 Haskell 程式:

 1: import Control.Monad
 2: 
 3: main = do
 4:   n <- getLine
 5:   forM_ [1 .. read n] (\_ -> do
 6:     x <- getLine
 7:     print $ ((\[a, b] -> rev $ rev a + rev b) . (map read) . words) x
 8:     )
 9: 
10: rev = read . reverse . show

在該網站提交後,執行結果是“accepted”,執行時間是 1.65 秒,記憶體佔用為 3.6 MB。程式分析如下:

  • 第 7 行進行主要的計算,這和上一小節的程式的第 7 行差不多,只是其中的 lambda 表示式不同。那裡是計算乘法,這裡是計算倒過來的加法。
  • 第 10 行的 rev 函式將一個整數倒過來。首先使用 show 函式將整數轉換為字串,然後再使用 reverse 函式將字串倒過來,最後使用 read 函式將字串轉換回整數。

Julka

這道題目的主要內容如下所示:

Julka:

Julka surprised her teacher at preschool by solving the following riddle:

Klaudia and Natalia have 10 apples together, but Klaudia has two apples more than Natalia. How many apples does each of he girls have?

Julka said without thinking: Klaudia has 6 apples and Natalia 4 apples. The teacher tried to check if Julka's answer wasn't accidental and repeated the riddle every time increasing the numbers. Every time Julka answered correctly. The surprised teacher wanted to continue questioning Julka, but with big numbers she could't solve the riddle fast enough herself. Help the teacher and write a program which will give her the right answers.

Task:Write a program which

reads from standard input the number of apples the girls have together and how many more apples Klaudia has, counts the number of apples belonging to Klaudia and the number of apples belonging to Natalia, writes the outcome to standard output

Input: Ten test cases (given one under another, you have to process all!). Every test case consists of two lines. The first line says how many apples both girls have together. The second line says how many more apples Klaudia has. Both numbers are positive integers. It is known that both girls have no more than 10100 (1 and 100 zeros) apples together. As you can see apples can be very small.

Output: For every test case your program should output two lines. The first line should contain the number of apples belonging to Klaudia. The second line should contain the number of apples belonging to Natalia.

Time limit: 2s

這次講述幾個小女孩和蘋果的故事。要求我們計算的大整數大約有 100 個十進位制數字。Haskell 程式如下所示:

 1: import Control.Monad
 2: 
 3: main = do
 4:   forM_ [1..10] (\_ -> do
 5:     x <- sequence $ replicate 2 getLine
 6:     print $ f (+) x
 7:     print $ f (-) x
 8:     )
 9: 
10: f g = (\[a, b] -> div (g a b) 2) . (map read)

在該網站提交後,執行結果是“accepted”,執行時間是 0.00 秒,記憶體佔用為 3.6 MB。程式分析如下:

  • 第 4 行直接進行 10 次迴圈,因為這次題目的輸入固定為 20 行,分為 10 組。
  • 第 5 行使用 sequence 和 getLine 函式一次性讀入兩行到列表 x 中。
  • 第 6 行和第 7 行分別呼叫隨後定義的 f 函式進行計算,一次是加法計算,一次是減法計算。注意,(+) 和 (-) 函式作為引數傳入 f 函式。
  • 第 10 行就是進行具體計算的 f 函式了。其中的引數 g 的型別是函式,所以 f 是個高階函式。

後記

這些題目的其他語言解法,可參閱我 2010 年 7 月發表在部落格園的文章:大整數和ACM題。那篇文章中使用了 Ruby、F#、C# 等程式語言。


Haskell

相關文章