Skip to content

Commit 056f8e4

Browse files
committed
binpow code
1 parent 013fd01 commit 056f8e4

4 files changed

Lines changed: 82 additions & 30 deletions

File tree

‎content/english/hpc/number-theory/euclid-extended.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ int inverse(int a) {
9191

9292
134.33
9393

94+
250 (there is probably a worse input if we change modulo or aternate between inputs to make )
95+
9496
Another application is the exact division modulo $2^k$.
9597

9698
**Exercise**. Try to adapt the technique for binary GCD.

‎content/english/hpc/number-theory/exponentiation.md‎

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,77 @@ weight: 2
44
draft: true
55
---
66

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:
833

934
```c++
10-
int inv(int x) {
11-
return binpow(x, mod - 2);
35+
const int M = 1e9 + 7; // modulo
36+
typedef unsigned long long u64;
37+
38+
u64 binpow(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+
}
1247
}
1348
```
1449
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$, то
1667

1768
$$
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
2270
$$
2371

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+
2478
We essentially group it like this:
2579

2680
$$
@@ -29,16 +83,25 @@ $$
2983

3084
This allows using only $O(\log n)$ operations (or, more specifically, at most $2 \cdot \log_2 n$ modular multiplications).
3185

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+
3293
```c++
33-
int binpow(int a, int n) {
34-
int res = 1;
94+
u64 binpow(u64 a, u64 n) {
95+
u64 r = 1;
96+
3597
while (n) {
3698
if (n & 1)
37-
res = res * a % mod;
38-
a = a * a % mod;
99+
r = res * a % M;
100+
a = a * a % M;
39101
n >>= 1;
40102
}
41-
return res;
103+
104+
return r;
42105
}
43106
```
44107
@@ -47,8 +110,8 @@ int binpow(int a, int n) {
47110
This helps if `n` or `mod` is a constant.
48111
49112
```c++
50-
int inverse(int _a) {
51-
long long a = _a, r = 1;
113+
u64 inverse(u64 a) {
114+
u64 r = 1;
52115
53116
#pragma GCC unroll(30)
54117
for (int l = 0; l < 30; l++) {
@@ -62,16 +125,3 @@ int inverse(int _a) {
62125
```
63126

64127
171.68
65-
66-
67-
Несколько причин:
68-
69-
Это выражение довольно легко вбивать (1e9+7).
70-
Простое число.
71-
Достаточно большое.
72-
int не переполняется при сложении.
73-
long long не переполняется при умножении.
74-
Кстати, 10^9 + 910
75-
9
76-
+9 обладает всеми теми же свойствами. Иногда используют и его.
77-

‎content/english/hpc/number-theory/modular.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Modular Arithmetic
3-
weight: -1
3+
weight: 1
44
---
55

66

‎content/russian/cs/algebra/binpow.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ authors:
66
weight: -10
77
---
88

9-
*Бинарное возведение в степень* — приём, позволяющий возводить любое число в $n$-ую степень за $O(\log n)$ умножений (вместо n умножений при обычном подходе).
9+
*Бинарное возведение в степень* — приём, позволяющий возводить любое число в $n$-ую степень за $O(\log n)$ умножений (вместо $n$ умножений при обычном подходе).
1010

1111
## Основная идея
1212

0 commit comments

Comments
 (0)