【Math】快速幂

百科名片

快速幂,二进制取幂(Binary Exponentiation,也称平方法),是一个在 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn) 的时间内计算 a n a^n an 的小技巧,而暴力的计算需要 Θ ( n ) \Theta(n) Θ(n) 的时间。而这个技巧也常常用在非计算的场景,因为它可以应用在任何具有结合律的运算中。其中显然的是它可以应用于模意义下取幂、矩阵幂等运算,我们接下来会讨论。

算法描述

计算 a a a n n n 次方表示将 n n n a a a 乘在一起: a n = a × a ⋯ × a ⏟ n  个 a a^{n} = \underbrace{a \times a \cdots \times a}_{n\text{ 个 a}} an=n  a a×a×a 。然而当 a , n a,n a,n 太大的时侯,这种方法就不太适用了。不过我们知道: a b + c = a b ⋅ a c ,    a 2 b = a b ⋅ a b = ( a b ) 2 a^{b+c} = a^b \cdot a^c,\,\,a^{2b} = a^b \cdot a^b = (a^b)^2 ab+c=abac,a2b=abab=(ab)2 。二进制取幂的想法是,我们将取幂的任务按照指数的 二进制表示 来分割成更小的任务。

首先我们将 n n n 表示为 2 进制,举一个例子:

3 13 = 3 ( 1101 ) 2 = 3 8 ⋅ 3 4 ⋅ 3 1 3^{13} = 3^{(1101)_2} = 3^8 \cdot 3^4 \cdot 3^1 313=3(1101)2=383431

因为 n n n ⌊ log ⁡ 2 n ⌋ + 1 \lfloor \log_2 n \rfloor + 1 log2n+1 个二进制位,因此当我们知道了 a 1 , a 2 , a 4 , a 8 , … , a 2 ⌊ log ⁡ 2 n ⌋ a^1, a^2, a^4, a^8, \dots, a^{2^{\lfloor \log_2 n \rfloor}} a1,a2,a4,a8,,a2log2n 后,我们只用计算 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn) 次乘法就可以计算出 a n a^n an

于是我们只需要知道一个快速的方法来计算上述 3 的 2 k 2^k 2k 次幂的序列。这个问题很简单,因为序列中(除第一个)任意一个元素就是其前一个元素的平方。举一个例子:

3 1 = 3 3 2 = ( 3 1 ) 2 = 3 2 = 9 3 4 = ( 3 2 ) 2 = 9 2 = 81 3 8 = ( 3 4 ) 2 = 8 1 2 = 6561 \begin{aligned} 3^1 &= 3 \\ 3^2 &= \left(3^1\right)^2 = 3^2 = 9 \\ 3^4 &= \left(3^2\right)^2 = 9^2 = 81 \\ 3^8 &= \left(3^4\right)^2 = 81^2 = 6561 \end{aligned} 31323438=3=(31)2=32=9=(32)2=92=81=(34)2=812=6561

因此为了计算 3 13 3^{13} 313 ,我们只需要将对应二进制位为 1 的整系数幂乘起来就行了:

3 13 = 6561 ⋅ 81 ⋅ 3 = 1594323 3^{13} = 6561 \cdot 81 \cdot 3 = 1594323 313=6561813=1594323

将上述过程说得形式化一些,如果把 n n n 写作二进制为 ( n t n t − 1 ⋯ n 1 n 0 ) 2 (n_tn_{t-1}\cdots n_1n_0)_2 (ntnt1n1n0)2 ,那么有:

n = n t 2 t + n t − 1 2 t − 1 + n t − 2 2 t − 2 + ⋯ + n 1 2 1 + n 0 2 0 n = n_t2^t + n_{t-1}2^{t-1} + n_{t-2}2^{t-2} + \cdots + n_12^1 + n_02^0 n=nt2t+nt12t1+nt22t2++n121+n020

其中 n i ∈ 0 , 1 n_i\in{0,1} ni0,1 。那么就有

