Китайська теорема про остачі
Китайську теорему про остачі (далі в цій статті ми позначатимемо її як CRT) відкрив китайський математик Сунь Цзи.
- Чи задача дає систему конгруенцій і просить знайти за модулем добутку ?
- Чи модулі попарно взаємно прості (інакше потрібен окремий розбір степенів простих, описаний у статті)? (якщо модуль один і простий → Лінійне конгруентне рівняння)
- Чи ви хочете уникнути довгої арифметики, виконуючи обчислення над великим числом через його остачі за кількома малими модулями? (якщо так, для відновлення числа → Алгоритм Гарнера)
Формулювання
Нехай , де попарно взаємно прості. Окрім , нам також задано систему конгруенцій
де — деякі задані константи. Початкове формулювання CRT стверджує, що задана система конгруенцій завжди має рівно один розв'язок за модулем .
Наприклад, система конгруенцій
має розв'язок за модулем , бо , і . Кожен розв'язок ми можемо записати як для .
Наслідок
Наслідком CRT є те, що рівняння
еквівалентне системі рівнянь
(як і вище, вважаємо, що , а попарно взаємно прості).
Розв'язок для двох модулів
Розглянемо систему з двох рівнянь для взаємно простих :
Ми хочемо знайти розв'язок для . За допомогою розширеного алгоритму Евкліда ми можемо знайти коефіцієнти Безу такі, що
Насправді і — це просто обернені елементи за модулем для і за модулями і . Маємо , отже , і навпаки .
За допомогою цих двох коефіцієнтів ми можемо задати розв'язок:
Легко переконатися, що це справді розв'язок, обчисливши і .
Зауважимо, що китайська теорема про остачі також гарантує, що за модулем існує лише розв'язок. Це теж легко довести.
Припустимо, що ми маємо два різні розв'язки та . Оскільки і , то звідси випливає, що , а отже або, що еквівалентно, . Тож та насправді є одним і тим самим розв'язком.
Розв'язок для загального випадку
Індуктивний розв'язок
Оскільки взаємно просте з , ми можемо індуктивно багаторазово застосовувати розв'язок для двох модулів для будь-якої кількості модулів. Спочатку, використовуючи перші дві конгруенції, ми обчислюємо , потім можемо обчислити за допомогою конгруенцій та , і так далі.
Пряма побудова
Можлива пряма побудова, схожа на інтерполяцію Лагранжа.
Нехай — добуток усіх модулів, крім , а — обернені елементи за модулем . Тоді розв'язком системи конгруенцій є:
Ми можемо перевірити, що це справді розв'язок, обчисливши для всіх . Оскільки є кратним для , маємо
Реалізація
- C++
- Python
- TypeScript
- Go
struct Congruence {
long long a, m;
};
long long chinese_remainder_theorem(vector<Congruence> const& congruences) {
long long M = 1;
for (auto const& congruence : congruences) {
M *= congruence.m;
}
long long solution = 0;
for (auto const& congruence : congruences) {
long long a_i = congruence.a;
long long M_i = M / congruence.m;
long long N_i = mod_inv(M_i, congruence.m);
solution = (solution + a_i * M_i % M * N_i) % M;
}
return solution;
}
# Конгруенцію подаємо як пару (a, m): a ≡ a (mod m)
def chinese_remainder_theorem(congruences):
M = 1
for a_i, m_i in congruences:
M *= m_i
solution = 0
for a_i, m_i in congruences:
M_i = M // m_i
# У Python є вбудований обернений елемент за модулем: pow(x, -1, mod)
N_i = pow(M_i, -1, m_i)
# Цілі числа Python мають довільну точність, тому проміжного
# переповнення (як з __int128 у C++) тут не виникає
solution = (solution + a_i * M_i % M * N_i) % M
return solution
// Конгруенцію подаємо як пару [a, m]: a ≡ a (mod m).
// Використовуємо bigint, бо добуток a_i * M_i легко переповнює Number
type Congruence = [bigint, bigint];
// обернений елемент за модулем через розширений алгоритм Евкліда
function modInv(a: bigint, m: bigint): bigint {
let [old_r, r] = [a % m, m];
let [old_s, s] = [1n, 0n];
while (r !== 0n) {
const q = old_r / r;
[old_r, r] = [r, old_r - q * r];
[old_s, s] = [s, old_s - q * s];
}
return ((old_s % m) + m) % m;
}
function chineseRemainderTheorem(congruences: Congruence[]): bigint {
let M = 1n;
for (const [, m_i] of congruences) {
M *= m_i;
}
let solution = 0n;
for (const [a_i, m_i] of congruences) {
const M_i = M / m_i;
const N_i = modInv(M_i, m_i);
solution = (solution + ((a_i * M_i) % M) * N_i) % M;
}
return solution;
}
// Конгруенцію подаємо як пару {a, m}: a ≡ a (mod m).
// Використовуємо math/big, бо добуток a_i * M_i легко переповнює int64
import "math/big"
type Congruence struct {
a, m *big.Int
}
func chineseRemainderTheorem(congruences []Congruence) *big.Int {
M := big.NewInt(1)
for _, c := range congruences {
M.Mul(M, c.m)
}
solution := big.NewInt(0)
for _, c := range congruences {
Mi := new(big.Int).Div(M, c.m)
// ModInverse повертає M_i^{-1} mod m
Ni := new(big.Int).ModInverse(Mi, c.m)
term := new(big.Int).Mul(c.a, Mi) // a_i * M_i
term.Mul(term, Ni) // * N_i
solution.Add(solution, term)
solution.Mod(solution, M)
}
return solution
}
Розв'язок для не взаємно простих модулів
Як уже згадувалося, наведений вище алгоритм працює лише для взаємно простих модулів .
У випадку не взаємно простих модулів система конгруенцій має рівно один розв'язок за модулем або не має розв'язку взагалі.
Наприклад, у наведеній нижче системі перша конгруенція означає, що розв'язок непарний, а друга конгруенція означає, що розв'язок парний. Неможливо, щоб число було одночасно непарним і парним, тому розв'язку, очевидно, немає.
Визначити, чи має система розв'язок, доволі просто. А якщо він є, ми можемо застосувати початковий алгоритм для розв'язання трохи зміненої системи конгруенцій.
Одна конгруенція еквівалентна системі конгруенцій , де — це розклад на прості множники числа .
Скориставшись цим фактом, ми можемо перетворити систему конгруенцій на систему, в якій модулями є лише степені простих чисел. Наприклад, наведена вище система конгруенцій еквівалентна:
Оскільки спочатку деякі модулі мали спільні множники, ми отримаємо кілька конгруенцій з модулями, що ґрунтуються на тому самому простому числі, але, можливо, з різними степенями цього простого.
Можна помітити, що конгруенція з модулем, який є найвищим степенем простого, буде найсильнішою серед усіх конгруенцій, що ґрунтуються на тому самому простому числі. Вона або даватиме суперечність з якоюсь іншою конгруенцією, або вже випливатиме з неї всі інші конгруенції.
У нашому випадку перша конгруенція випливає , а тому суперечить другій конгруенції . Отже, ця система конгруенцій не має розв'язку.
Якщо суперечностей немає, то система рівнянь має розв'язок. Ми можемо знехтувати всіма конгруенціями, крім тих, у яких модулі є найвищими степенями простих. Тепер ці модулі взаємно прості, а тому ми можемо розв'язати таку систему за допомогою алгоритму, обговореного в попередніх розділах.
Наприклад, наведена нижче система має розв'язок за модулем .
Ця система конгруенцій еквівалентна системі конгруенцій:
Єдині конгруенції з тим самим простим модулем — це та . Перша вже випливає другу, тож ми можемо знехтувати другою і натомість розв'язати таку систему зі взаємно простими модулями:
Вона має розв'язок , і справді та .
Алгоритм Гарнера
Ще одним наслідком CRT є те, що ми можемо подавати великі числа за допомогою масиву невеликих цілих.
Замість того, щоб виконувати багато обчислень з дуже великими числами, що може бути дорого (уявіть собі ділення з 1000-значними числами), ви можете обрати кілька взаємно простих модулів, подати велике число як систему конгруенцій і виконувати всі операції над цією системою рівнянь. Будь-яке число , менше за , можна подати як масив , де .
Скориставшись наведеним вище алгоритмом, ви можете будь-коли знову відновити велике число, коли воно вам потрібне.
Як альтернатива, ви можете подати число у вигляді мішаної системи числення (mixed radix):
Алгоритм Гарнера, який обговорюється в окремій статті Алгоритм Гарнера, обчислює коефіцієнти . А з цими коефіцієнтами ви можете відновити повне число.