NTL之旅
原文来自链接:https://shoup.net/ntl/doc/tour-ex1.html
第一章:大整数
我们给出的第一个例子会使用ZZ类,该类代表了"大整数":即带有符号的,任意长度的整数。下面给出的程序会读入两个大整数a和b,并且输出(a+1)*(b+1)。
实例代码
#include <NTL/ZZ.h>
using namespace std;
using namespace NTL;
int main(){
ZZ a,b,c;
cin>>a;
cin>>b;
c=(a+1)*(b+1);
cout<<c<<"\n"
}
运行结果如下:
上面的程序中声明了三个变量:a,b,c,三个变量的类型均为ZZ。 a和b的值从标准输入中读取,c为(a+1)*(b+1)。最终,c的值通过标准输出打印出来。
注意到,我们能够使用ZZ计算的数据范围远超int的计算范围,在int中,大部分标准的数学运算和赋值符号都能以非常自然的方式进行运算。C++编译器和NTL库中考虑到全部涉及到内存管理和临时对象的问题(bookkeeping them)。
我们得注意到默认来说,全部NTL组件都在namespace NTL中,使用 "using directive"的方式引用。
using namespace NTL;
在上面的例子中,人们可以直接访问到这些元素。
#include <NTL/ZZ.h>
using namespace std;
using namespace NTL;
int main()
{
ZZ acc, val;
acc = 0;
while (cin >> val)
acc += val*val;
cout << acc << "\n";
}
对于输入算子的传统,NTL的输入算子会对输入流设置’fail bits,如果输入丢失或者格式不对,条件是while loop能够检测到这个问题。
这里我们给出一个稍微简单一点的模指数运算,该末指数运算是计算a**e mod n,NTL已经给出了一个非常优秀的版本了。
ZZ PowerMod(const ZZ& a,const ZZ& e,const ZZ& n){
if(e == 0) return ZZ(1);
long k=NumBits(e);
for(long i=k-1;i>=0; i--){
res = (res*res)%n;
if (bit(e,i) ==1) res = (res*a)%n;
}
if(e<0) return InvMod(res,n);
else return res;
}
我们首先得注意到,作为一个替代品,我们可以按照如下的方式进行循环:
res=SqrtMod(res,n);
if(bit(e,i)==1) res=MulMod(res,a,n);
我们同样可以按照如下的方式写成:
res = SqrtMod(res,n);
if(bit(e,i)==1) MulMod(res,res,a,n);
上面的故事阐明了关于NTL编程界面的一个非常关键的点:NTL中的每一个函数,都存在一个程序性的版本(procedural version),该版本将结果存储在第一个参数中。使用程序性版本的原因在于该**程序性版本的效率非常高 **
原因:SqrtMod的函数形式(将运算结果通过赋值符号进行赋值的版本)中产生临时的ZZ变量,随后需要对其进行销毁。然而使用程序性版本(procedual version)不会产生任意的临时变量。 在性能非常重要的时候,使用程序性版本就是必然的。尽管一般来说过分纠结于性能不太可取,但是模指数运算对于运算速度的要求是典型代表。
我们得注意一下,该函数的功能性版本能够很自然的使用操作符进行命名。 举例来说,NTL为ZZ类提供了一个3-参数的mul过程,我们会使用*来给这个函数命名,而非使用mul进行命名。
当我们考虑临时变量相关的问题的时候,我们首先考虑第一个版本中的循环,对下面语句的执行:
res=(res*res)%n
就会导致两个临时对象的创建:第一个临时变量的创建是为了乘积创建,另外一个临时变量的创建是为了模元素进行创建,并在结束之后被拷贝到res中。当然了,编译器会自动产生代码以便在合适的时机清除这些临时变量和其他局部变量。 程序员无需担心这些临时变量引发的问题。
下面的例子更有意思,下面的程序会命令用户产生一个输入,随后使用一个简单的素性检测。 我们得注意到NTL中已经提供了更加高级的素性检测。
#include <NTL/ZZ.h>
using namespace std;
using namespace NTL;
long witness(const ZZ& n, const ZZ& x)
{
ZZ m, y, z;
long j, k;
if (x == 0) return 0;
// compute m, k such that n-1 = 2^k * m, m odd:
k = 1;
m = n / 2;
while (m % 2 == 0) {
k++;
m /= 2;
}
z = PowerMod(x, m, n); // z = x^m % n
if (z == 1) return 0;
j = 0;
do {
y = z;
z = (y*y) % n;
j++;
} while (j < k && z != 1);
return z != 1 || y != n - 1;
}
long PrimeTest(const ZZ& n, long t)
{
if (n <= 1) return 0;
// first, perform trial division by primes up to 2000
PrimeSeq s; // a class for quickly generating primes in sequence
long p;
p = s.next(); // first prime is always 2
while (p && p < 2000) {
if ((n % p) == 0) return (n == p);
p = s.next();
}
// second, perform t Miller-Rabin tests
ZZ x;
long i;
for (i = 0; i < t; i++) {
x = RandomBnd(n); // random number between 0 and n-1
if (witness(n, x))
return 0;
}
return 1;
}
int main()
{
ZZ n;
cout << "n: ";
cin >> n;
if (PrimeTest(n, 10))
cout << n << " is probably prime\n";
else
cout << n << " is composite\n";
}
我们得注意到在NTL中,有很多种方式来达到同样的目的,我们举例来说,在函数witness中计算m 和k,我们可以将其写成:
k=1;
m=n>>2;//m=n/2
while(!IsOdd(m)){
k++;
m>>=1;
}
下面的更加高效一些:
k=1;
while(bit(n,k)==0) k++;
m=n>>k;
当上面的过程真正发生之后,在NTL内部存在一个内置的过程,会达到同样的效果。
m=n-1;
k=MakeOdd(m);
构造函数、赋值和求逆
当我们声明一个ZZ中的新对象的时候,默认的构造函数会给其赋值为0。和我们刚看到的一样,存在拷贝构造函数,这种拷贝构造函数能够将ZZ中的一个值赋值给另一个。 我们得注意到这些拷贝(和大部分NTL中的拷贝一样),都是深拷贝。也就是说,除了指针之外,连实际的值也被拷贝过去了,当然,如果被分配的目标空间不足以盛放被拷贝的值,空间实际上就会被重新分配。
我们也可以给ZZ赋一个long类型的值:
ZZ x;
x=1;
得注意到我们是不能直接给ZZ赋值的:
ZZ x=1;
但是我们可以用下面的方式进行赋值:
ZZ x=ZZ(1);
ZZ y(1);
ZZ z{1};
使用的构造函数能够显示的从long中构造ZZ的方式。
换种方式,我们可以写成
ZZ x=conv<ZZ>(1);
这是NTL转化的一种形式,即使用ZZ作为conv模板的参数,用1初始化。对于非常大的常数,我们可以写成:
ZZ x=conv<ZZ>("9999999999999999999");
这些例子表明了在功能性格式(functional forms)中的写法。在程序性格式中也有类似的
ZZ x;
conv(x,1);
conv(x,"9999999999999")
功能
支持所有基础的数学运算,包括比较,数学,移位和逐bit的逻辑操作。人们可以将ZZ和long类型混用,表达方式非常自然。NTL并不明确支持隐式转化,但是对于基本操作,NTL会重载操作符或者函数,采用的方式看起来类似“提升逻辑”:如果一个输入是ZZ另一个是long(或者可以隐式转化为long,比如int),long类型的输入被非常高效的转化为ZZ。 此外,在可能的地方,实现都尽可能高效,通常会避免构造临时ZZ。
对于基本操作,还存在下面的基本数学操作存在程序性版本:
add,sub,negate,mul,sqr,div,rem,DivRem,
LeftShift,RightShift,
bit_and,bit_or,bit_xor
存在很多其他过程,下面是简略的描述:
GCD:计算两个整数的最大公约数
XGCD:扩展欧几里得算法
AddMod,SubMod,NegateMod,MulMod,SqrMod,InvMod,PowerMod:模运算基操(routine),包含求逆和指数
NumBits:二元表达式的常数
bit:提取某个bit
ZZFromBytes,BytesFromZZ–在八进制和ZZ之间来回转换
RandomBnd,RandomBits, RandomLen:随机数产生相关基操
GenPrime,ProbPrime:素数的产生和检测相关基操
power:非模操作的指数
SqrRoot:平方根中的整数部分
Jacobi,SqrRoorMod:Jacobi符号和模平方根
大部分函数都有纯的long版本,并且都有功能版和程序版(functional and procedual)
也存在其他函数,后续继续推出。