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

Дискретний корінь

Задача знаходження дискретного кореня формулюється так. Дано просте число nn і два цілих числа aa та kk; потрібно знайти всі xx, для яких:

xka(modn)x^k \equiv a \pmod n

Коли підходить цей алгоритм?
  • Чи задача просить знайти основу xx у рівнянні xka(modn)x^k \equiv a \pmod n? (якщо натомість невідомий показник → Дискретний логарифм)
  • Чи модуль nn простий (метод спирається на існування первісного кореня)? (якщо ні → потрібні узагальнення через первісний корінь за степенем простого; див. Первісний корінь)
  • Чи модуль nn достатньо малий, щоб вкладений крок дискретного логарифма O(nlogn)O(\sqrt{n}\log n) був прийнятним?

Алгоритм

Ми розв'яжемо цю задачу, звівши її до задачі дискретного логарифма.

Скористаймося поняттям первісного кореня за модулем nn. Нехай gg — первісний корінь за модулем nn. Зауважимо, що оскільки nn просте, він обов'язково існує, і його можна знайти за O(Anslogϕ(n)logn)=O(Anslog2n)O(Ans \cdot \log \phi (n) \cdot \log n) = O(Ans \cdot \log^2 n) плюс час на факторизацію ϕ(n)\phi (n).

Випадок a=0a = 0 легко відкинути. У цьому випадку відповідь, очевидно, єдина: x=0x = 0.

Оскільки ми знаємо, що nn просте, і будь-яке число від 1 до n1n-1 можна подати як степінь первісного кореня, ми можемо переписати задачу про дискретний корінь так:

(gy)ka(modn)(g^y)^k \equiv a \pmod n

де

xgy(modn)x \equiv g^y \pmod n

Це, своєю чергою, можна переписати як

(gk)ya(modn)(g^k)^y \equiv a \pmod n

Тепер ми маємо одне невідоме yy, а це задача дискретного логарифма. Розв'язок можна знайти за допомогою алгоритму великих і малих кроків Шенкса (baby-step giant-step) за O(nlogn)O(\sqrt {n} \log n) (або переконатися, що розв'язків немає).

Знайшовши один розв'язок y0y_0, одним із розв'язків задачі про дискретний корінь буде x0=gy0(modn)x_0 = g^{y_0} \pmod n.

Знаходження всіх розв'язків з одного відомого

Щоб розв'язати поставлену задачу повністю, нам потрібно знайти всі розв'язки, знаючи один із них: x0=gy0(modn)x_0 = g^{y_0} \pmod n.

Пригадаймо, що первісний корінь завжди має порядок ϕ(n)\phi (n), тобто найменший степінь gg, який дає 1, — це ϕ(n)\phi (n). Тому якщо ми додамо до показника доданок ϕ(n)\phi (n), то отримаємо те саме значення:

xkgy0k+lϕ(n)a(modn)lZx^k \equiv g^{ y_0 \cdot k + l \cdot \phi (n)} \equiv a \pmod n \forall l \in Z

Отже, усі розв'язки мають вигляд:

x=gy0+lϕ(n)k(modn)lZx = g^{y_0 + \frac {l \cdot \phi (n)}{k}} \pmod n \forall l \in Z.

де ll обрано так, щоб дріб був цілим числом. Аби це виконувалося, чисельник має ділитися на найменше спільне кратне ϕ(n)\phi (n) і kk. Пам'ятаючи, що найменше спільне кратне двох чисел lcm(a,b)=abgcd(a,b)lcm(a, b) = \frac{a \cdot b}{gcd(a, b)}, отримаємо

x=gy0+iϕ(n)gcd(k,ϕ(n))(modn)iZx = g^{y_0 + i \frac {\phi (n)}{gcd(k, \phi (n))}} \pmod n \forall i \in Z.

Це остаточна формула для всіх розв'язків задачі про дискретний корінь.

Реалізація

Нижче наведено повну реалізацію, що включає процедури знаходження первісного кореня, дискретного логарифма, а також знаходження та виведення всіх розв'язків.

int gcd(int a, int b) {
return a ? gcd(b % a, a) : b;
}

int powmod(int a, int b, int p) {
int res = 1;
while (b > 0) {
if (b & 1) {
res = res * a % p;
}
a = a * a % p;
b >>= 1;
}
return res;
}

// Знаходить первісний корінь за модулем p
int generator(int p) {
vector<int> fact;
int phi = p-1, n = phi;
for (int i = 2; i * i <= n; ++i) {
if (n % i == 0) {
fact.push_back(i);
while (n % i == 0)
n /= i;
}
}
if (n > 1)
fact.push_back(n);

for (int res = 2; res <= p; ++res) {
bool ok = true;
for (int factor : fact) {
if (powmod(res, phi / factor, p) == 1) {
ok = false;
break;
}
}
if (ok) return res;
}
return -1;
}

// Ця програма знаходить усі числа x такі, що x^k = a (mod n)
int main() {
int n, k, a;
scanf("%d %d %d", &n, &k, &a);
if (a == 0) {
puts("1\n0");
return 0;
}

int g = generator(n);

// Алгоритм дискретного логарифма великих і малих кроків (baby-step giant-step)
int sq = (int) sqrt (n + .0) + 1;
vector<pair<int, int>> dec(sq);
for (int i = 1; i <= sq; ++i)
dec[i-1] = {powmod(g, i * sq * k % (n - 1), n), i};
sort(dec.begin(), dec.end());
int any_ans = -1;
for (int i = 0; i < sq; ++i) {
int my = powmod(g, i * k % (n - 1), n) * a % n;
auto it = lower_bound(dec.begin(), dec.end(), make_pair(my, 0));
if (it != dec.end() && it->first == my) {
any_ans = it->second * sq - i;
break;
}
}
if (any_ans == -1) {
puts("0");
return 0;
}

// Виводимо всі можливі відповіді
int delta = (n-1) / gcd(k, n-1);
vector<int> ans;
for (int cur = any_ans % delta; cur < n-1; cur += delta)
ans.push_back(powmod(g, cur, n));
sort(ans.begin(), ans.end());
printf("%d\n", ans.size());
for (int answer : ans)
printf("%d ", answer);
}

Задачі для практики