Обернений елемент за модулем
Означення
Обернений елемент за модулем для цілого числа — це таке ціле число , що конгруентне за деяким модулем . Формально: ми хочемо знайти таке ціле число , що
Ми також будемо позначати просто як .
Зауважимо, що обернений елемент за модулем існує не завжди. Наприклад, нехай , . Перебравши всі можливі значення за модулем , легко переконатися, що ми не можемо знайти , яке задовольняє наведене рівняння. Можна довести, що обернений елемент за модулем існує тоді й лише тоді, коли та взаємно прості (тобто ).
У цій статті ми наведемо два методи знаходження оберненого елемента за модулем у випадку, коли він існує, і один метод знаходження оберненого елемента для всіх чисел за лінійний час.
- Чи виконується умова існування оберненого, тобто (для довільного без цієї умови оберненого немає)?
- Чи потрібно ділити за модулем у задачі, де модуль не обов'язково простий? Тоді підходить розширений алгоритм Евкліда (формула через бінарне піднесення працює лише для простого ).
- Чи треба передобчислити обернені для всіх чисел за один прохід? Тоді використовуйте лінійний метод із цієї статті замість окремих викликів.
Знаходження оберненого елемента за модулем за допомогою розширеного алгоритму Евкліда
Розглянемо таке рівняння (з невідомими та ):
Це лінійне діофантове рівняння з двома змінними. Як показано у статті за посиланням, коли , рівняння має розв'язок, який можна знайти за допомогою розширеного алгоритму Евкліда. Зауважимо, що — це також умова існування оберненого елемента за модулем.
Тепер, якщо взяти обидві частини за модулем , ми можемо позбутися , і рівняння набуває вигляду:
Отже, оберненим елементом для є .
Реалізація виглядає так:
- C++
- Python
- TypeScript
- Go
int x, y;
int g = extended_euclidean(a, m, x, y);
if (g != 1) {
cout << "No solution!";
}
else {
x = (x % m + m) % m;
cout << x << endl;
}
# extended_euclidean повертає (g, x, y) таке, що a*x + m*y = g
g, x, y = extended_euclidean(a, m)
if g != 1:
print("Немає розв'язку!")
else:
# x може бути від'ємним, тому зводимо у діапазон [0, m)
x = (x % m + m) % m
print(x)
# Підказка: у Python є вбудований обернений елемент: pow(a, -1, m)
// для модульної арифметики з множенням використовуємо bigint
// extendedEuclidean повертає [g, x, y] таке, що a*x + m*y = g
const [g, x, y] = extendedEuclidean(a, m);
if (g !== 1n) {
console.log("Немає розв'язку!");
} else {
// x може бути від'ємним, тому зводимо у діапазон [0, m)
const inv = ((x % m) + m) % m;
console.log(inv.toString());
}
// extendedEuclidean повертає (g, x, y) таке, що a*x + m*y = g
g, x, _ := extendedEuclidean(a, m)
if g != 1 {
fmt.Println("Немає розв'язку!")
} else {
// x може бути від'ємним, тому зводимо у діапазон [0, m)
x = (x%m + m) % m
fmt.Println(x)
}
Зверніть увагу на те, як ми змінюємо x.
Отримане з розширеного алгоритму Евкліда значення x може бути від'ємним, тому x % m також може бути від'ємним, і нам спочатку доводиться додати m, щоб зробити його додатним.
Знаходження оберненого елемента за модулем за допомогою бінарного піднесення до степеня
Інший метод знаходження оберненого елемента за модулем — використати теорему Ейлера, яка стверджує, що наступна конгруенція виконується, якщо та взаємно прості:
— це функція Ейлера. Знову ж таки, зауважимо, що взаємна простота та була також умовою існування оберненого елемента за модулем.
Якщо — просте число, це спрощується до малої теореми Ферма:
Помножимо обидві частини наведених рівнянь на , і отримаємо:
- Для довільного (але взаємно простого) модуля :
- Для простого модуля :
З цих результатів ми можемо легко знайти обернений елемент за модулем за допомогою алгоритму бінарного піднесення до степеня, який працює за час .
Хоча цей метод простіший для розуміння, ніж метод, описаний у попередньому абзаці, у випадку, коли не є простим числом, нам потрібно обчислити функцію Ейлера , що передбачає факторизацію , яка може бути дуже складною. Якщо розклад на прості множники відомий, то складність цього методу становить .
Знаходження оберненого елемента за модулем для простих модулів за допомогою ділення з остачею
Нехай дано простий модуль (або ми можемо застосувати модуль, щоб зменшити число за один крок); згідно з діленням з остачею
де та , тоді
Зауважимо, що це міркування не виконується, якщо не є простим, оскільки існування у загальному випадку не означає існування . Щоб це побачити, спробуймо обчислити за модулем за наведеною формулою. Ми хотіли б отримати , оскільки . Однак , і ми маємо та , причому не є оборотним за модулем .
Проте якщо модуль простий, то всі з є оборотними за модулем , і ми можемо записати таку рекурсивну функцію для обчислення оберненого елемента за модулем для числа відносно
- C++
- Python
- TypeScript
- Go
int inv(int a) {
return a <= 1 ? a : m - (long long)(m/a) * inv(m % a) % m;
}
# m — простий модуль; обернений елемент для a у діапазоні [1, m)
def inv(a: int) -> int:
return a if a <= 1 else m - (m // a) * inv(m % a) % m
# Підказка: у Python той самий результат дає pow(a, -1, m)
// для модульної арифметики з множенням використовуємо bigint
// m — простий модуль; обернений елемент для a у діапазоні [1, m)
function inv(a: bigint): bigint {
return a <= 1n ? a : m - ((m / a) * inv(m % a)) % m;
}
// m — простий модуль; обернений елемент для a у діапазоні [1, m)
func inv(a int64) int64 {
if a <= 1 {
return a
}
return m - (m/a)*inv(m%a)%m
}
Точна часова складність цієї рекурсії невідома. Вона десь між та . Див. On the length of Pierce expansions. На практиці ця реалізація швидка, наприклад, для модуля вона завжди завершиться менш ніж за 50 ітерацій.
Обернений елемент для кожного числа за модулем
Застосувавши цю формулу, ми також можемо попередньо обчислити обернений елемент за модулем для кожного числа з діапазону за .
- C++
- Python
- TypeScript
- Go
inv[1] = 1;
for(int a = 2; a < m; ++a)
inv[a] = m - (long long)(m/a) * inv[m%a] % m;
# inv[a] — обернений елемент для a за простим модулем m, для всіх a з [1, m)
inv = [0] * m
inv[1] = 1
for a in range(2, m):
inv[a] = m - (m // a) * inv[m % a] % m
// для модульної арифметики з множенням використовуємо bigint
// inv[a] — обернений елемент для a за простим модулем m, для всіх a з [1, m)
const inv: bigint[] = new Array(Number(m)).fill(0n);
inv[1] = 1n;
for (let a = 2n; a < m; a++) {
inv[Number(a)] = m - ((m / a) * inv[Number(m % a)]) % m;
}
// inv[a] — обернений елемент для a за простим модулем m, для всіх a з [1, m)
inv := make([]int64, m)
inv[1] = 1
for a := int64(2); a < m; a++ {
inv[a] = m - (m/a)*inv[m%a]%m
}
Знаходження оберненого елемента за модулем для масиву чисел
Припустимо, нам дано масив, і ми хочемо знайти обернений елемент за модулем для всіх чисел у ньому (усі вони оборотні). Замість того щоб обчислювати обернений елемент для кожного числа, ми можемо розширити дріб префіксним добутком (не включаючи саме число) та суфіксним добутком (не включаючи саме число), і в результаті обчислити лише один обернений елемент.
У коді ми можемо просто побудувати масив префіксних добутків (не включаючи саме число, починаючи з нейтрального елемента), обчислити обернений елемент за модулем для добутку всіх чисел, а потім помножити його на префіксний та суфіксний добутки (не включаючи саме число). Суфіксний добуток обчислюється ітеруванням з кінця до початку.
- C++
- Python
- TypeScript
- Go
std::vector<int> invs(const std::vector<int> &a, int m) {
int n = a.size();
if (n == 0) return {};
std::vector<int> b(n);
int v = 1;
for (int i = 0; i != n; ++i) {
b[i] = v;
v = static_cast<long long>(v) * a[i] % m;
}
int x, y;
extended_euclidean(v, m, x, y);
x = (x % m + m) % m;
for (int i = n - 1; i >= 0; --i) {
b[i] = static_cast<long long>(x) * b[i] % m;
x = static_cast<long long>(x) * a[i] % m;
}
return b;
}
def invs(a: list[int], m: int) -> list[int]:
n = len(a)
if n == 0:
return []
b = [0] * n
# b[i] = префіксний добуток a[0..i-1]
v = 1
for i in range(n):
b[i] = v
v = v * a[i] % m
# обернений елемент для добутку всіх чисел (можна й x = pow(v, -1, m))
g, x, _ = extended_euclidean(v, m)
x = (x % m + m) % m
# ідемо з кінця, домножуючи на суфіксний добуток
for i in range(n - 1, -1, -1):
b[i] = x * b[i] % m
x = x * a[i] % m
return b
// для модульної арифметики з множенням використовуємо bigint
function invs(a: bigint[], m: bigint): bigint[] {
const n = a.length;
if (n === 0) return [];
const b: bigint[] = new Array(n).fill(0n);
// b[i] = префіксний добуток a[0..i-1]
let v = 1n;
for (let i = 0; i < n; i++) {
b[i] = v;
v = (v * a[i]) % m;
}
// обернений елемент для добутку всіх чисел
const [, gx] = extendedEuclidean(v, m);
let x = ((gx % m) + m) % m;
// ідемо з кінця, домножуючи на суфіксний добуток
for (let i = n - 1; i >= 0; i--) {
b[i] = (x * b[i]) % m;
x = (x * a[i]) % m;
}
return b;
}
func invs(a []int64, m int64) []int64 {
n := len(a)
if n == 0 {
return []int64{}
}
b := make([]int64, n)
// b[i] = префіксний добуток a[0..i-1]
var v int64 = 1
for i := 0; i < n; i++ {
b[i] = v
v = v * a[i] % m
}
// обернений елемент для добутку всіх чисел
_, x, _ := extendedEuclidean(v, m)
x = (x%m + m) % m
// ідемо з кінця, домножуючи на суфіксний добуток
for i := n - 1; i >= 0; i-- {
b[i] = x * b[i] % m
x = x * a[i] % m
}
return b
}
Задачі для практики
- UVa 11904 - One Unit Machine
- Hackerrank - Longest Increasing Subsequence Arrays
- Codeforces 300C - Beautiful Numbers
- Codeforces 622F - The Sum of the k-th Powers
- Codeforces 717A - Festival Organization
- Codeforces 896D - Nephren Runs a Cinema