a n = ( a n t 2 t + ⋯ + n 0 2 0 ) = a n 0 2 0 × a n 1 2 1 × ⋯ × a n t 2 t \begin{aligned} a^n & = (a^{n_t 2^t + \cdots + n_0 2^0})\\\\ & = a^{n_0 2^0} \times a^{n_1 2^1}\times \cdots \times a^{n_t2^t} \end{aligned} an=(ant2t++n020)=an020×an121××ant2t

根据上式我们发现,原问题被我们转化成了形式相同的子问题的乘积,并且我们可以在常数时间内从 2 i 2^i 2i 项推出 2 i + 1 2^{i+1} 2i+1 项。

这个算法的复杂度是 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn) 的,我们计算了 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn) 2 k 2^k 2k 次幂的数,然后花费 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn) 的时间选择二进制为 1 对应的幂来相乘。

代码实现

首先我们可以直接按照上述递归方法实现:

long long binpow(long long a, long long b) {
	if (b == 0) return 1;
	long long res = binpow(a, b / 2);
	if (b % 2) return res * res * a;
		else return res * res;
}

第二种实现方法是非递归式的。它在循环的过程中将二进制位为 1 时对应的幂累乘到答案中。尽管两者的理论复杂度是相同的,但第二种在实践过程中的速度是比第一种更快的,因为递归会花费一定的开销。

long long binpow(long long a, long long b) {
	long long res = 1;
	while (b > 0) {
		if (b & 1) res = res * a;
		a = a * a;
		b >>= 1;
	}
	return res;
}

模板: Luogu-P1226

应用

模意义下取幂

问题描述

计算 x n   m o d   m x^n\bmod m xnmodm

这是一个非常常见的应用,例如它可以用于计算模意义下的乘法逆元。

既然我们知道取模的运算不会干涉乘法运算,因此我们只需要在计算的过程中取模即可。

long long binpow(long long a, long long b, long long m) {
	a %= m;
	long long res = 1;
	while (b > 0) {
		if (b & 1) res = res * a % m;
		a = a * a % m;
		b >>= 1;
	}
	return res;
}

注意:根据费马小定理,如果 m m m 是一个质数,我们可以计算 x n   m o d   ( m − 1 ) x^{n\bmod (m-1)} xnmod(m1) 来加速算法过程。

计算斐波那契数

问题描述

计算斐波那契数列第 n n n F n F_n Fn

根据斐波那契数列的递推式 F n = F n − 1 + F n − 2 F_n = F_{n-1} + F_{n-2} Fn=Fn1+Fn2 ,我们可以构建一个 2 × 2 2\times 2 2×2 的矩阵来表示从 F i , F i + 1 F_i,F_{i+1} Fi,Fi+1 F i + 1 , F i + 2 F_{i+1},F_{i+2} Fi+1,Fi+2 的变换。于是在计算这个矩阵的 n n n 次幂的时侯,我们使用快速幂的思想,可以在 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn) 的时间内计算出结果。

多次置换

问题描述

给你一个长度为 n n n 的序列和一个置换,把这个序列置换 k k k 次。

简单地把这个置换取 k k k 次幂,然后把它应用到序列 n n n 上即可。时间复杂度是 O ( n log ⁡ k ) O(n \log k) O(nlogk) 的。

注意:给这个置换建图,然后在每一个环上分别做 k k k 次幂(事实上做一下 k k k 对环长取模的运算即可)可以取得更高效的算法,达到 O ( n ) O(n) O(n) 的复杂度。

加速几何中对点集的操作

三维空间中, n n n 个点 p i p_i pi ,要求将 m m m 个操作都应用于这些点。包含 3 种操作:

  1. 沿某个向量移动点的位置(Shift)。
  2. 按比例缩放这个点的坐标(Scale)。
  3. 绕某个坐标轴旋转(Rotate)。

还有一个特殊的操作,就是将一个操作序列重复 k k k 次(Loop),这个序列中也可能有 Loop 操作(Loop 操作可以嵌套)。现在要求你在低于 O ( n ⋅ l e n g t h ) O(n \cdot length) O(nlength) 的时间内将这些变换应用到这个 n n n 个点,其中 l e n g t h length length 表示把所有的 Loop 操作展开后的操作序列的长度。

