Алгоритм Гарнера
Наслідком китайської теореми про остачі є те, що ми можемо представляти великі числа за допомогою масиву невеликих цілих чисел. Наприклад, нехай — добуток перших простих чисел. Тоді має близько цифр.
Будь-яке число , менше за , можна представити як масив , де . Але щоб це зробити, нам, очевидно, потрібно вміти відновлювати число з його представлення. Один зі способів розглянуто у статті про китайську теорему про остачі.
У цій статті ми розглянемо альтернативу — алгоритм Гарнера, який також можна використати для цієї мети.
- Чи задача зводиться до відновлення великого числа з його остач за набором попарно взаємно простих модулів? (якщо ні → Китайська теорема про остачі)
- Чи модулі фіксовані наперед, тож можна один раз передобчислити обернені елементи за час ?
- Чи результат не вміщується у вбудовані типи, тобто фінальну відповідь усе одно доведеться збирати довгою арифметикою?
Представлення в мішаній системі числення
Ми можемо представити число у представленні в мішаній системі числення:
Представлення в мішаній системі числення — це позиційна система числення, яка є узагальненням типових систем числення, як-от двійкова чи десяткова. Наприклад, десяткова система числення — це позиційна система числення з основою (radix) 10. Кожне число представляється рядком цифр від до . Наприклад, рядок представляє число . У загальному випадку рядок цифр представляє число у позиційній системі числення з основою .
У мішаній системі числення ми більше не маємо однієї основи. Основа змінюється від позиції до позиції.
Алгоритм Гарнера
Алгоритм Гарнера обчислює цифри . Зауважимо, що ці цифри відносно невеликі. Цифра — це ціле число між та .
Нехай позначає обернений елемент до за модулем
який можна знайти за допомогою алгоритму, описаного у статті Обернений елемент за модулем.
Підставивши з представлення в мішаній системі числення у перше конгруентне рівняння, отримуємо
Підстановка у друге рівняння дає
що можна переписати, віднявши та поділивши на , у вигляді
Аналогічно отримуємо, що
Тепер ми чітко бачимо закономірність, яка з'являється, і яку можна виразити таким кодом:
- C++
- Python
- TypeScript
- Go
for (int i = 0; i < k; ++i) {
x[i] = a[i];
for (int j = 0; j < i; ++j) {
x[i] = r[j][i] * (x[i] - x[j]);
x[i] = x[i] % p[i];
if (x[i] < 0)
x[i] += p[i];
}
}
for i in range(k):
x[i] = a[i]
for j in range(i):
x[i] = r[j][i] * (x[i] - x[j])
# У Python % завжди повертає невід'ємну остачу,
# тому окрема корекція знаку не потрібна
x[i] %= p[i]
for (let i = 0; i < k; ++i) {
x[i] = a[i];
for (let j = 0; j < i; ++j) {
// BigInt, бо добуток r[j][i] * (x[i] - x[j]) легко переповнює Number
x[i] = r[j][i] * (x[i] - x[j]);
x[i] = x[i] % p[i];
if (x[i] < 0n)
x[i] += p[i];
}
}
for i := 0; i < k; i++ {
x[i] = a[i]
for j := 0; j < i; j++ {
x[i] = r[j][i] * (x[i] - x[j])
x[i] = x[i] % p[i]
if x[i] < 0 {
x[i] += p[i]
}
}
}
Отже, ми навчилися обчислювати цифри за час . Тепер число можна обчислити за згаданою раніше формулою
Варто зазначити, що на практиці нам майже напевно доведеться обчислювати відповідь за допомогою довгої арифметики, але цифри (оскільки вони невеликі) зазвичай можна обчислювати за допомогою вбудованих типів, а тому алгоритм Гарнера дуже ефективний.
Реалізація алгоритму Гарнера
Цей алгоритм зручно реалізувати на Java, оскільки вона має вбудовану підтримку великих чисел через клас BigInteger.
Тут ми наводимо реалізацію, яка може зберігати великі числа у вигляді набору конгруентних рівнянь. Вона підтримує додавання, віднімання та множення. А за допомогою алгоритму Гарнера ми можемо перетворити набір рівнянь на єдине ціле число. У цьому коді ми беремо 100 простих чисел, більших за , що дозволяє представляти числа аж до .
final int SZ = 100;
int pr[] = new int[SZ];
int r[][] = new int[SZ][SZ];
void init() {
for (int x = 1000 * 1000 * 1000, i = 0; i < SZ; ++x)
if (BigInteger.valueOf(x).isProbablePrime(100))
pr[i++] = x;
for (int i = 0; i < SZ; ++i)
for (int j = i + 1; j < SZ; ++j)
r[i][j] =
BigInteger.valueOf(pr[i]).modInverse(BigInteger.valueOf(pr[j])).intValue();
}
class Number {
int a[] = new int[SZ];
public Number() {
}
public Number(int n) {
for (int i = 0; i < SZ; ++i)
a[i] = n % pr[i];
}
public Number(BigInteger n) {
for (int i = 0; i < SZ; ++i)
a[i] = n.mod(BigInteger.valueOf(pr[i])).intValue();
}
public Number add(Number n) {
Number result = new Number();
for (int i = 0; i < SZ; ++i)
result.a[i] = (a[i] + n.a[i]) % pr[i];
return result;
}
public Number subtract(Number n) {
Number result = new Number();
for (int i = 0; i < SZ; ++i)
result.a[i] = (a[i] - n.a[i] + pr[i]) % pr[i];
return result;
}
public Number multiply(Number n) {
Number result = new Number();
for (int i = 0; i < SZ; ++i)
result.a[i] = (int)((a[i] * 1l * n.a[i]) % pr[i]);
return result;
}
public BigInteger bigIntegerValue(boolean can_be_negative) {
BigInteger result = BigInteger.ZERO, mult = BigInteger.ONE;
int x[] = new int[SZ];
for (int i = 0; i < SZ; ++i) {
x[i] = a[i];
for (int j = 0; j < i; ++j) {
long cur = (x[i] - x[j]) * 1l * r[j][i];
x[i] = (int)((cur % pr[i] + pr[i]) % pr[i]);
}
result = result.add(mult.multiply(BigInteger.valueOf(x[i])));
mult = mult.multiply(BigInteger.valueOf(pr[i]));
}
if (can_be_negative)
if (result.compareTo(mult.shiftRight(1)) >= 0)
result = result.subtract(mult);
return result;
}
}