Перейти до основного вмісту

Алгоритм Гарнера

Наслідком китайської теореми про остачі є те, що ми можемо представляти великі числа за допомогою масиву невеликих цілих чисел. Наприклад, нехай pp — добуток перших 10001000 простих чисел. Тоді pp має близько 30003000 цифр.

Будь-яке число aa, менше за pp, можна представити як масив a1,…,aka_1, \ldots, a_k, де ai≡a(modpi)a_i \equiv a \pmod{p_i}. Але щоб це зробити, нам, очевидно, потрібно вміти відновлювати число aa з його представлення. Один зі способів розглянуто у статті про китайську теорему про остачі.

У цій статті ми розглянемо альтернативу — алгоритм Гарнера, який також можна використати для цієї мети.

Коли підходить цей алгоритм?
  • Чи задача зводиться до відновлення великого числа з його остач за набором попарно взаємно простих модулів? (якщо ні → Китайська теорема про остачі)
  • Чи модулі pip_i фіксовані наперед, тож можна один раз передобчислити обернені елементи rijr_{ij} за час O(k2)O(k^2)?
  • Чи результат не вміщується у вбудовані типи, тобто фінальну відповідь усе одно доведеться збирати довгою арифметикою?

Представлення в мішаній системі числення​

Ми можемо представити число aa у представленні в мішаній системі числення:

a=x1+x2p1+x3p1p2+…+xkp1⋯pk−1 з xi∈[0,pi)a = x_1 + x_2 p_1 + x_3 p_1 p_2 + \ldots + x_k p_1 \cdots p_{k-1} \text{ з }x_i \in [0, p_i)

Представлення в мішаній системі числення — це позиційна система числення, яка є узагальненням типових систем числення, як-от двійкова чи десяткова. Наприклад, десяткова система числення — це позиційна система числення з основою (radix) 10. Кожне число представляється рядком цифр d1d2d3…dnd_1 d_2 d_3 \dots d_n від 00 до 99. Наприклад, рядок 415415 представляє число 4⋅102+1⋅101+5⋅1004 \cdot 10^2 + 1 \cdot 10^1 + 5 \cdot 10^0. У загальному випадку рядок цифр d1d2d3…dnd_1 d_2 d_3 \dots d_n представляє число d1bn−1+d2bn−2+⋯+dnb0d_1 b^{n-1} + d_2 b^{n-2} + \cdots + d_n b^0 у позиційній системі числення з основою bb.

У мішаній системі числення ми більше не маємо однієї основи. Основа змінюється від позиції до позиції.

Алгоритм Гарнера​

Алгоритм Гарнера обчислює цифри x1,…,xkx_1, \ldots, x_k. Зауважимо, що ці цифри відносно невеликі. Цифра xix_i — це ціле число між 00 та pi−1p_i - 1.

Нехай rijr_{ij} позначає обернений елемент до pip_i за модулем pjp_j

rij=(pi)−1(modpj)r_{ij} = (p_i)^{-1} \pmod{p_j}

який можна знайти за допомогою алгоритму, описаного у статті Обернений елемент за модулем.

Підставивши aa з представлення в мішаній системі числення у перше конгруентне рівняння, отримуємо

a1≡x1(modp1).a_1 \equiv x_1 \pmod{p_1}.

Підстановка у друге рівняння дає

a2≡x1+x2p1(modp2),a_2 \equiv x_1 + x_2 p_1 \pmod{p_2},

що можна переписати, віднявши x1x_1 та поділивши на p1p_1, у вигляді

a2−x1≡x2p1(modp2)(a2−x1)r12≡x2(modp2)x2≡(a2−x1)r12(modp2)\begin{array}{rclr} a_2 - x_1 &\equiv& x_2 p_1 &\pmod{p_2} \\ (a_2 - x_1) r_{12} &\equiv& x_2 &\pmod{p_2} \\ x_2 &\equiv& (a_2 - x_1) r_{12} &\pmod{p_2} \end{array}

Аналогічно отримуємо, що

x3≡((a3−x1)r13−x2)r23(modp3).x_3 \equiv ((a_3 - x_1) r_{13} - x_2) r_{23} \pmod{p_3}.

Тепер ми чітко бачимо закономірність, яка з'являється, і яку можна виразити таким кодом:

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];
}
}

Отже, ми навчилися обчислювати цифри xix_i за час O(k2)O(k^2). Тепер число aa можна обчислити за згаданою раніше формулою

a=x1+x2⋅p1+x3⋅p1⋅p2+…+xk⋅p1⋯pk−1a = x_1 + x_2 \cdot p_1 + x_3 \cdot p_1 \cdot p_2 + \ldots + x_k \cdot p_1 \cdots p_{k-1}

Варто зазначити, що на практиці нам майже напевно доведеться обчислювати відповідь aa за допомогою довгої арифметики, але цифри xix_i (оскільки вони невеликі) зазвичай можна обчислювати за допомогою вбудованих типів, а тому алгоритм Гарнера дуже ефективний.

Реалізація алгоритму Гарнера​

Цей алгоритм зручно реалізувати на Java, оскільки вона має вбудовану підтримку великих чисел через клас BigInteger.

Тут ми наводимо реалізацію, яка може зберігати великі числа у вигляді набору конгруентних рівнянь. Вона підтримує додавання, віднімання та множення. А за допомогою алгоритму Гарнера ми можемо перетворити набір рівнянь на єдине ціле число. У цьому коді ми беремо 100 простих чисел, більших за 10910^9, що дозволяє представляти числа аж до 1090010^{900}.

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;
}
}