让我们来观察一下这三种操作对坐标的影响:

  1. Shift 操作:将每一维的坐标分别加上一个常量;
  2. Scale 操作:把每一维坐标分别乘上一个常量;
  3. Rotate 操作:这个有点复杂,我们不打算深入探究,不过我们仍然可以使用一个线性组合来表示新的坐标。

可以看到,每一个变换可以被表示为对坐标的线性运算,因此,一个变换可以用一个 4 × 4 4\times 4 4×4 的矩阵来表示:

[ a 11 a 12 a 13 a 14 a 21 a 22 a 23 a 24 a 31 a 32 a 33 a 34 a 41 a 42 a 43 a 44 ] \begin{bmatrix} a_{11} & a_ {12} & a_ {13} & a_ {14} \\ a_{21} & a_ {22} & a_ {23} & a_ {24} \\ a_{31} & a_ {32} & a_ {33} & a_ {34} \\ a_{41} & a_ {42} & a_ {43} & a_ {44} \\ \end{bmatrix} a11a21a31a41a12a22a32a42a13a23a33a43a14a24a34a44

使用这个矩阵就可以将一个坐标(向量)进行变换,得到新的坐标(向量):

[ a 11 a 12 a 13 a 14 a 21 a 22 a 23 a 24 a 31 a 32 a 33 a 34 a 41 a 42 a 43 a 44 ] ⋅ [ x y z 1 ] = [ x ′ y ′ z ′ 1 ] \begin{bmatrix} a_{11} & a_ {12} & a_ {13} & a_ {14} \\ a_{21} & a_ {22} & a_ {23} & a_ {24} \\ a_{31} & a_ {32} & a_ {33} & a_ {34} \\ a_{41} & a_ {42} & a_ {43} & a_ {44} \\ \end{bmatrix}\cdot \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} x' \\ y' \\ z' \\ 1 \end{bmatrix} a11a21a31a41a12a22a32a42a13a23a33a43a14a24a34a44xyz1=xyz1

你可能会问,为什么一个三维坐标会多一个 1 出来?原因在于,如果没有这个多出来的 1,我们没法使用矩阵的线性变换来描述 Shift 操作。

接下来举一些简单的例子来说明我们的思路:

  1. Shift 操作:让 x x x 坐标方向的位移为 5 5 5 y y y 坐标的位移为 7 7 7 z z z 坐标的位移为 9 9 9

    [ 1 0 0 5 0 1 0 7 0 0 1 9 0 0 0 1 ] \begin{bmatrix} 1 & 0 & 0 & 5 \\ 0 & 1 & 0 & 7 \\ 0 & 0 & 1 & 9 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} 1000010000105791

  2. Scale 操作:把 x x x 坐标拉伸 10 倍, y , z y,z y,z 坐标拉伸 5 倍:

    [ 10 0 0 0 0 5 0 0 0 0 5 0 0 0 0 1 ] \begin{bmatrix} 10 & 0 & 0 & 0 \\ 0 & 5 & 0 & 0 \\ 0 & 0 & 5 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} 10000050000500001

  3. Rotate 操作:绕 x x x 轴旋转 θ \theta θ 弧度,遵循右手定则(逆时针方向)

    [ 1 0 0 0 0 cos ⁡ θ sin ⁡ θ 0 0 − sin ⁡ θ cos ⁡ θ 0 0 0 0 1 ] \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos \theta & \sin \theta & 0 \\ 0 & -\sin \theta & \cos \theta & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} 10000cosθsinθ00sinθcosθ00001

现在,每一种操作都被表示为了一个矩阵,变换序列可以用矩阵的乘积来表示,而一个 Loop 操作相当于取一个矩阵的 k 次幂。这样可以用 O ( m log ⁡ k ) O(m \log k) O(mlogk) 计算出整个变换序列最终形成的矩阵。最后将它应用到 n n n 个点上,总复杂度 O ( n + m log ⁡ k ) O(n + m \log k) O(n+mlogk)

