10.1 数论初步
数论被“数学王子”高斯誉为整个数学王国的皇后。在算法竞赛中,数论常常以各种面貌出现,但万变不离其宗,大部分数论题目并不涉及多少特殊的知识,但对数学思维和能力要求较高。本节介绍几个最为常用的算法,并通过例题展示一些常用的思维方式。
10.1.1 欧几里德算法和唯一分解定理
除法表达式。给出一个这样的除法表达式:X1 / X2 / X3 / …/ Xk,其中Xi是正整数。除法表达式应当按照从左到右的顺序求和,例如,表达式1/2/1/2的值为1/4。但可以在表达式中嵌入括号以改变计算顺序,例如,表达式(1/2)/(1/2)的值为1。
输入X1, X2, …, Xk,判断是否可以通过添加括号,使表达式的值为整数。K≤10000,Xi≤109。
【分析】
表达式的值一定可以写成A/B的形式:A是其中一些Xi的乘积,而B是其他数的乘积。不难发现,X2必须放在分母位置,那其他数呢?
幸运的是,其他数都可以在分子位置:
接下来的问题就变成了:判断E是否为整数。
第1种方法是利用前面介绍的高精度运算:k次乘法加一次除法。显然,这个方法是正确的,但却比较麻烦。
第2种方法是利用唯一分解定理,把X2写成若干素数相乘的形式:
然后依次判断每个
是否是X1X3X4…Xk的约数。这次不用高精度乘法了,只需把所有Xi中pi的指数加起来。如果结果比ai小,说明还会有pi约不掉,因此E不是整数。这种方法在第5章中已经用过,这里不再赘述。
第3种方法是直接约分:每次约掉Xi和X2的最大公约数gcd(Xi, X2),则当且仅当约分结束后X2=1时E为整数,程序如下:
int judge(int* X) {
X[2] /= gcd(X[2], X[1]);
for(int i = 3; i <= k; i++) X[2] /= gcd(X[i], X[2]);
return X[2] == 1;
}
整个算法的时间效率取决于这里的gcd算法。尽管依次试除也能得到正确的结果,但还有一个简单、高效,而且相当优美的算法——辗转相除法。它也许是最广为人知的数论算法。
辗转相除法的关键在于如下恒等式:gcd(a,b) = gcd(b, a mod b)。它和边界条件gcd(a, 0)=a一起构成了下面的程序:
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a%b);
}
这个算法称为欧几里德算法(Euclid algorithm)。既然是递归,那么免不了问一句:会栈溢出吗?答案是不会。可以证明,gcd函数的递归层数不超过4.785lgN + 1.6723,其中N=max{a,b}。值得一提的是,让gcd递归层数最多的是gcd(Fn,Fn-1),其中Fn是后文要介绍的Fibonacci数。
利用gcd还可以求出两个整数a和b的最小公倍数lcm(a,b)。这个结论很容易由唯一分解定理得到。设
由此不难验证gcd(a,b)*lcm(a,b)=a*b。不过即使有了公式也不要大意。如果把lcm写成a * b/gcd(a,b),可能会因此丢掉不少分数——a*b可能会溢出!正确的写法是先除后乘,即a/gcd(a,b) * b。这样一来,只要题面上保证最终结果在int范围之内,这个函数就不会出错。但前一份代码却不是这样:即使最终答案在int范围之内,也有可能中间过程越界。注意这样的细节,毕竟算法竞赛不是数学竞赛。
10.1.2 Eratosthenes筛法
无平方因子的数。给出正整数n和m,区间[n, m]内的“无平方因子”的数有多少个?整数p无平方因子,当且仅当不存在k>1,使得p是k2的倍数。1≤n≤m≤1012,m-n≤107。
【分析】
对于这样的限制,直接枚举判断会超时:需要判断107个整数,而每个整数还需要花费一定的时间判断是否没有平方因子。怎么办呢?在介绍具体算法之前,需要学会用Eratosthenes筛法构造1~n的素数表。
筛法的思想特别简单:对于不超过n的每个非负整数p,删除2p, 3p, 4p,…,当处理完所有数之后,还没有被删除的就是素数。如果用vis[i]表示i已经被删除,筛法的代码可以写成:
memset(vis, 0, sizeof(vis));
for(int i = 2; i <= n; i++)
for(int j = i*2; j <= n; j+=i) vis[j] = 1;
尽管可以继续改进,但这份代码已经相当高效了。为什么呢?给定外层循环变量i,内层循环的次数是
。这样,循环的总次数小于
。这个结论来源于欧拉在1734年得到的结果:
,其中欧拉常数γ≈0.577218。这样低的时间复杂度允许在很短的时间内得到106以内的所有素数。
下面来改进这份代码。首先,在“对于不超过n的每个非负整数p”中,p可以限定为素数——只需在第二重循环前加一个判断if(!vis[i])即可。另外,内层循环也不必从i*2开始——它已经在i=2时被筛掉了。改进后的代码如下:
int m = sqrt(n+0.5);
memset(vis, 0, sizeof(vis));
for(int i = 2; i <= m; i++) if(!vis[i])
for(int j = i*i; j <= n; j+=i) vis[j] = 1;
这里有一个有意思的问题:给定的n,c的值是多少呢?换句话说,不超过n的正整数中,有多少个是素数呢?
素数定理:
。
其中,π(x)表示不超过x的素数的个数。上述定理的直观含义是:它和x/lnx比较接近——对于算法入门来说,这已足够。表10-1给出了一些值来加深读者的印象。
表10-1 素数定理的直观验证

