E010. Дискретное логарифмирование
Источник: e-maxx.ru/algo, страница PDF 31.
Problema дискретного логарифмирования заключается в том, чтобы по данным целым
,
,
решить уравнение:
где
и
— взаимно просты (примечание: если они не взаимно просты, то описанный ниже Algoritmo является некорректным; хотя, предположительно, его можно модифицировать, чтобы он по-прежнему работал). Здесь описан Algoritmo, известный как "baby-step-giant-step algorithm", предложенный
Шэнксом (Shanks) в 1971 г., работающий за время за
. Часто этот Algoritmo просто
называют Algoritmoом "meet-in-the-middle" (потому что это одно из классических применений техники "meet-in- the-middle": "разделение задачи пополам").
Algoritmo
Итак, мы имеем уравнение:
где
и
взаимно просты.
Преобразуем уравнение. Положим
где
— это заранее выбранная константа (как её выбирать в зависимости от
, мы поймём чуть позже). Иногда
называют "giant step" (поскольку увеличение его на единицу увеличивает
сразу на
), а в противоположность ему
— "baby step".
Очевидно, что любое
(из промежутка
— понятно, что такого диапазона значений будет достаточно)
можно представить в такой форме, причём для этого будет достаточно значений: Тогда уравнение принимает вид:
откуда, пользуясь тем, что
и
взаимно просты, получаем: Чтобы решить исходное уравнение, нужно find соответствующие значения
и
, чтобы значения левой и правой
частей совпали. Иначе говоря, надо решить уравнение: Эта Problema решается с помощью метода meet-in-the-middle следующим образом. Первая фаза Algoritmoа:
посчитаем значения функции
для всех значений аргумента
, и отсортируем эти значения. Вторая фаза
Algoritmoа: будем перебирать значение второй переменной
, вычислять вторую функцию
, и искать это значение
среди предвычисленных значений первой функции с помощью бинарного поиска.
Asymptotic complexity
Сначала оценим время вычисления каждой из функций
и
. И та, и другая содержит возведение в
степень, которое можно выполнять с помощью Algoritmoа бинарного возведения в степень. Тогда обе этих функции
мы можем вычислять за время
.
Сам Algoritmo в первой фазе содержит вычисление функции
для каждого возможного значения
и
дальнейшую сортировку значений, что даёт нам асимптотику:
Во второй фазе Algoritmoа происходит вычисление функции
для каждого возможного значения
и бинарный
поиск по arrayу значений
, что даёт нам асимптотику:
Теперь, когда мы сложим эти две асимптотики, у нас получится
, умноженный на сумму
и
, и
практически очевидно, что минимум достигается, когда
, т.е. для оптимальной работы Algoritmoа константу
следует выбирать так: Тогда Asymptotic complexity Algoritmoа принимает вид:
Примечание. Мы могли бы обменять ролями
и
(т.е. на первой фазе вычислять значения функции
, а а второй
—
), однако легко понять, что результат от этого не изменится, и асимптотику этим мы никак не улучшим.
Implementazione
Простейшая Implementazione
Функция
выполняет бинарное возведение числа
в степень
по модулю
, см. Бинарное возведение
в степень.
Функция
производит собственно Soluzione задачи. Эта функция returns ответ (number в промежутке
), точнее говоря, один из ответов. Функция вернёт
, если решения не существует.
int powmod (int a, int b, int m) {
int res = 1;
while (b > 0)
if (b & 1) {
res = (res * a) % m;
--b;
}
else {
a = (a * a) % m;
b >>= 1;
}
return res % m;
}
int solve (int a, int b, int m) {
int n = (int) sqrt (m + .0) + 1;
map<int,int> vals;
for (int i=n; i>=1; --i)
vals[ powmod (a, i * n, m) ] = i;
for (int i=0; i<=n; ++i) {
int cur = (powmod (a, i, m) * b) % m;
if (vals.count(cur)) {
int ans = vals[cur] * n - i;
if (ans < m)
return ans;
} }
return -1;
} Здесь мы для удобства при реализации первой фазы Algoritmoа воспользовались структурой данных "map" (красно-чёрным alberoм), которая для каждого значения функции
хранит аргумент
, при котором
это значение достигалось. При этом если одно и то же значение достигалось несколько раз, записывается наименьший из всех аргументов. Это сделано для того, чтобы впоследствии, на второй фазе Algoritmoа, нашёлся ответ в
промежутке
.
given, что аргумент функции
на первой фазе у нас перебирался от единицы и до
, а аргумент функции
на второй фазе перебирается от нуля до
, то в итоге мы покрываем всё множество возможных ответов, т.к.
отрезок
содержит в себе промежуток
. При этом отрицательным ответ получиться не мог, а
ответы, большие либо равные
мы можем игнорировать — всё равно должны находиться соответствующие им
ответы из промежутка
. Эту функцию можно изменить на тот случай, если it is required находить все решения задачи дискретного логарифма. Для этого надо заменить "map" на какую-либо другую структуру данных, позволяющую хранить для одного аргумента сразу несколько значений (наEsempio, "multimap"), и соответствующим образом изменить код второй фазы.
Улучшенная Implementazione
При оптимизации по скорости можно поступить следующим образом. Во-первых, сразу бросается в глаза ненужность бинарного возведения в степень на второй фазе Algoritmoа. Вместо этого можно просто завести переменную и домножать её каждый раз на . Во-вторых, таким же образом можно избавиться от бинарного возведения в степень и на первой фазе: в самом
деле, достаточно один раз посчитать величину
, и потом просто домножать на неё. Таким образом, логарифм в асимптотике по-прежнему останется, но это будет только логарифм, связанный со
структурой данных
(т.е., в терминах Algoritmoа, с сортировкой и бинарным поиском значений) — т.е. это
будет логарифм от
, что на практике даёт заметное ускорение.
int solve (int a, int b, int m) {
int n = (int) sqrt (m + .0) + 1;
int an = 1;
for (int i=0; i<n; ++i)
an = (an * a) % m;
map<int,int> vals;
for (int i=1, cur=an; i<=n; ++i) {
if (!vals.count(cur))
vals[cur] = i;
cur = (cur * an) % m;
}
for (int i=0, cur=b; i<=n; ++i) {
if (vals.count(cur)) {
int ans = vals[cur] * n - i;
if (ans < m)
return ans;
}
cur = (cur * a) % m;
}
return -1;
}
Наконец, если модуль
достаточно мал, то можно и вовсе избавиться от логарифма в асимптотике — просто
заведя вместо
обычный array. Также можно вспомнить про хеш-таблицы: в среднем они работают также за
, что в целом даёт
асимптотику
.
C# soluzione
bozza automatica, rivedere prima dell'inviousing System;
using System.Collections.Generic;
using System.Linq;
public static class AlgorithmDraft
{
// Auto-generated C# draft from the original e-maxx C/C++ listing. Review before production use.
int powmod (int a, int b, int m) {
int res = 1;
while (b > 0)
if (b & 1) {
res = (res * a) % m;
--b;
}
else {
a = (a * a) % m;
b >>= 1;
}
return res % m;
}
int solve (int a, int b, int m) {
int n = (int) sqrt (m + .0) + 1;
map<int,int> vals;
for (int i=n; i>=1; --i)
vals[ powmod (a, i * n, m) ] = i;
for (int i=0; i<=n; ++i) {
int cur = (powmod (a, i, m) * b) % m;
if (vals.count(cur)) {
int ans = vals[cur] * n - i;
if (ans < m)
return ans;
}
}
return -1;
}
int solve (int a, int b, int m) {
int n = (int) sqrt (m + .0) + 1;
int an = 1;
for (int i=0; i<n; ++i)
an = (an * a) % m;
map<int,int> vals;
for (int i=1, cur=an; i<=n; ++i) {
if (!vals.count(cur))
vals[cur] = i;
cur = (cur * an) % m;
}
for (int i=0, cur=b; i<=n; ++i) {
if (vals.count(cur)) {
int ans = vals[cur] * n - i;
if (ans < m)
return ans;
}
cur = (cur * a) % m;
}
return -1;
}
}
C++ soluzione
abbinato/originaleint powmod (int a, int b, int m) {
int res = 1;
while (b > 0)
if (b & 1) {
res = (res * a) % m;
--b;
}
else {
a = (a * a) % m;
b >>= 1;
}
return res % m;
}
int solve (int a, int b, int m) {
int n = (int) sqrt (m + .0) + 1;
map<int,int> vals;
for (int i=n; i>=1; --i)
vals[ powmod (a, i * n, m) ] = i;
for (int i=0; i<=n; ++i) {
int cur = (powmod (a, i, m) * b) % m;
if (vals.count(cur)) {
int ans = vals[cur] * n - i;
if (ans < m)
return ans;
}
}
return -1;
}
int solve (int a, int b, int m) {
int n = (int) sqrt (m + .0) + 1;
int an = 1;
for (int i=0; i<n; ++i)
an = (an * a) % m;
map<int,int> vals;
for (int i=1, cur=an; i<=n; ++i) {
if (!vals.count(cur))
vals[cur] = i;
cur = (cur * an) % m;
}
for (int i=0, cur=b; i<=n; ++i) {
if (vals.count(cur)) {
int ans = vals[cur] * n - i;
if (ans < m)
return ans;
}
cur = (cur * a) % m;
}
return -1;
}
Java soluzione
bozza automatica, rivedere prima dell'invioimport java.util.*;
import java.math.*;
public class AlgorithmDraft {
// Auto-generated Java draft from the original e-maxx C/C++ listing. Review before production use.
int powmod (int a, int b, int m) {
int res = 1;
while (b > 0)
if (b & 1) {
res = (res * a) % m;
--b;
}
else {
a = (a * a) % m;
b >>= 1;
}
return res % m;
}
int solve (int a, int b, int m) {
int n = (int) sqrt (m + .0) + 1;
map<int,int> vals;
for (int i=n; i>=1; --i)
vals[ powmod (a, i * n, m) ] = i;
for (int i=0; i<=n; ++i) {
int cur = (powmod (a, i, m) * b) % m;
if (vals.count(cur)) {
int ans = vals[cur] * n - i;
if (ans < m)
return ans;
}
}
return -1;
}
int solve (int a, int b, int m) {
int n = (int) sqrt (m + .0) + 1;
int an = 1;
for (int i=0; i<n; ++i)
an = (an * a) % m;
map<int,int> vals;
for (int i=1, cur=an; i<=n; ++i) {
if (!vals.count(cur))
vals[cur] = i;
cur = (cur * an) % m;
}
for (int i=0, cur=b; i<=n; ++i) {
if (vals.count(cur)) {
int ans = vals[cur] * n - i;
if (ans < m)
return ans;
}
cur = (cur * a) % m;
}
return -1;
}
}
Материал разбит как Algoritmoическая Problema: изучить постановку, понять асимптотику и реализовать Algoritmo на выбранном языке.
Vacancies for this task
offerte attive with overlapping task tags are mostrati.