定长路径计数

问题描述

给一个有向图(边权为 1),求任意两点 u , v u,v u,v 间从 u u u v v v ,长度为 k k k 的路径的条数。

我们把该图的邻接矩阵 M 取 k 次幂,那么 M i , j M_{i,j} Mi,j 就表示从 i i i j j j 长度为 k k k 的路径的数目。该算法的复杂度是 O ( n 3 log ⁡ k ) O(n^3 \log k) O(n3logk)

模意义下大整数乘法

计算 a × b   m o d   m ,    a , b ≤ m ≤ 1 0 18 a\times b\bmod m,\,\,a,b\le m\le 10^{18} a×bmodm,a,bm1018

与二进制取幂的思想一样,这次我们将其中的一个乘数表示为若干个 2 的整数次幂的和的形式。因为在对一个数做乘 2 并取模的运算的时侯,我们可以转化为加减操作防止溢出。这样仍可以在 O ( log ⁡ 2 m ) O (\log_2 m) O(log2m) 的时内解决问题。递归方法如下:

a ⋅ b = { 0 if  a = 0 2 ⋅ a 2 ⋅ b if  a > 0  and  a  even 2 ⋅ a − 1 2 ⋅ b + b if  a > 0  and  a  odd a \cdot b = \begin{cases} 0 &\text{if }a = 0 \\\\ 2 \cdot \frac{a}{2} \cdot b &\text{if }a > 0 \text{ and }a \text{ even} \\\\ 2 \cdot \frac{a-1}{2} \cdot b + b &\text{if }a > 0 \text{ and }a \text{ odd} \end{cases} ab=022ab22a1b+bif a=0if a>0 and a evenif a>0 and a odd

注意:你也可以利用双精度浮点数在常数时间内计算大整数乘法。因为 a × b   m o d   m = a × b − ⌊ a × b m ⌋ m a\times b\bmod m=a\times b-\left\lfloor\frac{a\times b}{m}\right\rfloor m a×bmodm=a×bma×bm 。由于 a , b < m a,b<m a,b<m ,因此 ⌊ a × b m ⌋ < m \left\lfloor\frac{a\times b}{m}\right\rfloor<m ma×b<m ,于是可以用双精度浮点数计算这个分式。作差的时侯直接自然溢出。因为两者的差是一定小于 m m m 的,我们只关心低位。这样再调整一下正负性就行了。

高精度快速幂

前置技能

请先学习 高精度

例题

【NOIP2003 普及组改编·麦森数】(原题在此
题目大意:从文件中输入 P(1000<P<3100000),计算 2 P − 1 2^P−1 2P1 的最后 100 位数字(用十进制高精度数表示),不足 100 位时高位补 0。

代码实现如下:

#include <bits/stdc++.h>
using namespace std;
int a[505], b[505], t[505], i, j;

int mult(int x[], int y[]) { // 高精度乘法
	memset(t, 0, sizeof(t));
	for (i = 1; i <= x[0]; i++) {
		for (j = 1; j <= y[0]; j++) {
			if (i + j - 1 > 100) continue;
			t[i + j - 1] += x[i] * y[j];
			t[i + j] += t[i + j - 1] / 10;
			t[i + j - 1] %= 10;
			t[0] = i + j;
		}
	}
	memcpy(b, t, sizeof(b));
}

void ksm(int p) { // 快速幂
	if (p == 1) {
		memcpy(b, a, sizeof(b));
		return;
	}
	ksm(p / 2);
	mult(b, b);
	if (p % 2 == 1) mult(b, a);
}

int main() {
	int p;
	scanf("%d", &p);
	a[0] = 1;
	a[1] = 2;
	b[0] = 1;
	b[1] = 1;
	ksm(p);
	for (i = 100; i >= 1; i--) {
		if (i == 1) {
			printf("%d\n", b[i] - 1);
		} else printf("%d", b[i]);
	}
	return 0;
}

相关练习

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值