大整数ZZ类主要实现了任意长度大整数表示、最大公因数、Jacobi符号和素性检验。笔者将通过逐个分析ZZ.cpp源代码中函数的形式来一步步向读者展示NTL是如何实现上述功能的。
(1)判断输入的I/O是否合法:
static NTL_CHEAP_THREAD_LOCAL long iodigits = 0; static NTL_CHEAP_THREAD_LOCAL long ioradix = 0; static void InitZZIO() { long x; x = (NTL_WSP_BOUND-1)/10; iodigits = 0; ioradix = 1; while (x) { x = x / 10; iodigits++; ioradix = ioradix * 10; } if (iodigits <= 0) TerminalError("problem with I/O"); }
注释:
这里的NTL_WSP_BOUND的值为(1L<<30)。因为长整型是一个有符号数,其二进制数的最高位是是符号位,所以一个长整型数最多表示范围是[-2147483647, 2147483647],所以最多有30位二进制数代表数字大小。1L代表1的数据类型是long类型的,也就是长整型。“<<30”代表将二进制的1左移30位。原来1在第一位,左移30位后,1在第31位。此时代表的数字是2147483648,超出了长整型表示范围,所以需要减一。此时代表的数字是2147483647,代表最大的长整型数的大小和位数。
Iodigits 变量记录最大长整型数的十进制位数(9位)
Ioradix 变量记录最大长整型数的十进制数+1(10位,1000000000)
Iodigits,Ioradix均为静态变量。
(2)接收大整数
istream& operator>>(istream& s, ZZ& x) { long c; long cval; long sign; long ndigits; long acc; NTL_ZZRegister(a); if (!s) NTL_INPUT_ERROR(s, "bad ZZ input"); //若无输入,则直接报错 if (!iodigits) InitZZIO(); //一个长整型最大接收的十进制位数是9位,该数据存在 a = 0; // iodigits中。故先要将iodigits中数据进行初始化 SkipWhiteSpace(s); //跳过输入时候有效数据前的空格 c = s.peek(); //peek()探测指针,提前检测下一位,并不改变指针指向 if (c == '-') { sign = -1; s.get(); c = s.peek(); } else sign = 1; //确认该大整数的正负号 cval = CharToIntVal(c); //接受大整数的时候只接受十进制,数字在0-9之间才合法 if (cval < 0 || cval > 9) NTL_INPUT_ERROR(s, "bad ZZ input"); ndigits = 0; acc = 0; while (cval >= 0 && cval <= 9) { acc = acc*10 + cval; ndigits++; if (ndigits == iodigits) { //用于九位九位的存储大整数,大整数超过九位会采用截断存储处理方法 mul(a, a, ioradix); add(a, a, acc); ndigits = 0; acc = 0; } s.get(); //从输入流中提取已经存好的一位 c = s.peek(); //探测下一位 cval = CharToIntVal(c); //检测下一位输入是否合法 } if (ndigits != 0) { long mpy = 1; while (ndigits > 0) { mpy = mpy * 10; ndigits--; } mul(a, a, mpy); add(a, a, acc); } if (sign == -1) negate(a, a); x = a; return s; }
注释:
NTL_ZZRegister(a)这个函数相当于一种注册,生成了一个叫a的大整数类对象。和我们平时写的代码ZZ a;功能类似。
关于截断存储大整数的方法:NTL中定义了一个无符号长整型的数组(*cc),以十进制九位九位的将大整数进行拆分然后存入数组中。方便以后取用。
2.2.2输出大整数
(1)定义_ZZ_local_stack堆栈(采用结构体的方式定义)
struct _ZZ_local_stack { long top; Vec<long> data; _ZZ_local_stack() { top = -1; } //构造函数 long pop() { return data[top--]; } //弹栈,出栈 long empty() { return (top == -1); } //堆栈判空 void push(long x); //压栈 }; void _ZZ_local_stack::push(long x) { if (top+1 >= data.length()) //判断堆栈的是否已满。若满,则扩容 data.SetLength(max(32, long(1.414*data.length()))); top++; data[top] = x; }
注释:
堆栈的作用。因为大整数在运算的时候是从低位开始运算的,但是输出的时候是从高为开始逐渐输出的。所以低位做好运算之后适合进行压栈处理,不断地压栈,直到大整数运算结束再从堆栈中一一出栈,得到正确的输出结果。
(2)定义长整型输出函数
static void PrintDigits(ostream& s, long d, long justify) { NTL_TLS_LOCAL_INIT(Vec<char>, buf, (INIT_SIZE, iodigits)); //初始化数组buf long i = 0; while (d) { buf[i] = IntValToChar(d % 10); //将要输出的长整型数的每一位存到buf数组中 d = d / 10; i++; } if (justify) { long j = iodigits - i; while (j > 0) { s << "0"; j--; } } while (i > 0) { i--; s << buf[i]; //逐位输出长整型数据(只是*cc数组中的一个元素) } }
(3)对于输出流运算符的重载
ostream& operator<<(ostream& s, const ZZ& a) { ZZ b; _ZZ_local_stack S; long r; long k; if (!iodigits) InitZZIO(); b = a; k = sign(b); if (k == 0) { s << "0"; //判断输出符号,若数值为零,则直接输出“0” return s; } if (k < 0) { s << "-"; //判断输出符号,若为负号,则先输出“-” negate(b, b); } do { r = DivRem(b, b, ioradix); S.push(r); } while (!IsZero(b)); r = S.pop(); PrintDigits(s, r, 0); while (!S.empty()) { r = S.pop(); PrintDigits(s, r, 1); } return s; }
void XGCD(long& d, long& s, long& t, long a, long b) { long u, v, u0, v0, u1, v1, u2, v2, q, r; long aneg = 0, bneg = 0; if (a < 0) { if (a < -NTL_MAX_LONG) ResourceError("XGCD: integer overflow"); a = -a; aneg = 1; } if (b < 0) { if (b < -NTL_MAX_LONG) ResourceError("XGCD: integer overflow"); b = -b; bneg = 1; } u1=1; v1=0; u2=0; v2=1; u = a; v = b; while (v != 0) { q = u / v; r = u % v; u = v; v = r; //到此为止是广义欧几里得算法 u0 = u2; //从此开始是贝祖公式的求解 v0 = v2; u2 = u1 - q*u2; v2 = v1- q*v2; u1 = u0; v1 = v0; } if (aneg) u1 = -u1; if (bneg) v1 = -v1; d = u; s = u1; t = v1; //最后返回u = sa+tb }
贝祖公式的数学证明详见@回首,阑珊