在上一篇随笔“回文数问题”中,要找出比指定的数大的最小回文数,而这个数可能包含有百万个十进制数字。所以不能使用大整数来求解。现在让我们来看看应该使用大整数来求解的几道ACM题,这些题目均来源于 Sphere Online Judge (SPOJ) 网站。
Not So Fast Multiplication
首先是一道大整数乘法的题目,主要内容如下所示:
也就是说,要求在十二秒之内计算大约一千道的整数乘法,而参数与运算的每个整数大约都有一万个十进制数字。由于 Ruby 语言内置支持 Bignum ,所以程序非常简单,就是以下这么四行:
gets.to_i.downto(1) do a, b = gets.split puts a.to_i * b.to_i end
在该网站提交,结果是“accepted”。运行时间是 10.19 秒,内存占用为 10 MB,目前在 RUBY 语言中排名第五位。
相应的 F# 程序如下:
for _ in 1 .. int(System.Console.ReadLine()) do let ss = System.Console.ReadLine().Split() bigint.Parse(ss.[0]) * bigint.Parse(ss.[1]) |> printfn "%O"
在该网站提交,结果超时了。看来该网站的 F# 的性能不怎么样。
Fast Multiplication
这道题目的内容和上一道题目一样,仅是时间限制从十二秒改为两秒。在该网站提交上述 Ruby 程序,非常遗憾,结果是“time limit exceeded”。看来 Ruby 语言内置的 Bignum 类的乘法运算速度还不够快。 :(
我在2008年7月写过一篇随笔“再谈 BigInteger - 使用快速傅里叶变换”,用 C# 语言实现了 Skyiv.Numeric.BigInteger 类。那么,就让我们使用 C# 语言来求解这道题目吧:
01: using System; 02: using Skyiv.Numeric; 03: 04: namespace Skyiv.SphereOnlineJudge 05: { 06: // http://www.spoj.pl/problems/MUL/ 07: sealed class Multiplication 08: { 09: static void Main() 10: { 11: for (var i = int.Parse(Console.ReadLine()); i > 0; i--) 12: { 13: var ss = Console.ReadLine().Split(); 14: Console.WriteLine(BigInteger.Parse(ss[0]) * BigInteger.Parse(ss[1])); 15: } 16: } 17: } 18: }
在该网站提交,终于“accepted”了,运行时间是 1.49 秒,内存占用为 13 MB,目前在 C# 语言中排名第一位,也是唯一的一位。在 Microsoft .NET Framework 4 Base Class Library 中也有 System.Numerics.BigInteger 结构,其乘法是使用 Karatsuba 算法,其时间复杂度约为 O(N1.585),相关的源代码请参阅“浅谈 BigInteger”。而我的 Skyiv.Numeric.BigInteger 类的乘法是使用快速傅里叶变换,时间复杂度可降低到 O(N logN loglogN)。Sphere Onlile Judge (SPOJ) 网站使用的 C# 编译器是 Mono C# compiler version 2.0.1,并不支持 System.Numerics.BigInteger 结构。即使该网站支持 Microsoft(R) Visual C# 2010 编译器 4.0.30319.1 版,使用 .NET 4 内置的 BigInteger 来做这道题目也会和使用 Ruby 语言内置的 Bignum 一样超时。
Small factorials
这是一道求解 100 以内的数的阶乘的题目,主要内容如下所示:
也就是说,要求在一秒之内计算出大约 100 个 100 以内的数的阶乘。注意阶乘增长是非常快的,100! 就有 158 位十进制数字。下面就是 Ruby 程序:
hash = {} prod = 1 1.upto(100) do |i| prod *= i hash[i] = prod end gets.to_i.downto(1) do puts hash[gets.to_i] end
在该网站提交后,结果是“accepted”,运行时间是 0.04 秒,内存占用为 4.7 MB。可以看出 Ruby 语言在进行比较小的数的乘法时还是很快的,因为它会根据数的大小自动在 Fixnum 和 Bignum 之间切换。
下面来看看 C# 程序吧:
01: using System; 02: 03: namespace Skyiv.SphereOnlineJudge 04: { 05: // http://www.spoj.pl/problems/FCTRL2/ 06: sealed class SmallFactorial 07: { 08: static void Main() 09: { 10: var array = GetArray(100); 11: for (var i = int.Parse(Console.ReadLine()); i > 0; i--) 12: Console.WriteLine(array[int.Parse(Console.ReadLine())]); 13: } 14: 15: static BigInteger[] GetArray(int n) 16: { 17: var array = new BigInteger[n + 1]; 18: BigInteger prod = 1; 19: for (var i = 1; i <= n; i++) 20: { 21: prod *= i; 22: array[i] = prod; 23: } 24: return array; 25: } 26: } 27: 28: sealed class BigInteger 29: { 30: int[] digits = new int[180]; 31: 32: public BigInteger(int n) 33: { 34: digits[0] = n; 35: if (digits[0] > 9) Format(); 36: } 37: 38: public BigInteger(BigInteger x) 39: { 40: Array.Copy(x.digits, digits, digits.Length); 41: } 42: 43: public static implicit operator BigInteger(int x) 44: { 45: return new BigInteger(x); 46: } 47: 48: public static BigInteger operator +(BigInteger x, BigInteger y) 49: { 50: BigInteger z = new BigInteger(x); 51: for (int i = x.digits.Length - 1; i >= 0; i--) z.digits[i] = x.digits[i] + y.digits[i]; 52: z.Format(); 53: return z; 54: } 55: 56: public static BigInteger operator *(BigInteger x, int y) 57: { 58: BigInteger z = new BigInteger(x); 59: for (int i = x.digits.Length - 1; i >= 0; i--) z.digits[i] = x.digits[i] * y; 60: z.Format(); 61: return z; 62: } 63: 64: void Format() 65: { 66: for (int quotient = 0, i = 0; i < digits.Length; i++) 67: { 68: int numerator = digits[i] + quotient; 69: quotient = numerator / 10; 70: digits[i] = numerator % 10; 71: } 72: } 73: 74: public override string ToString() 75: { 76: int n = digits.Length - 1; 77: while (n >= 0 && digits[n] == 0) n--; 78: if (n < 0) return "0"; 79: char[] cs = new char[n + 1]; 80: for (int i = n; i >= 0; i--) cs[i] = (char)(digits[n - i] + '0'); 81: return new string(cs); 82: } 83: } 84: }
注意,这道题目要求源程序的大小限制的 2,000 字节以内。如果没有这个限制的话,就很容易作弊,也就是事先计算好所有的 100 以内的数的阶乘,然后把这些计算出来的值放到源程序的一个静态字符串数组中,在程序中读取输入后直接查表输出就行了。而且,由于这个限制,就不能使用前面提到的使用快速傅里叶变换的 Skyiv.Numeric.BigInteger 类了。这里使用的是“Timus 1013. K-based numbers. Version 3”中的简单的只有加法和乘法的 BigInteger 类。
在该网站提交,结果也是“accepted”,运行时间是 0.17 秒,内存占用为 11 MB,目前在 C# 语言中排名第一位,仅领先第二名 0.01 秒。但是这个结果比 Ruby 语言的 0.04 秒慢了很多。
Factorial
这道题目的主要内容如下所示:
别看这道题目说了一大堆,其实大部分内容都是在讲一个有趣的故事,实际上只是要求计算指定的正整数的阶乘末尾有多少个零。但是要计算的数大约有十万个,而每个数可能达到十亿,并且需要在六秒之内计算完毕。天哪,阶乘函数可是增长得非常快的,十亿的阶乘有多少位十进制数字?我晕了!其实,这是一个经典问题,并不需要用到大整数,使用普通的 Int32 就足够了:
01: using System; 02: using System.IO; 03: 04: namespace Skyiv.SphereOnlineJudge 05: { 06: // http://www.spoj.pl/problems/FCTRL/ 07: class Factorial 08: { 09: static void Main() 10: { 11: new Factorial().Run(Console.In, Console.Out); 12: } 13: 14: void Run(TextReader reader, TextWriter writer) 15: { 16: for (int t = int.Parse(reader.ReadLine()); t > 0; t--) 17: { 18: var n = int.Parse(reader.ReadLine()); 19: writer.WriteLine(FactorialTailZeros(n)); 20: } 21: } 22: 23: int FactorialTailZeros(int n) 24: { 25: var sum = 0; 26: while ((n /= 5) > 0) sum += n; 27: return sum; 28: } 29: } 30: }
在该网站提交后,运行结果是“accepted”,运行时间是 2.97 秒,内存占用为 11 MB。
下面来看看等效的 Ruby 程序吧:
def get_factorial_tail_zeros n zeros = 0 zeros += n while (n /= 5) > 0 zeros end gets.to_i.downto(1) do puts get_factorial_tail_zeros gets.to_i end
在该网站提交后,结果也是“accepted”,运行时间是 2.18 秒,内存占用为 4.7 MB,目前在 Ruby 语言中排名第二位。这个 Ruby 程序的算法和前面的 C# 程序是一模一样的,但是源程序更短小,运行时间更短,内存占用更少,样样都比 C# 好。 :)
Adding Reversed Numbers
这道题目的主要内容如下所示:
讲述古代的洗具和杯具的故事,为了把杯具转换为洗具,需要把一些(很大的)整数倒转过来。题目给出大约一万对已经倒转了整数,要求计算出每一对整数的和,然后再倒转。好了,下面就是我们的 Ruby 程序:
def reverse n n.to_s.reverse.to_i end gets.to_i.downto(1) do a, b = gets.split puts reverse reverse(a.to_i) + reverse(b.to_i) end
在该网站提交后,运行结果是“accepted”,运行时间是 0.49 秒,内存占用为 4.7 MB。这道题目只涉及大整数的加法和字符串的反转。
下面来看看 C# 程序吧:
01: using System; 02: 03: namespace Skyiv.SphereOnlineJudge 04: { 05: // http://www.spoj.pl/problems/ADDREV/ 06: sealed class AddingReversedNumbers 07: { 08: static void Main() 09: { 10: for (var i = int.Parse(Console.ReadLine()); i > 0; i--) 11: { 12: var ss = Console.ReadLine().Split(); 13: Console.WriteLine(ReverseAdd(ss[0], ss[1])); 14: } 15: } 16: 17: static string ReverseAdd(string a, string b) 18: { 19: if (a.Length < b.Length) Swap(ref a, ref b); 20: var sum = new char[a.Length + 1]; 21: int carry, i; 22: for (carry = i = 0; i < a.Length; i++) 23: { 24: var c = a[i] + carry; 25: if (i < b.Length) c += b[i] - '0'; 26: sum[i] = (char)((c > '9') ? (c - 10) : c); 27: carry = (c > '9') ? 1 : 0; 28: } 29: if (carry == 1) sum[i] = '1'; 30: return new string(sum).TrimStart('0').TrimEnd('\0'); 31: } 32: 33: static void Swap<T>(ref T a, ref T b) 34: { 35: T c = a; 36: a = b; 37: b = c; 38: } 39: } 40: }
上述程序中,没有使用 BigInteger 和字符串反转,而是直接对输入的字符串进行 ReverseAdd (第 17 到 31 行)。在该网站提交后,运行结果是“accepted”,运行时间是 0.57 秒,内存占用为 11 MB。
Julka
这道题目的主要内容如下所示:
这次讲述几个小女孩和苹果的故事。要求我们计算的大整数大约有 100 个十进制数字。Ruby 程序如下所示:
1.upto(10) do sum = gets.to_i diff = gets.to_i puts (sum + diff) / 2 puts (sum - diff) / 2 end
在该网站提交后,运行结果是“accepted”,运行时间是 0.03 秒,内存占用为 4.7 MB。这道题目只涉及大整数的加法和减法。
相应的 F# 程序如下所示:
for _ in 1 .. 10 do let sum = bigint.Parse(System.Console.ReadLine()) let diff = bigint.Parse(System.Console.ReadLine()) (sum + diff) / 2I |> printfn "%O" (sum - diff) / 2I |> printfn "%O"
在该网站提交后,运行结果是“accepted”,运行时间是 0.48 秒,内存占用为 12 MB。看来该网站的 F# 的性能比不上 Ruby。