最后回到原题:如何求出区间内无平方因子的数?方法和筛素数是类似的:对于不超过
的所有素数p,筛掉区间[n, m]内p2的所有倍数。
10.1.3 扩展欧几里德算法
直线上的点。求直线ax+by+c=0上有多少个整点(x,y)满足x∈[x1, x2], y∈[y1, y2]。
【分析】
在解决这个问题之前,首先学习扩展欧几里德算法——找出一对整数(x,y),使得ax+by= gcd(a,b)。注意,这里的x和y不一定是正数,也可能是负数或者0。例如,gcd(6,15)=3,6*3-15*1=3,其中x=3,y=-1。这个方程还有其他解,如x=-2,y=1。
下面是扩展欧几里德算法的程序:
void gcd(int a, int b, int& d, int& x, int& y) {
if(!b){ d = a; x = 1; y = 0; }
else{ gcd(b, a%b, d, y, x); y -= x*(a/b); }
}
用数学归纳法并不难证明算法的正确性,此处略去。注意在递归调用时,x和y的顺序变了,而边界也是不难得出的:gcd(a,0)=1*a-0*0=a。这样,唯一需要记忆的是y-=x*(a/b),哪怕暂时不懂得其中的原因也不要紧。
上面求出了ax+by=gcd(a,b)的一组解(x1,y1),那么其他解呢?任取另外一组解(x2,y2),则ax1+by1=ax2+by2(它们都等于gcd(a,b)),变形得a(x1-x2)=b(y2-y1)。假设gcd(a,b)=g,方程左右两边同时除以g(1),得a'(x1-x2)=b' (y2-y1),其中a'=a/g,b'=b/g。注意,此时a'和b'互素,因此x1-x2一定是b'的整数倍。设它为kb',计算得y2-y1=ka'。注意,上面的推导过程并没有用到“ax+by的右边是什么”,因此得出如下结论。
提示10-1:设a, b, c为任意整数。若方程ax+by=c的一组整数解为(x0,y0),则它的任意整数解都可以写成(x0+kb', y0-ka'),其中a'=a/gcd(a,b),b'=b/gcd(a,b),k取任意整数。
有了这个结论,移项得ax+by=-c,然后求出一组解即可。例如:
例1:6x+15y=9。根据欧几里德算法,已经得到了6×(-2)+15×1=3,两边同时乘以3得6×(-6)+15×3=9,即x=-6,y=3时6x+15y=9。
例2:6x+15=8,两边除以3得2x+5=8/3。左边是整数,右边不是整数,显然无解。综合起来,有下面的结论。
提示10-2:设a, b, c为任意整数,g=gcd(a,b),方程ax+by=g的一组解是(x0,y0),则当c是g的倍数时ax+by=c的一组解是(x0c/g, y0c/g);当c不是g的倍数时无整数解。
这样,即完整地解决了本问题。顺便说一句,本题的名称为什么叫“直线上的点”呢?这是因为在平面坐标系下,ax+by+c=0是一条直线的方程。
10.1.4 同余与模算术
你需要花多少时间做下面这道题目呢?
123456789*987654321=( )
A.121932631112635266
B.121932631112635267
C.121932631112635268
D.121932631112635269
既然是选择题,不必费力把答案完整地计算出来——4个选项的个位数都不相同,因此只需要计算出答案的最后一位即可。不难得出,它等于1*9=9。把刚才的解题过程抽象出来就是下面的式子:
123456789*987654321 mod10=((123456789 mod10)*(987654321 mod10)) mod10
其中a mod b表示a除以b的余数,C语言表达式是a % b。在本章中,b一定是正整数,尽管b < 0时表达式a % b也是合法的(但b=0时会出现除零错)。
不难得到下面的公式:
注意在减法中,由于a mod n可能小于b mod n,需要在结果加上n,而在乘法中,需要注意a mod n和b mod n相乘是否会溢出。例如,当n=109时,ab mod n一定在int范围内,但a mod n和b mod n的乘积可能会超过int。需要用long long保存中间结果,例如:
int mul_mod(int a, int b, int n) {
a %= n; b %= n;
return (int)((long long)a * b % n);
}
当然,如果n本身超过int但又在long long范围内,上述方法就不适用了。在这种情况下,建议初学者使用高精度乘法——尽管有办法可以避免,但技巧性很强,不推荐初学者学习。
大整数取模。输入正整数n和m,输出n mod m的值。n≤10100,m≤109。
【分析】
首先,把大整数写成“自左向右”的形式:1234=((1*10+2)*10+3)*10+4,然后用前面的公式,每步取模,例如:
scanf("%s%d", n, &m);
int len = strlen(n);
int ans = 0;
for(int i = 0; i < len; i++)
ans = (int)(((long long)ans*10 + n[i] - '0') % m);
printf("%d\n",ans);
当然,也可以把ans声明成long long类型的,然后在输出时临时转换为int,但要注意乘法溢出的问题。
幂取模。输入正整数a、 n和m,输出an mod m的值。a, n, m≤109。
【分析】
很容易写出下面的代码:
int pow_mod(int a, int n, int m) {
int ans = 1;
for(int i = 0; i < n; i++) ans = (int)((long long)ans * n % m);
}
这个函数的时间复杂度为O(n),当n很大时速度很不理想。有没有办法算得更快呢?可以利用分治法:
int pow_mod(int a, int n, int m) {
if(n == 0) return 1;
int x = pow_mod(a, n/2, m);
long long ans = (long long)x * x % m;
if (n%2 == 1) ans = ans * a % m;
return (int)ans;
}
例如,a29=(a14)2*a,而a14=(a7)2,a7=(a3)2*a,a3=a2*a,一共只做了7次乘法。不知读者有没有发现,上述递归方式和二分查找很类似——每次规模近似减小一半。因此,时间复杂度为O(logn),比O(n)好了很多。
模线性方程组。输入正整数a, b, n,解方程ax≡b(mod n)。a, b, n≤109。
【分析】
本题中出现了一个新记号:同余。a≡b(mod n)的含义是“a和b关于模n同余”,即a mod n = b mod n。不难得出,a≡b(mod n)的充要条件是:a-b是n的整数倍。
提示10-3:a≡b(mod n)的含义是“a和b除以n的余数相同”,其充要条件是“a-b是n的整数倍”。
这样,原来的方程就可以理解成:ax-b是n的正整数倍。设这个“倍数”为y,则ax-b=ny,移项得ax-ny=b,这恰好就是10.1.3节介绍的不定方程(a, n, b是已知量,x和y是未知数)!接下来的步骤不再介绍。唯一需要说明的是,如果x是方程的解,满足x≡y(mod n)的其他整数y也是方程的解。因此,当谈到同余方程的一个解时,其实指的是一个同余等价类。
尽管算法已无须继续讨论,有一个特殊情况需要引起读者重视。b=1时,ax≡1(mod n)的解称为a关于模n的逆(inverse),它类似于实数运算中“倒数”的概念。什么时候a的逆存在呢?根据上面的讨论,方程ax-ny=1要有解。这样,1必须是gcd(a,n)的倍数,因此a和n必须互素(即gcd(a,n)=1)。在满足这个条件的前提下,ax≡1(mod n)只有唯一解。注意,同余方程的解是指一个等价类。
提示10-4:方程ax≡1(mod n)的解称为a关于模n的逆。当gcd(a,n)=1时,该方程有唯一解;否则,该方程无解。
10.1.5 应用举例
例题10-1 巨大的斐波那契数!(Colossal Fibonacci Numbers!, UVa11582)
输入两个非负整数a、b和正整数n(0≤a,b<264,1≤n≤1000),你的任务是计算f(ab)除以n的余数。其中f(0)=f(1)=1,且对于所有非负整数i,f(i+2)=f(i+1)+f(i)。
【分析】
所有计算都是对n取模的,不妨设F(i)=f(i) mod n。不难发现,当二元组(F(i), F(i+1))出现重复时,整个序列就开始重复。例如,n=3,序列F(i)的前10项为1,1,2,0,2,2,1,0,1,1,第9、10项和前两项完全一样。根据递推公式,第11项会等于第3项,第12项等于第4项……
多久会出现重复呢?因为余数最多n种,所以最多n2项就会出现重复。设周期为M,则只需计算出F(0)~F(n2),然后算出F(ab)等于其中的哪一项即可。
例题10-2 不爽的裁判(Disgruntled Judge, NWERC 2008, UVa12169)
有个裁判出的题太难,总是没人做,所以他很不爽。有一次他终于忍不住了,心想:“反正我的题没人做,我干嘛要费那么多心思出题?不如就输入一个随机数,输出一个随机数吧。”
于是他找了3个整数x1、a和b,然后按照递推公式xi=(axi-1+b) mod 10001计算出了一个长度为2T的数列,其中T是测试数据的组数。然后,他把T和x1, x3,…, x2T-1写到输入文件中,x2, x4,…, x2T写到了输出文件中。
你的任务就是解决这个疯狂的题目:输入T, x1, x3,…, x2T-1,输出x2, x4,…, x2T。输入保证T≤100,且输入的所有x值为0~10000的整数。如果有多种可能的输出,任意输出一个即可。
【分析】
如果知道了a,就可以计算出x2,进而根据x3=(ax2+b) mod 10001算出b。有了x1、a和b,就可以在O(T)时间内计算出整个序列了。如果在计算过程中发现和输入矛盾,则这个a是非法的。由于a是0~10000的整数(因为递推公式对10001取模),即使枚举所有的a,时间效率也足够高。
例题10-3 选择与除法(Choose and Divide, UVa10375)
已知C(m,n) = m!/(n!(m-n)!),输入整数p, q, r, s(p≥q,r≥s,p,q,r,s≤10000),计算C(p,q)/C(r,s)。输出保证不超过108,保留5位小数。
【分析】
本题正是唯一分解定理的用武之地。组合数C(m,n)的性质将在10.2.1节中介绍,本题只需要用到它的定义。
首先,求出10000以内的所有素数primes,然后用数组e表示当前结果的唯一分解式中各个素数的指数。例如,e={1,0,2,0,0,0,…}表示21*52=50。主程序如下:
while(cin >> p >> q >> r >> s) {
memset(e, 0, sizeof(e));
add_factorial(p, 1);
add_factorial(q, -1);
add_factorial(p-q, -1);
add_factorial(r, -1);
add_factorial(s, 1);
add_factorial(r-s, 1);
double ans = 1;
for(int i = 0; i < primes.size(); i++)
ans *= pow(primes[i], e[i]);
printf("%.5lf\n", ans);
}
其中add_factorial(n,d)表示把结果乘以(n!)d,它的实现如下:
//乘以或除以n. d=0表示乘,d=-1表示除
void add_integer(int n, int d) {
for(int i = 0; i < primes.size(); i++) {
while(n % primes[i] == 0) {
n /= primes[i];
e[i] += d;
}
if(n == 1) break; //提前终止循环,节约时间
}
}
void add_factorial(int n, int d) {
for(int i = 1; i <= n; i++)
add_integer(i, d);
}
例题10-4 最小公倍数的最小和(Minimum Sum LCM, UVa10791)
输入整数n(1≤n<231),求至少两个正整数,使得它们的最小公倍数为n,且这些整数的和最小。输出最小的和。
【分析】
本题再次用到了唯一分解定理。设唯一分解式n=a1p1*a2p2…,不难发现每个aipi作为一个单独的整数时最优。
如果就这样匆匆编写程序,可能会掉入陷阱。本题有好几个特殊情况要处理:n=1时答案为1+1=2;n只有一种因子时需要加个1,还要注意n=231-1时不要溢出。
例题10-5 GCD等于XOR(GCD XOR, ACM/ICPC Dhaka 2013, UVa12716)
输入整数n(1≤n≤30000000),有多少对整数(a,b)满足:1≤b≤a≤n,且gcd(a,b)=a XOR b。例如n=7时,有4对:(3,2), (5,4), (6,4), (7,6)。
【分析】
本题看上去很难找到简洁的数学公式,因为gcd和xor看上去似乎毫不相干。不过xor的好处是:a xor b = c,则a xor c = b,所以可以枚举a和c,然后算出b=a xor c,最后验证一下是否有gcd(a,b)=c。时间复杂度如何?因为c是a的约数,所以和素数筛法类似,时间复杂度为n/1+n/2+…+n/n=O(nlogn)。再加上gcd的时间复杂度为O(logn),所以总的时间复杂度为O(n(logn)2)。
我们还可以做得更好。上述程序写出来之后,可以打印一些满足gcd(a,b)=a xor b=c的三元组(a,b,c),然后很容易发现一个现象:c=a-b。
证明如下:不难发现a-b≤a xor b,且a-b≥c。假设存在c使得a-b>c,则c<a-b≤a xor b,与c=a xor b矛盾。
有了这个结论,还是沿用上述算法,枚举a和c,计算b=a-c,则gcd(a,b)=gcd(a,a-c)=c,因此只需验证是否有c = a xor b,时间复杂度降为了O(nlogn)。