E010. Дискретное логарифмирование

e-maxx algorithm original: C/C++ #algorithm #emaxx #math #number-theory
선택한 UI 언어에 맞게 문제 텍스트를 러시아어에서 번역합니다. 코드는 변경하지 않습니다.

Источник: e-maxx.ru/algo, страница PDF 31.

문제 дискретного логарифмирования заключается в том, чтобы по данным целым

,

,

решить уравнение:

где

и

— взаимно просты (примечание: если они не взаимно просты, то описанный ниже 알고리즘 является некорректным; хотя, предположительно, его можно модифицировать, чтобы он по-прежнему работал). Здесь описан 알고리즘, известный как "baby-step-giant-step algorithm", предложенный

Шэнксом (Shanks) в 1971 г., работающий за время за

. Часто этот 알고리즘 просто

называют 알고리즘ом "meet-in-the-middle" (потому что это одно из классических применений техники "meet-in- the-middle": "разделение задачи пополам").

알고리즘

Итак, мы имеем уравнение:

где

и

взаимно просты.

Преобразуем уравнение. Положим

где

— это заранее выбранная константа (как её выбирать в зависимости от

, мы поймём чуть позже). Иногда

называют "giant step" (поскольку увеличение его на единицу увеличивает

сразу на

), а в противоположность ему

— "baby step".

Очевидно, что любое

(из промежутка

— понятно, что такого диапазона значений будет достаточно)

можно представить в такой форме, причём для этого будет достаточно значений: Тогда уравнение принимает вид:

откуда, пользуясь тем, что

и

взаимно просты, получаем: Чтобы решить исходное уравнение, нужно find соответствующие значения

и

, чтобы значения левой и правой

частей совпали. Иначе говоря, надо решить уравнение: Эта 문제 решается с помощью метода meet-in-the-middle следующим образом. Первая фаза 알고리즘а:

посчитаем значения функции

для всех значений аргумента

, и отсортируем эти значения. Вторая фаза

알고리즘а: будем перебирать значение второй переменной

, вычислять вторую функцию

, и искать это значение

среди предвычисленных значений первой функции с помощью бинарного поиска.

Asymptotic complexity

Сначала оценим время вычисления каждой из функций

и

. И та, и другая содержит возведение в

степень, которое можно выполнять с помощью 알고리즘а бинарного возведения в степень. Тогда обе этих функции

мы можем вычислять за время

.

Сам 알고리즘 в первой фазе содержит вычисление функции

для каждого возможного значения

и

дальнейшую сортировку значений, что даёт нам асимптотику:

Во второй фазе 알고리즘а происходит вычисление функции

для каждого возможного значения

и бинарный

поиск по 배열у значений

, что даёт нам асимптотику:

Теперь, когда мы сложим эти две асимптотики, у нас получится

, умноженный на сумму

и

, и

практически очевидно, что минимум достигается, когда

, т.е. для оптимальной работы 알고리즘а константу

следует выбирать так: Тогда Asymptotic complexity 알고리즘а принимает вид:

Примечание. Мы могли бы обменять ролями

и

(т.е. на первой фазе вычислять значения функции

, а а второй

), однако легко понять, что результат от этого не изменится, и асимптотику этим мы никак не улучшим.

구현

Простейшая 구현

Функция

выполняет бинарное возведение числа

в степень

по модулю

, см. Бинарное возведение

в степень.

Функция

производит собственно 해법 задачи. Эта функция 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;

} Здесь мы для удобства при реализации первой фазы 알고리즘а воспользовались структурой данных "map" (красно-чёрным 트리м), которая для каждого значения функции

хранит аргумент

, при котором

это значение достигалось. При этом если одно и то же значение достигалось несколько раз, записывается наименьший из всех аргументов. Это сделано для того, чтобы впоследствии, на второй фазе 알고리즘а, нашёлся ответ в

промежутке

.

given, что аргумент функции

на первой фазе у нас перебирался от единицы и до

, а аргумент функции

на второй фазе перебирается от нуля до

, то в итоге мы покрываем всё множество возможных ответов, т.к.

отрезок

содержит в себе промежуток

. При этом отрицательным ответ получиться не мог, а

ответы, большие либо равные

мы можем игнорировать — всё равно должны находиться соответствующие им

ответы из промежутка

. Эту функцию можно изменить на тот случай, если it is required находить все решения задачи дискретного логарифма. Для этого надо заменить "map" на какую-либо другую структуру данных, позволяющую хранить для одного аргумента сразу несколько значений (на예제, "multimap"), и соответствующим образом изменить код второй фазы.

Улучшенная 구현

При оптимизации по скорости можно поступить следующим образом. Во-первых, сразу бросается в глаза ненужность бинарного возведения в степень на второй фазе 알고리즘а. Вместо этого можно просто завести переменную и домножать её каждый раз на . Во-вторых, таким же образом можно избавиться от бинарного возведения в степень и на первой фазе: в самом

деле, достаточно один раз посчитать величину

, и потом просто домножать на неё. Таким образом, логарифм в асимптотике по-прежнему останется, но это будет только логарифм, связанный со

структурой данных

(т.е., в терминах 알고리즘а, с сортировкой и бинарным поиском значений) — т.е. это

будет логарифм от

, что на практике даёт заметное ускорение.

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# 해법

자동 초안, 제출 전 검토
using 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++ 해법

매칭됨/원본
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;
}

Java 해법

자동 초안, 제출 전 검토
import 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;
    }
}

Материал разбит как 알고리즘ическая 문제: изучить постановку, понять асимптотику и реализовать 알고리즘 на выбранном языке.

Vacancies for this task

활성 채용 with overlapping task tags are 표시됨.

전체 채용
아직 활성 채용이 없습니다.