You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 056f8e4
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: content/english/hpc/number-theory/exponentiation.md
+78-28Lines changed: 78 additions & 28 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,23 +4,77 @@ weight: 2
4
4
draft: true
5
5
---
6
6
7
-
You can calculate $a^{p-2}$ in $O(\log p)$ time using binary exponentiation:
7
+
In modular arithmetic and computational algebra in general, you often need to raise a number to the $n$-th power — to do [modular division](../modular/#modular-division), perform [primality tests](../modular/#fermats-theorem), or compute some combinatorial values — and you usually want to spend fewer than $\Theta(n)$ operations calculating it.
8
+
9
+
*Binary exponentiation*, also known as *exponentiation by squaring*, is a method that allows for computation of the $n$-th power using $O(\log n)$ multiplications, relying on the following observation:
10
+
11
+
$$
12
+
\begin{aligned}
13
+
a^{2k} &= (a^k)^2
14
+
\\ a^{2k + 1} &= (a^k)^2 \cdot a
15
+
\end{aligned}
16
+
$$
17
+
18
+
To compute $a^n$, we can recursively compute $a^{\lfloor n / 2 \rfloor}$, square it, and then optionally multiply by $a$ if $n$ is odd, corresponding to the following recurrence:
19
+
20
+
$$
21
+
a^n = f(a, n) = \begin{cases}
22
+
1, && n = 0
23
+
\\ f(a, \frac{n}{2})^2, && 2 \mid n
24
+
\\ f(a, n - 1) \cdot a, && 2 \nmid n
25
+
\end{cases}
26
+
$$
27
+
28
+
Since $n$ is at least halved every two recursive transitions, the depth of this recurrence and the total number of multiplications will be at most $O(\log n)$.
29
+
30
+
### Implementation
31
+
32
+
Since we already have a recurrence, it is natural to implement the algorithm as a case matching recursive function:
8
33
9
34
```c++
10
-
intinv(int x) {
11
-
return binpow(x, mod - 2);
35
+
constint M = 1e9 + 7; // modulo
36
+
typedefunsignedlonglongu64;
37
+
38
+
u64binpow(u64 a, u64 n) {
39
+
if (n == 0)
40
+
return 1;
41
+
if (n % 2 == 1)
42
+
return binpow(a, n - 1) * a % M;
43
+
else {
44
+
u64 b = binpow(a, n / 2) % M;
45
+
return b * b % m;
46
+
}
12
47
}
13
48
```
14
49
15
-
To perform the Fermat test, we need to raise a number to power $n-1$, preferrably using less than $n-2$ modular multiplications. We can use the fact that multiplication is associative:
50
+
Since $m$ is a compile-time constant, the compiler can optimize the modulo by [replacing it with multiplication](/hpc/arithmetic/division/) (even if it is not a compile-time constant, it is still cheaper to compute the magic constants by hand once).
51
+
52
+
The execution path and hence the running time depends on the value of $n$. In our benchmark, we use $m = 10^9+7$ and $n = m - 2$ so that we compute the [multiplicative inverse](../modular/#modular-division) of $a$ modulo $m$. This modulo is commonly used in competitive programming to calculate checksums in combinatorial problems because it is prime (allowing inverse via binary exponentiation), sufficiently large, doesn't overflow `int` for addition, doesn't overflow `long long` for multiplication, and is easy to type as `1e9 + 7`.
53
+
54
+
```c++
55
+
u64 inverse(u64 a) {
56
+
return binpow(a, M - 2);
57
+
}
58
+
```
59
+
60
+
For this particular $n$, the recursive implementation takes around 400ns per call.
61
+
62
+
As recursion introduces some [overhead](/hpc/architecture/functions/)
63
+
64
+
Эта реализация рекурсивная, что работает долго. Попробуем «развернуть» рекурсию и получить итеративную.
65
+
66
+
Рассмотрим двоичное представление числа $n$. Результат $a^n$ можно представить как произведение $a$ в степенях каких-то степеней двоек. Например, если $n = 42 = 32 + 8 + 2$, то
16
67
17
68
$$
18
-
\begin{aligned}
19
-
a^{2k} &= (a^k)^2
20
-
\\ a^{2k + 1} &= (a^k)^2 \cdot a
21
-
\end{aligned}
69
+
a^{42} = a^{32+8+2} = a^{32} \cdot a^8 \cdot a^2
22
70
$$
23
71
72
+
Чтобы посчитать это произведение итеративно, пройдемся по всем битам числа $n$, поддерживая две переменные: непосредственно результат и текущее значение $a^{2^k}$, где $k$ это номер текущей итерации. На каждом шаге будем домножать $a^{2^k}$ на текущий результат, если $k$-тый бит числа $n$ единичный, и в любом случае возводить её в квадрат, получая $a^{2^k \cdot 2} = a^{2^{k+1}}$ для следующей итерации.
73
+
74
+
Стоит отметить, что во многих языках программирования бинарное возведение в степень уже реализовано. Но не в C++: функция `pow` из стандартной библиотеки реализована только для действительных чисел и использует приближенные методы, и поэтому не дает точных результатов для целочисленных аргументов.
75
+
76
+
77
+
24
78
We essentially group it like this:
25
79
26
80
$$
@@ -29,16 +83,25 @@ $$
29
83
30
84
This allows using only $O(\log n)$ operations (or, more specifically, at most $2 \cdot \log_2 n$ modular multiplications).
31
85
86
+
87
+
88
+
You can calculate $a^{p-2}$ in $O(\log p)$ time using binary exponentiation:
89
+
90
+
91
+
To perform the Fermat test, we need to raise a number to power $n-1$, preferrably using less than $n-2$ modular multiplications.
92
+
32
93
```c++
33
-
int binpow(int a, int n) {
34
-
int res = 1;
94
+
u64binpow(u64 a, u64 n) {
95
+
u64 r = 1;
96
+
35
97
while (n) {
36
98
if (n & 1)
37
-
res = res * a % mod;
38
-
a = a * a % mod;
99
+
r = res * a % M;
100
+
a = a * a % M;
39
101
n >>= 1;
40
102
}
41
-
return res;
103
+
104
+
return r;
42
105
}
43
106
```
44
107
@@ -47,8 +110,8 @@ int binpow(int a, int n) {
47
110
This helps if `n` or `mod` is a constant.
48
111
49
112
```c++
50
-
intinverse(int _a) {
51
-
long long a = _a, r = 1;
113
+
u64 inverse(u64 a) {
114
+
u64 r = 1;
52
115
53
116
#pragma GCC unroll(30)
54
117
for (int l = 0; l < 30; l++) {
@@ -62,16 +125,3 @@ int inverse(int _a) {
62
125
```
63
126
64
127
171.68
65
-
66
-
67
-
Несколько причин:
68
-
69
-
Это выражение довольно легко вбивать (1e9+7).
70
-
Простое число.
71
-
Достаточно большое.
72
-
int не переполняется при сложении.
73
-
long long не переполняется при умножении.
74
-
Кстати, 10^9 + 910
75
-
9
76
-
+9 обладает всеми теми же свойствами. Иногда используют и его.
Copy file name to clipboardExpand all lines: content/russian/cs/algebra/binpow.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,7 +6,7 @@ authors:
6
6
weight: -10
7
7
---
8
8
9
-
*Бинарное возведение в степень* — приём, позволяющий возводить любое число в $n$-ую степень за $O(\log n)$ умножений (вместо n умножений при обычном подходе).
9
+
*Бинарное возведение в степень* — приём, позволяющий возводить любое число в $n$-ую степень за $O(\log n)$ умножений (вместо $n$ умножений при обычном подходе).
0 commit comments