银河

SKYIV STUDIO

  博客园 :: 首页 ::  ::  :: 订阅 订阅 :: 管理 ::
  105 随笔 :: 2 文章 :: 753 评论 :: 22 引用

我在“浅谈 BigInteger”的随笔中实现了一个 Skyiv.Numeric.BigInteger 类,那时乘法是使用常规的 O(N2) 的算法,所以比 .NET Framework 3.5 Base Class Library 中的 System.Numeric.BigInteger 类稍慢,后者的乘法是使用 Karatsuba 算法,其时间复杂度约为 O(N1.585)。

现在,我已经实现了一个提供任意精度的算术运算的静态类: BigArithmetic,该类使用快速傅里叶变换计算大整数乘法,其时间复杂度降低到 O(N logN loglogN)。所以,Skyiv.Numeric.BigInteger 类也重新改写调用 BigArithmetic 类的静态方法来进行算术运算。

下面就是改写后的 Skyiv.Numeric.BigInteger 类的源程序代码:

  1 using System;
  2 using System.Text;
  3 
  4 namespace Skyiv.Numeric
  5 {
  6   sealed class BigInteger : IEquatable<BigInteger>, IComparable<BigInteger>
  7   {
  8     static readonly byte Len = 2;
  9     static readonly byte Base = (byte)Math.Pow(10, Len);
 10 
 11     sbyte sign;  // 符号,取值:-1, 0, 1。
 12     byte[] data; // 字节数组以 100 为基,字节数组中第一个元素存储的数字是最高有效位。
 13 

因为 BigArithmetic 类是对字节数组进行算术运算,字节数组以 100 为基,为了方便调用 BigArithmetic 类的静态方法进行算术运算,BigInteger 也改为使用以 100 为基的字节数组来存储。原来是使用 109 为基的 List<int> 来存储的。

我在改写过程中曾经使用过以 108 为基的 List<int> 来存储,在需要调用 BigArithmetic 类的静态方法进行算术运算时将参数转换为以 100 为基的字节数组,运算完成后再将运算结果转换回来。这样做的好处是加法和减法运算比较快(还使用原来的方法,不调用 BigArithmetic 类的静态方法),并且除了 operator *、DivRem 和 Sqrt 方法以外,其他所有的方法都不用改写。不过经过综合考虑,还是采用现在的方案。

 

 14     BigInteger()
 15     {
 16     }
 17 
 18     BigInteger(long x)
 19     {
 20       sign = (sbyte)((x == 0? 0 : ((x > 0? 1 : -1));
 21       data = new byte[10]; // long.MinValue = -9,223,372,036,854,775,808
 22       ulong z = (x < 0? (ulong)-x : (ulong)x;
 23       for (int i = data.Length - 1; z != 0; i--, z /= Base) data[i] = (byte)(z % Base);
 24       Shrink();
 25     }
 26 
 27     BigInteger(BigInteger x)
 28     {
 29       sign = x.sign; // x != null
 30       data = new byte[x.data.Length];
 31       Array.Copy(x.data, data, data.Length);
 32     }
 33 
 34     public static implicit operator BigInteger(long x)
 35     {
 36       return new BigInteger(x);
 37     }
 38 

私有的构造函数,和原来的大同小异。

 

 39     public static BigInteger Parse(string s)
 40     {
 41       if (s == nullreturn null;
 42       s = s.Trim().Replace(","null);
 43       if (s.Length == 0return 0;
 44       BigInteger z = new BigInteger();
 45       z.sign = (sbyte)((s[0== '-'? -1 : 1);
 46       if (s[0== '-' || s[0== '+') s = s.Substring(1);
 47       int r = s.Length % Len;
 48       z.data = new byte[s.Length / Len + ((r != 0? 1 : 0)];
 49       int i = 0;
 50       if (r != 0) z.data[i++= byte.Parse(s.Substring(0, r));
 51       for (; i < z.data.Length; i++, r += Len) z.data[i] = byte.Parse(s.Substring(r, Len));
 52       z.Shrink();
 53       return z;
 54     }
 55 

静态的 Parse 方法,也和原来的大同小异。

程序的第 56 到 95 行以及 111 到 116 行是 Abs、Pow 方法以及单目 +、-、++ 和 -- 运算符,以及减法(-)运算符,和原来的相同。

 

 96     public static BigInteger operator +(BigInteger x, BigInteger y)
 97     {
 98       if (x == null || y == nullreturn null;
 99       if (x.AbsCompareTo(y) < 0) Utility.Swap(ref x, ref y);
100       BigInteger z = new BigInteger();
101       z.sign = x.sign;
102       byte[] bs = Utility.Expand(y.data, x.data.Length);
103       bool isAdd = x.sign * y.sign == 1;
104       z.data = new byte[x.data.Length + (isAdd ? 1 : 0)];
105       if (isAdd) BigArithmetic.Add(z.data, x.data, bs, bs.Length);
106       else BigArithmetic.Subtract(z.data, x.data, bs, bs.Length);
107       z.Shrink();
108       return z;
109     }
110

加法(+)运算符,调用 BigArithmetic 类的 Add 和 Subtract 方法。


117     public static BigInteger operator *(BigInteger x, BigInteger y)
118     {
119       if (x == null || y == nullreturn null;
120       if (x.sign * y.sign == 0return 0;
121       BigInteger z = new BigInteger();
122       z.sign = (sbyte)(x.sign * y.sign);
123       z.data = new byte[x.data.Length + y.data.Length];
124       BigArithmetic.Multiply(z.data, x.data, x.data.Length, y.data, y.data.Length);
125       z.Shrink();
126       return z;
127     }
128 

乘法(*)运算,直接调用 BigArithmetic 类的 Multiply 方法。

程序的第 129 到 141 行是 / 和 % 运算符,和原来的相同。

 

142     public static BigInteger DivRem(BigInteger dividend, BigInteger divisor, out BigInteger remainder)
143     {
144       remainder = null;
145       if (dividend == null || divisor == nullreturn null;
146       if (divisor.sign == 0throw new DivideByZeroException();
147       if (dividend.AbsCompareTo(divisor) < 0)
148       {
149         remainder = new BigInteger(dividend);
150         return 0;
151       }
152       BigInteger quotient = new BigInteger();
153       remainder = new BigInteger();
154       quotient.data = new byte[dividend.data.Length - divisor.data.Length + 1];
155       remainder.data = new byte[divisor.data.Length];
156       BigArithmetic.DivRem(quotient.data, remainder.data, dividend.data,
157         dividend.data.Length, divisor.data, divisor.data.Length);
158       quotient.sign = (sbyte)(dividend.sign * divisor.sign);
159       remainder.sign = dividend.sign;
160       quotient.Shrink();
161       remainder.Shrink();
162       return quotient;
163     }
164 

静态的 DevRem 方法,也是调用 BigArithmetic 类的 DivRem 方法。

 

165     public static BigInteger Sqrt(BigInteger x)
166     {
167       if (x == null || x.sign < 0return null;
168       if (x.sign == 0return 0;
169       if (x.data.Length == 1return new BigInteger((long)Math.Sqrt(x.data[0]));
170       BigInteger z = new BigInteger();
171       z.sign = 1;
172       z.data = new byte[x.data.Length / 2 + 3];
173       z.data = Adjust(BigArithmetic.Sqrt(z.data, z.data, z.data.Length, x.data, x.data.Length), x.data.Length);
174       z.Shrink();
175       BigInteger z1 = z + 1// 平方根有可能比实际小 1。
176       return (z1 * z1 <= x) ? z1 : z;
177     }
178 
179     static byte[] Adjust(byte[] bs, int digits)
180     {
181       if (bs[0>= 10throw new OverflowException("sqrt adjust");
182       byte[] nbs = new byte[(digits + 1/ 2];
183       if (digits % 2 == 0)
184         for (int k = bs[0], i = 0; i < nbs.Length; i++, k = bs[i] % 10)
185           nbs[i] = (byte)(k * 10 + bs[i + 1/ 10);
186       else Array.Copy(bs, nbs, nbs.Length);
187       return nbs;
188     }
189 

求平方根(Sqrt),调用 BigArithmetic 类的 Sqrt 方法。但是要注意,BigArithmetic 类的 Sqrt 方法求得的平方根是小数,需要使用 Adjust 方法调整为整数。并且平方根有可能比实际的小 1,也需要判断和调整。

 

190     void Shrink()
191     {
192       int i;
193       for (i = 0; i < data.Length; i++if (data[i] != 0break;
194       if (i != 0)
195       {
196         byte[] bs = new byte[data.Length - i];
197         Array.Copy(data, i, bs, 0, bs.Length);
198         data = bs;
199       }
200       if (data.Length == 0) sign = 0;
201     }
202 

私有的 Shrink 方法用于在需要时收缩存储数据的字节数组。

程序的第 203 到 238 行是各种逻辑运算符,和原来的完全一样。

239     public override string ToString()
240     {
241       StringBuilder sb = new StringBuilder();
242       if (sign < 0) sb.Append('-');
243       sb.Append((data.Length == 0? 0 : (int)data[0]);
244       for (int i = 1; i < data.Length; i++) sb.Append(data[i].ToString("D" + Len));
245       return sb.ToString();
246     }
247 

从基类继承的 ToString 方法,和原来的大同小异。

程序的第 248 到 274 行是从基类继承的 GetHashCode、Equals 方法,以及用于实现接口的 Equals 和 CompareTo 方法,和原来的相同。

 

275     int AbsCompareTo(BigInteger other)
276     {
277       if (data.Length < other.data.Length) return -1;
278       if (data.Length > other.data.Length) return 1;
279       return BigArithmetic.Compare(data, other.data, data.Length);
280     }
281   }
282 }

最后是私有的 AbsCompareTo 方法,调用 BigArithmetic 类的 Compare 方法。

下面是程序中用到的辅助类:

 1 using System;
 2 
 3 namespace Skyiv
 4 {
 5   static class Utility
 6   {
 7     public static T[] Expand<T>(T[] x, int n)
 8     {
 9       T[] z = new T[n]; // assume n >= x.Length
10       Array.Copy(x, 0, z, n - x.Length, x.Length);
11       return z;
12     }
13 
14     public static void Swap<T>(ref T x, ref T y)
15     {
16       T z = x;
17       x = y;
18       y = z;
19     }
20   }
21 }

 

经过运行测试程序,发现计算 256,142 位的乘积的运行时间从原来的 34.2497761 秒降低到 0.1749114 秒,速度提高了将近二百倍。如果计算的数字更大的话,速度将提高得更多。因为原来乘法的时间复杂度是 O(N2)。而现在乘法使用 FFT,时间复杂度是 O(N logN loglogN)。

我想,这个 BigInteger 类的运算速度已经是非常快的了。不知道有没有其他 C# 的 BigInteger 类的运算速度更快。

本随笔中提到的所有源程序代码可以到这里下载。

posted on 2008-07-25 22:07 银河 阅读(951) 评论(13)  编辑 收藏 网摘 所属分类: 算法

评论

#1楼  2008-07-25 22:30 空间/IV      
沙发先。
  回复  引用  查看    

#2楼  2008-07-25 22:57 dudu      
@空间/IV
这么少的字抢沙发,要浪费多少人的点击。
以为有什么好的评论,打开一看,原来竟是没有实际内容的沙发。
  回复  引用  查看    

#3楼  2008-07-25 23:26 Angel Lucifer      
楼主深入钻研的精神值得称赞!
顶你个肺!
哈哈。
  回复  引用  查看    

#4楼  2008-07-26 17:25 JimLiu      
终于看到了博主的强作!
除法也改进了吗?
  回复  引用  查看    

#5楼  2008-07-26 17:41 JimLiu      
嗯……在我们的OJ上测了一下,可能是数据不够BT的原因,乘法的速度没有你原来那个版本快
果然FFT的优势只有在大数据上才能体现出来
回头我再看一下我们的OJ上那个高精乘法的数据有多大
我估计1M位以下都不会比传统方法快吧
  回复  引用  查看    

素晴らしい
  回复  引用    

#7楼 [楼主] 2008-07-28 21:07 银河      
@JimLiu (5楼)
确实,FFT 的优势只有在大数据上才能体现出来。不过,根据我在 Timus Online Judge 网站在线提交的结果来看,大概在 1027,000 左右 FFT 做乘法的优势就非常明显了,大约是 1.5 秒 vs 0.1 秒。
如果是求平方根,由于新程序使用了二次收敛的牛顿迭代法以及迭代过程中的乘法使用 FFT 双重因素影响,在 101,000 左右新程序的优势就比较明显了,大约是 0.6 秒 vs 0.1 秒。

  回复  引用  查看    

#8楼 [楼主] 2008-07-28 21:20 银河      
@JimLiu (4楼)
> 除法也改进了吗?
是的,除法也改进,体现在两个方面,首先,是使用二次收敛的牛顿迭代法求除数的倒数后再乘以被除数,其次,这个乘法使用 FFT。
所以,在大数据的情况下应该会快很多。
希望你用这个新程序试试她在 http://coder.buct.edu.cn/oj/Problem.aspx?pid=1594 题目中能运行得多快。
谢谢!
  回复  引用  查看    

#9楼  2008-07-29 00:31 JimLiu      
@银河
http://coder.buct.edu.cn/oj/Problem.aspx?pid=1594
最多只有2000位,体现不了什么优势,反而会慢
  回复  引用  查看    

#10楼  2008-07-29 10:58 JimLiu      
@银河
不过话说回来,算法还真是适合C/C++,我用C++写的,使用的简单算法,O(n^2),同样的算法在OJ上跑,C++的就才109ms,C#的就200+了
  回复  引用  查看    

#11楼 [楼主] 2008-07-29 15:18 银河      
@JimLiu (9楼)
> http://coder.buct.edu.cn/oj/Problem.aspx?pid=1594
> 最多只有2000位,体现不了什么优势,反而会慢
我认为可以试一下。因为我在 Timus 网点在线提交的话,求 1000 位左右的平方根新程序的优势就比较明显了,大约是 0.6 秒 vs 0.1 秒。而除法的算法和求平方根的算法是差不多的。
  回复  引用  查看    

#12楼 [楼主] 2008-07-29 15:22 银河      
@JimLiu (10楼)
是呀,C/C++ 是非常适合在线解 ACM 题目的。
如果使用同样的算法,C# 程序运行的时间可能会比 C/C++ 长一点,因为 C# JIT 编译的时间也是要计算到运行时间里去的,就是什么也不做的程序也需要几十毫秒,而 C/C++ 程序最短只需 1 毫秒。但是,我想大多数题目不会因为这几十毫秒而超时吧。
  回复  引用  查看    

#13楼  2008-07-30 17:07 JimLiu      
@银河
一般来说除非题目故意卡时间(我同学就出过卡900ms的题目),用C++不会TLE的题目应该C#都不会TLE吧。
但是不知道为什么ACM比赛一直不支持用C#,TopCoder倒是支持。

Time(ms) Mem(KB) Code.Len(B)
109 1140 9499 //我用C++写的,没用STL,O(N^2)简单算法
468 1052 28337 //你的FFT版本
218 1340 11006 //你上一个版本


样例数据最多1000位
109应该是简单算法的极限了,我和另一个同学的都是109ms,没用STL,用vector的话会慢很多。我是直接开a.len + b.len长的数组的。需要扩容的时候开2倍长度的数组,用memcpy。

  回复  引用  查看    


标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-07-25 22:48 编辑过
"五向定位"职业成长路线公开课(上海、南京、大连)
Google站内搜索


相关链接: