夜深人静写算法(十九)- 快速幂

一、模幂运算

【例题一】 已知三个正整数 a , b , c ( a , b , c < = 1 0 5 ) a, b, c(a , b, c<=10^5) a,b,c(a,b,c<=105),求:
f ( a , b , c ) = a b m o d    c f(a,b,c) = a^b \mod c f(a,b,c)=abmodc

  • 这里求 a a a b b b 次幂(次方)对 c c c 取余数的计算,被称为模幂运算,本文会针对以上的式子,根据数据范围提供完全不同的算法来进行讲解,确保正确性的前提下,以求达到时间和空间的平衡;
  • 高端的算法往往只需要采用最朴素的诠释方式。

1、朴素算法

1)枚举

  • 直接一个 b b b 次的循环,枚举对 a a a b b b 次乘法,最后进行取模运算,代码如下:
// 1.朴素枚举
int f(int a, int b, int c) {
    
    
	int ans = 1;
	while(b--) {
    
    
		ans = ans * a;
	}
	return ans % c;
}
  • 这段代码有没有问题呢?我们来尝试用一些数据测试一下:

f(3, 5, 7) = 5
f(3, 10, 7) = 4
f(3, 100, 7) = -2
f(3, 1000000000, 7) = …

  • 1、溢出问题:首先,在数据量比较大的时候,运算结果会出现负数,这是因为 int 的取值范围是 [ − 2 31 , 2 31 ) [-2^{31}, 2^{31}) [231,231),超过后就会溢出,结果就和预期的不符了;
  • 2、时间复杂度:其次,这个算法的时间复杂度是 O ( b ) O(b) O(b) 的,当 b b b 很大的时候,明显是无法满足时间要求的,尤其是写服务器代码的时候,时间复杂度尤为重要,10个、100个、1000个玩家出现这样的计算时,时间消耗都是成倍增长的,非常占用服务器 CPU 资源;

2)模乘

  • 模乘满足如下公式:
    a b m o d    c = ( a m o d    c ) ( b m o d    c ) m o d    c a b \mod c = (a \mod c) (b \mod c) \mod c abmodc=(amodc)(bmodc)modc
  • 证明:
  • a = r a + m a c a = r_a + m_ac a=ra+mac, b = r b + m b c b = r_b + m_bc b=rb+mbc, 其中 ( 0 < = r a , r b < c ) (0 <= r_a, r_b < c) (0<=ra,rb<c),将乘法代入原式,得到:
    a b m o d    c = ( r a + m a c ) ( r b + m b c ) m o d    c = ( r a r b + r a m b c + m a r b c + m a m b c 2 ) m o d    c = r a r b m o d    c = ( a m o d    c ) ( b m o d    c ) m o d    c \begin{aligned} ab \mod c &= (r_a + m_ac) (r_b + m_bc) \mod c\\ &= (r_ar_b + r_a m_bc + m_ar_bc + m_am_bc^2) \mod c \\ &= r_ar_b \mod c \\ &= (a \mod c) (b \mod c) \mod c \end{aligned} abmodc=(ra+mac)(rb+mbc)modc=(rarb+rambc+marbc+mambc2)modc=rarbmodc=(amodc)(bmodc)modc
  • 所以我们可以对朴素算法进行一些改进,改进如下:
// 2.模乘改进
int f(int a, int b, int c) {
    
    
	int ans = 1 % c;
	while(b--) {
    
    
		ans = ans * a % c;
	}
	return ans;
}
  • 利用模乘运算,可以先模再乘,每次运算完毕确保数字是小于模数的,这样确保数字不会太大而造成溢出,但是这里没有考虑一种情况,就是 a a a 有可能是负数;

3)负数考虑

  • 需要再进行一次改进,改进版如下:
// 3.负数考虑
int f(int a, int b, int c) {
    
    
	a = (a % c + c) % c;
	int ans = 1 % c;
	while(b--) {
    
    
		ans = ans * a % c;
	}
	return ans;
}
  • a % c a \% c a%c 是为了确保模完以后 a a a 的绝对值小于 c c c,再加上 c c c 是为了保证结果是正数,最后又模上 c c c 是为了确保 a a a 是正数的情况也能准确让结果落在 [ 0 , c ) [0,c) [0,c) 的范围内;
  • 这样改完还有哪些问题呢???
  • 1、溢出问题:溢出问题仍然存在,只要 c c c 的范围是 [ − 2 31 , 2 31 ) [-2^{31}, 2^{31}) [231,231) a n s ans ans a a a 的范围就在 [ 0 , c ) [0, c) [0,c) a n s ∗ a ans * a ansa 的范围就是 [ 0 , c 2 ) [0, c^2) [0,c2),最坏情况就是 [ 0 , 2 62 ) [0, 2^{62}) [0,262),还是超过了 i n t int int 的范围;
  • 2、时间复杂度:时间复杂度仍然是 O ( b ) O(b) O(b) 的,而且每次都有一个取模运算,常数时间变大了;
  • 为了改善溢出问题,我们可以在相乘的时候转成 l o n g long long l o n g long long 来避免,如下:

c++ 代码

// 4.转换64位整数
int f(int a, int b, int c) {
    
    
	a = (a % c + c) % c;
	int ans = 1 % c;
	while(b--) {
    
    
		ans = (long long)ans * (long long)a % c;
	}
	return ans;
}
  • 溢出问题暂且告一段落,接下来让我们考虑下如何改善时间复杂度的问题(也许有人会问,如果 模数 c c c 的范围是 l o n g long long l o n g long long 又该怎么办呢?这个问题我打算在本文的末尾进行讲解,不是今天的主要讨论内容);

二、循环节

【例题二】 已知三个正整数 a , b , c ( a , b < = 1 0 18 , c < = 1 0 5 ) a, b, c(a , b<=10^{18}, c <= 10^5) a,b,c(a,b<=1018,c<=105),求:
f ( a , b , c ) = a b m o d    c f(a,b,c) = a^b \mod c f(a,b,c)=abmodc

  • 虽然 a a a b b b 的数据量很大,但是 c c c 很小,根据上文提到的模乘的性质, a a a可以通过模 c c c 首先进行一次降数量级的操作,起码范围可以缩小到 [ 0 , c ) [0, c) [0,c),再来看上面这个函数,满足一个递推式:
    f ( a , i , c ) = a f ( a , i − 1 , c ) m o d    c f(a,i,c) = af(a,i-1,c) \mod c f(a,i,c)=af(a,i1,c)modc
  • f ( a , i − 1 , c ) f(a,i-1,c) f(a,i1,c) 的取值范围必定是 [ 0 , c ) [0, c) [0,c),所以当最多计算 c c c 次以后必定能够产生一个循环,举个例子:
    a = 10 , b = 181217 , c = 42 a = 10, b = 181217, c = 42 a=10,b=181217,c=42
b 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
1 0 b m o d    42 10^b \mod 42 10bmod42 1 10 16 34 4 40 22 10 16 34 4 40 22 10 16
  • 随着 b b b 的逐步递增,得到的余数是一个以 6 为一个循环的序列,如下:
    1 { 10 , 16 , 34 , 4 , 40 , 22 } ⏟ 6 { 10 , 16 , 34 , 4 , 40 , 22 } ⏟ 6 { 10 , 16 , 34 , 4 , 40 , 22 } ⏟ 6 . . . . . . 1\underbrace{\{10,16,34,4,40,22\}}_{\rm 6} \underbrace{\{10,16,34,4,40,22\}}_{\rm 6} \underbrace{\{10,16,34,4,40,22\}}_{\rm 6}... ... 16 { 10,16,34,4,40,22}6 { 10,16,34,4,40,22}6 { 10,16,34,4,40,22}......

算法实现方式

  • 1)枚举最多 c c c 次,将前 i i i 次的运算结果存储在 F [ i ] F[i] F[i] 并且用 F P o s [ i ] FPos[i] FPos[i] 来哈希每次取模得到的结果的第一次出现的位置,初始化 F P o s [ i ] FPos[i] FPos[i] 都为 − 1 -1 1
  • 2)每次枚举计算完 F [ i ] F[i] F[i] 后,通过哈希寻找它第一次出现的位置,如果是 − 1 -1 1,代表从来没出现过,更新 F P o s [ F [ i ] ] = i FPos[ F[i] ] = i FPos[F[i]]=i
  • 3)否则表示找到了和之前相同的值,循环节就是两次相同位置的差值,记为 K K K,后面无论 b b b 多大,都是每 K K K 次一个循环,可以通过 b b b K K K 取模算出最终的答案;
  • 算法时间复杂度为 O ( K ) O(K) O(K); (这个 O K OK OK 真是太有灵性了)
  • 基于 K K K 的不确定性,并且时间复杂度应该是算最坏情况下的,所以这个算法的实际时间复杂度是 O ( c ) O(c) O(c)

C++ 代码

#define MAXN 11000

int F[MAXN+1], FPos[MAXN+1];

int f(int a, int b, int c) {
    
    
	int pre;
	memset(FPos, -1, sizeof(FPos));
	F[0] = 1 % c;
	FPos[ F[0] ] = 0;
	for(int i = 1; i <= b; ++i) {
    
    
		F[i] = F[i-1] * a % c;
		int &pre =  FPos[ F[i] ];
		if( pre == -1) {
    
    
			pre = i;
		}else {
    
    
			int K = i - pre;
			int Index = ( b - pre ) % K + pre;
			return F[Index];
		}
	}
	return F[b];
}

三、二分快速幂

【例题三】 已知三个正整数 a , b , c ( a , b < = 1 0 18 , c < = 1 0 9 ) a, b, c(a , b<=10^{18}, c<=10^{9}) a,b,c(a,b<=1018,c<=109),求:
f ( a , b , c ) = a b m o d    c f(a,b,c) = a^b \mod c f(a,b,c)=abmodc

  • 这里 a a a b b b 的数据量没变,但是 c c c 的数据量一下就上来了,如果还是采用找循环节的算法,肯定是无法接受的;我们采用二分的思想,对原式进行分治法,有如下公式:
    a b m o d    c = { 1 m o d    c b 为 0 a ( a ( b − 1 ) / 2 ) 2 m o d    c b 为 奇 数 ( a b / 2 ) 2 m o d    c b 为 正 偶 数 a^b \mod c = \begin {cases} 1 \mod c & b 为 0 \\ a(a^{(b-1)/2})^2 \mod c & b为奇数\\ (a^{b/2})^2 \mod c & b为正偶数\\ \end{cases} abmodc=1modca(a(b1)/2)2modc(ab/2)2modcb0bb
  • 利用程序的递归思想,可以把函数描述成如下的形式:
    f ( a , b , c ) = { 1 m o d    c b 为 0 a f ( a , b − 1 2 , c ) 2 m o d    c b 为 奇 数 f ( a , b 2 , c ) 2 m o d    c b 为 正 偶 数 f(a,b,c) = \begin {cases} 1 \mod c & b 为 0 \\ af(a, \frac {b-1}{2},c)^2 \mod c & b为奇数\\ f(a, \frac {b}{2},c)^2 \mod c & b为正偶数\\ \end{cases} f(a,b,c)=1modcaf(a,2b1,c)2modcf(a,2b,c)2modcb0bb
  • 这个算法的时间复杂度为 O ( l o g b ) O(log_b) O(logb)

1、递归实现

c++ 代码实现

#define LL long long
LL f(LL a, LL b, LL c) {
    
    
	if (b == 0) {
    
    
		return 1 % c;
	}
	LL v = f(a * a % c, (b >> 1), c);
	if (b & 1) {
    
    
		v = v * a % c;
	}
	return v;
}
  • 代码实现非常简单,根据 c++ 整数除法是取下整的特性,偶数和奇数的除法可以统一处理,然后用位运算进行一部分优化;
  • 1)右移一位相当于 除2;
  • 2)位与 1 用于判断奇偶性;

2、二进制优化实现

  • 由于递归实现会有一些堆栈及函数调用的性能消耗,所以我们需要采用迭代的方式化解递归,来看一个例子:
    a ( 22 ) 10 = a ( 10110 ) 2 = a ( 10000 ) 2 + ( 100 ) 2 + ( 10 ) 2 = a 1 ∗ ( 10000 ) 2 + 0 ∗ ( 1000 ) 2 + 1 ∗ ( 100 ) 2 + 1 ∗ ( 10 ) 2 + 0 ∗ ( 0 ) 2 = a 1 ∗ ( 16 ) 10 + 0 ∗ ( 8 ) 10 + 1 ∗ ( 4 ) 10 + 1 ∗ ( 2 ) 10 + 0 ∗ ( 0 ) 10 \begin{aligned}a^{(22)_{10}} &= a^{(10110)_{2}} \\ &= a^{(10000)_2 + (100)_2 + (10)_2}\\ &= a^{1*(10000)_2 + 0*(1000)_2 + 1*(100)_2 + 1*(10)_2 + 0*(0)_2}\\ &= a^{1*(16)_{10} + 0*(8)_{10} + 1*(4)_{10} + 1*(2)_{10} + 0*(0)_{10}} \end {aligned} a(22)10=a(10110)2=a(10000)2+(100)2+(10)2=a1(10000)2+0(1000)2+1(100)2+1(10)2+0(0)2=a1(16)10+0(8)10+1(4)10+1(2)10+0(0)10
  • 我们发现只要依次求出 a 1 、 a 2 、 a 4 、 a 8 、 a 16 . . . a^1、a^2、a^4、a^8、a^{16} ... a1a2a4a8a16... ,然后将 指数 二进制分解中那一位是 1 1 1 的对应的次幂项都乘起来,就是原先的 a a a 的次幂;

c++ 代码实现

#define LL long long
LL f(LL a,LL b, LL c){
    
    
	LL ans = 1;
	while(b) {
    
    
		if(b & 1) ans = ans * a % c;     // 1)
		a = a * a % c;                   // 2)
		b >>= 1;                         // 3)
	}
	return ans;
}
  • 1)和 3)是配合着做的,检查 n n n 的第 i i i 位二进制低位否是1,如果是则乘上对应的 a a a 次幂: a n i ( 其 中 n i = 2 i ) a^{n_i} (其中 n_i = 2^i) ani(ni=2i)
  • 2)依次求出 a 1 、 a 2 、 a 4 、 a 8 、 a 16 . . . a^1、a^2、a^4、a^8、a^{16} ... a1a2a4a8a16...
  • 这个算法的时间复杂度为 O ( l o g b ) O(log_b) O(logb),相比递归算法减少了一些常数消耗;

四、指数降幂

【例题四】 已知三个正整数 a , b , c ( a < = 1 0 18 , b = 2 1 0 10 , c < = 1 0 9 ) a, b, c(a <=10^{18}, b =2^{10^{10}}, c<=10^{9}) a,b,c(a<=1018,b=21010,c<=109),其中 g c d ( a , c ) = 1 gcd(a,c)=1 gcd(a,c)=1, 求:
f ( a , b , c ) = a b m o d    c f(a,b,c) = a^b \mod c f(a,b,c)=abmodc

  • 这时候我们发现 b 的范围已经非常大了,如果采用简单的二分快速幂, O ( l o g b ) O(log_b) O(logb) 的时间复杂度,算下来计算次数也要达到 1 0 10 10^{10} 1010 次,需要有其它算法来解决这个问题;

1、欧拉定理降幂

  • 直接给出定理:对于正整数 a a a n n n,当 g c d ( a , n ) = 1 gcd(a,n)=1 gcd(a,n)=1 时, φ ( n ) 为 n \varphi(n) 为 n φ(n)n 的欧拉函数,则满足: a φ ( n ) ≡ 1 ( m o d    n ) a ^ {\varphi(n)} \equiv 1 (\mod n) aφ(n)1(modn)
  • 定理的证明在以下博客中有详细讲解:夜深人静写算法(十三)- RSA算法的加密与解密
  • b = k φ ( c ) + r b = k\varphi(c) + r b=kφ(c)+r,即 r r r b b b 模上 c c c 的欧拉函数的余数,根据欧拉定理,有:
    f ( a , b , c ) = a b m o d    c = a k φ ( c ) + r m o d    c = a k φ ( c ) a r m o d    c = a r m o d    c = a b m o d    φ ( c ) m o d    c \begin{aligned}f(a,b,c) &= a^b \mod c \\ &= a^{k\varphi(c) + r} \mod c\\ &= a^{k\varphi(c)}a^{r} \mod c\\ &= a^{r} \mod c\\ &= a^{b \mod \varphi(c) } \mod c\\ \end {aligned} f(a,b,c)=abmodc=akφ(c)+rmodc=akφ(c)armodc=armodc=abmodφ(c)modc
  • 于是,问题转化成了求 b m o d    φ ( c ) b \mod \varphi(c) bmodφ(c) 的问题,由于 b = 2 1 0 10 b =2^{10^{10}} b=21010,这里就转化成了求:
    f ( 2 , 1 0 10 , φ ( c ) ) = 2 1 0 10 m o d    φ ( c ) \begin{aligned}f(2,10^{10},\varphi(c)) &= 2^{10^{10}} \mod \varphi(c) \\ \end {aligned} f(2,1010,φ(c))=21010modφ(c)
  • 我们发现,这时候,又转化成了求二分快速幂的问题,只不过现在的 a = 2 , b = 1 0 10 , c = φ ( c ) a=2, b=10^{10}, c = \varphi(c) a=2b=1010,c=φ(c),利用 O ( l o g b ) O(log_b) O(logb) 时间复杂度的算法已经绰绰有余;
  • 这个算法的时间复杂度为 O ( l o g l o g b ) O(log_{log_b}) O(loglogb),但是对数据要求较高,要求模数和被模数互素(互质);

2、费马小定理降幂

  • 费马小定理是欧拉定理的特殊情况,这种情况下模数 p 为素数,则: a p − 1 ≡ 1 ( m o d    p ) a ^ {p-1} \equiv 1 (\mod p) ap11(modp)
  • 由于素数 p p p 的欧拉函数为 p − 1 p - 1 p1,所以欧拉定理成立,费马小定理必然成立;

【例题五】 已知三个正整数 a , b , c ( a < = 1 0 18 , b = 2 1 0 10 , c < = 1 0 9 ) a, b, c(a <=10^{18}, b =2^{10^{10}}, c<=10^{9}) a,b,c(a<=1018,b=21010,c<=109),其中 c c c 为素数, 求:
f ( a , b , c ) = a b m o d    c f(a,b,c) = a^b \mod c f(a,b,c)=abmodc

  • 问题比较明确, c c c 为素数,所以 a a a 只有两种情况,要么为 c c c 的倍数,要么和 c c c 互素;
  • 1)当 a a a c c c 的倍数时,答案恒为0;
  • 2)当 a a a c c c 互素时,利用费马小定理降幂;

3、扩展欧拉定理降幂

【例题六】 已知三个正整数 a , b , c ( a < = 1 0 18 , b = 2 1 0 10 , c < = 1 0 9 ) a, b, c(a <=10^{18}, b =2^{10^{10}}, c<=10^{9}) a,b,c(a<=1018,b=21010,c<=109), 求:
f ( a , b , c ) = a b m o d    c f(a,b,c) = a^b \mod c f(a,b,c)=abmodc

  • 扩展欧拉定理表示如下:
    a b m o d    c = { a b m o d    φ ( c ) m o d    c g c d ( a , c ) = 1 ( 1 ) a b m o d    c g c d ( a , c ) ≠ 1 , b < φ ( c ) ( 2 ) a b m o d    φ ( c ) + φ ( c ) m o d    c g c d ( a , c ) ≠ 1 , b > = φ ( c ) ( 3 ) a^{b} \mod c = \begin {cases} a^{b \mod \varphi(c)} \mod c & { gcd(a,c)=1} & (1)\\ a^{b} \mod c & { gcd(a,c) \neq 1 ,b < \varphi(c)} & (2)\\ a^{b \mod \varphi(c) + \varphi(c)} \mod c & { gcd(a,c) \neq 1 ,b >= \varphi(c)} & (3)\\ \end {cases} abmodc=abmodφ(c)modcabmodcabmodφ(c)+φ(c)modcgcd(a,c)=1gcd(a,c)=1b<φ(c)gcd(a,c)=1b>=φ(c)(1)(2)(3)
    (证明待补充)

  • (1) 直接根据欧拉定理得出;

  • (2) b 比较小,直接通过二分快速幂求即可;

  • (3) 当 b 非常大的时候,可以通过这一步对指数进行降幂;

  • 这个算法的时间复杂度也是 O ( l o g l o g b ) O(log_{log_b}) O(loglogb),同样的,如果 b b b 是一个次幂形式的数,降幂同样是采用 二分快速幂;

五、模数为 64 位整数

【例题七】 已知三个正整数 a , b , c ( a , b , c < = 1 0 18 ) a, b, c(a,b,c<=10^{18}) a,b,c(a,b,c<=1018), 求:
f ( a , b , c ) = a b m o d    c f(a,b,c) = a^b \mod c f(a,b,c)=abmodc

  • 和之前问题唯一不同的点在与: c c c 的范围不再是 32 32 32 位整数 i n t int int,所以无论是朴素算法、二分快速幂、指数降幂,都会面临一个问题:模乘运算的中间过程有很大概率会超过 64 64 64 位整数 (c++ 64位机器能够表示的最大整数);
  • 思考一下之前的二进制优化的快速幂,容易溢出的地方在于乘法,那么我们现在要解决的问题就变成了如何将两个 64 位整数相乘取模的中间过程不会超过 64 位整数的最大值;
  • 举个例子:
    22 ∗ 1981 m o d    181 = ( 16 + 4 + 2 ) ∗ 1981 m o d    181 = ( 1 ∗ 16 + 0 ∗ 8 + 1 ∗ 4 + 1 ∗ 2 + 0 ∗ 1 ) ∗ 1981 m o d    181 \begin{aligned} 22 * & 1981 \mod 181 \\ = (16+4+2) * & 1981 \mod 181 \\ = (1*16+0*8 + 1*4+1*2 + 0*1) * & 1981 \mod 181 \end{aligned} 22=(16+4+2)=(116+08+14+12+01)1981mod1811981mod1811981mod181
  • 我们发现,只要能够求出 1981 的 1倍、2倍、4倍、8倍、16倍,然后在 22 的对应二进制位去找,如果对应位是 1 的就加到结果中,最后这些累加的结果值模181就是最终的答案,和二进制优化二分快速幂的原理是一样的;

算法实现

  • 1)对于 a ∗ b m o d    c a*b \mod c abmodc,对 b b b 进行二进制分解;
  • 2) b b b 的对应二进制位为 b i b_i bi,且 b b b 最多 K K K 位,那么 a n s = ∑ i = 0 K − 1 b i ∗ a 2 i m o d    c ans = \sum_{i=0}^{K-1}b_i * a^{2i} \mod c ans=i=0K1bia2imodc
    c++ 代码实现
#define LL long long
//计算a*b%c
LL Product_Mod(LL a, LL b, LL c) {
    
    
	LL sum = 0;
	while(b) {
    
    
		if(b & 1) sum = (sum + a) % c;
		a = (a + a) % c;
		b >>= 1;
	}
	return sum;
}

  
//计算a^b%c
LL f(LL a, LL b, LL c) {
    
    
	LL ans = 1;
	while(b) {
    
    
		if(b & 1) sum = Product_Mod(sum, a, c);
		a = Product_Mod(a, a, c);
		b >>= 1;
	}
	return ans;
}

六、时间复杂度总结

  • 最后,对于各种 a b m o d    c a^b \mod c abmodc 的算法,总结一下各个算法的时间复杂度;
算法 时间复杂度 解析
朴素算法 O ( b ) O(b) O(b) 最常规的理解方式,适用于指数 b b b 比较小的问题
循环节算法 O ( c ) O(c) O(c) 适用于模数 c c c 比较小的问题
二分快速幂 - 递归 O ( l o g b ) O(log_b) O(logb) 分而治之的思想,适用于 b b b 比较大的情况
二分快速幂 - 二进制优化 O ( l o g b ) O(log_b) O(logb) 相对于递归来说,减少了递归函数调用带来的消耗
欧拉定理降幂 O ( l o g l o g b ) O(log_{log_b}) O(loglogb) 适用于模数和被模数互素,且指数为次幂形式能够最大化体现算法效益
费马小定理降幂 O ( l o g l o g b ) O(log_{log_b}) O(loglogb) 适用于模数为素数,且指数为次幂形式能够最大化体现算法效益
扩展欧拉定理降幂 O ( l o g l o g b ) O(log_{log_b}) O(loglogb) 适用于指数为次幂形式能够最大化体现算法效益

猜你喜欢

转载自blog.csdn.net/WhereIsHeroFrom/article/details/109698192