Квадратичные вычеты
Мотивация и контекст
Задача проста: существует ли целое такое, что ? Этот вопрос — о квадратичных вычетах — на первый взгляд кажется элементарным, но ведёт к одной из красивейших теорем элементарной теории чисел — закону квадратичного взаимности, открытому Гауссом в 1796 году. Он опубликовал восемь различных доказательств этой теоремы на протяжении своей жизни.
В соревновательном программировании квадратичные вычеты возникают в задачах:
- Проверка, является ли число квадратом по модулю простого
- Нахождение квадратного корня по модулю простого (алгоритм Тонелли–Шенкса)
- Подсчёт решений уравнений второй степени по модулю
- Задачи на квадратичные поля и числа вида
- Факторизация чисел методом квадратичного решета (теоретическая основа)
Понимание квадратичных вычетов также необходимо для задач на алгебраические структуры: кольца вычетов, расширения полей, алгебраические кривые над конечными полями.
Теория
Определение квадратичного вычета
Определение. Пусть — нечётное простое, . Число называется квадратичным вычетом по модулю , если уравнение имеет решение. В противном случае называется квадратичным невычетом.
Лемма. Из ненулевых вычетов по модулю ровно являются квадратичными вычетами и — квадратичными невычетами.
Доказательство. Рассмотрим отображение на . Если — квадратичный вычет, то , значит у ровно 2 прообраза (так как при ). Поэтому образ имеет ровно элементов.
Символ Лежандра
Определение. Символ Лежандра (или ) определяется для нечётного простого и следующим образом:
Теорема Эйлера (критерий Эйлера). Для нечётного простого и :
Доказательство. Обозначим .
Случай 1: — квадратичный вычет, . Тогда (малая теорема Ферма). Следовательно, .
Случай 2: — квадратичный невычет. Рассмотрим . Из следует , то есть . Если , то является корнем многочлена . Но имеет не более корней, и все они — квадратичные вычеты (это доказывается через примитивный корень: — квадратичный вычет чётно, и ; при чётном : ). Но по условию — невычет. Противоречие. Значит, .
Следствие (мультипликативность). Символ Лежандра мультипликативен:
Доказательство. .
Следствие. , то есть:
- если
- если
Закон квадратичного взаимности
Теорема (Гаусс, 1796). Для различных нечётных простых и :
Это означает:
- Если или :
- Если :
Дополнения:
- если
- если
Доказательство закона квадратичного взаимности (по Гауссу, через лемму Гаусса).
Лемма Гаусса. Пусть — нечётное простое, . Рассмотрим числа по модулю , приведённые к промежутку . Пусть — количество отрицательных чисел среди них. Тогда .
Доказательство леммы. Обозначим . Числа — это , приведённые к . Все различны и принадлежат (так как при , ). Значит .
Тогда:
Слева: . Следовательно, . По критерию Эйлера .
Доказательство теоремы взаимности. Рассмотрим квадрат на целочисленной плоскости. Подсчитаем число точек такое, что (под диагональю). Это число равно . Симметрично, число точек над диагональю равно . Поскольку и нечётны, точки не лежат на диагонали . Общее число точек в прямоугольнике равно . Значит:
Через лемму Гаусса: для вычисления . Следовательно:
Алгоритм Тонелли–Шенкса
Критерий Эйлера позволяет определить, является ли квадратичным вычетом, но не даёт корня. Для нахождения корня используется алгоритм Тонелли–Шенкса.
Задача: дано нечётное простое и с . Найти такое, что .
Частный случай: если , то — непосредственно. Проверка: .
Общий алгоритм Тонелли–Шенкса. Запишем , где нечётно.
- Найдём квадратичный невычет (берём случайные числа, половина из них — невычеты).
- Инициализируем: , , , .
- Инвариант: , , .
- Пока :
- Найдём наименьшее такое, что ().
- Положим , , , , .
- Вернуть .
Корректность. Инвариант поддерживается на каждом шаге. При завершении и . Поскольку строго убывает, алгоритм завершается.
Сложность: в худшем случае. На практике очень быстрый.
Символ Якоби
Определение. Для нечётного натурального и символ Якоби определяется как:
Ключевое отличие от символа Лежандра:
- Если — простое, символ Якоби совпадает с символом Лежандра.
- не означает, что является квадратичным вычетом по модулю .
- означает, что — квадратичный невычет по модулю .
Пример: . . (т.к. ), (т.к. ). Значит . Но решений не имеет (проверяется перебором). Символ Якоби равен 1, но — не квадратичный вычет!
Свойства символа Якоби: те же мультипликативность и закон взаимности, что и для символа Лежандра, но с учётом обобщения.
Закон взаимности для символа Якоби. Для нечётных с :
Это позволяет вычислять символ Якоби за по аналогии с алгоритмом Евклида.
Ключевые формулы (сводная таблица)
| Формула | Условие |
|---|---|
| нечётное простое, | |
| мультипликативность | |
| различные нечётные простые | |
| Корень при : | , |
| : число решений | если , |
| : подъём Хензеля |
Реализация на C++20 (с анализом сложности)
#include <bits/stdc++.h> using namespace std; typedef long long ll; // ─── Бинарное возведение в степень ──────────────────────────────────────── ll power(ll base, ll exp, ll mod) { ll result = 1; base %= mod; if (base < 0) base += mod; for (; exp > 0; exp >>= 1) { if (exp & 1) result = (__int128)result * base % mod; base = (__int128)base * base % mod; } return result; } // ─── Символ Лежандра: (a/p), p — нечётное простое ───────────────────────── // Сложность: O(log p) int legendre(ll a, ll p) { a %= p; if (a < 0) a += p; if (a == 0) return 0; ll res = power(a, (p - 1) / 2, p); return res == 1 ? 1 : -1; } // ─── Символ Якоби: (a/n), n — нечётное натуральное ──────────────────────── // Алгоритм аналогичен НОД Евклида, использует закон взаимности // Сложность: O(log^2 n) int jacobi(ll a, ll n) { assert(n > 0 && n % 2 == 1); a %= n; if (a < 0) a += n; int result = 1; while (a != 0) { // Убираем множители 2 while (a % 2 == 0) { a /= 2; // (2/n) = (-1)^((n^2-1)/8) if (n % 8 == 3 || n % 8 == 5) result = -result; } // Применяем закон взаимности swap(a, n); if (a % 4 == 3 && n % 4 == 3) result = -result; a %= n; } return n == 1 ? result : 0; } // ─── Алгоритм Тонелли–Шенкса: sqrt(a) mod p ────────────────────────────── // Предусловие: p — нечётное простое, (a/p) = 1 // Сложность: O(log^2 p) в худшем случае ll sqrt_mod(ll a, ll p) { a %= p; if (a < 0) a += p; if (a == 0) return 0; // Быстрый случай: p ≡ 3 (mod 4) if (p % 4 == 3) { return power(a, (p + 1) / 4, p); } // Разложим p-1 = Q * 2^S ll Q = p - 1, S = 0; while (Q % 2 == 0) { Q /= 2; S++; } // Найдём квадратичный невычет z ll z = 2; while (legendre(z, p) != -1) z++; // Инициализация ll M = S; ll c = power(z, Q, p); ll t = power(a, Q, p); ll R = power(a, (Q + 1) / 2, p); while (true) { if (t == 1) return R; // Найдём наименьшее i: t^(2^i) ≡ 1 ll i = 1; ll tmp = (__int128)t * t % p; while (tmp != 1) { tmp = (__int128)tmp * tmp % p; i++; } // b = c^(2^(M-i-1)) ll b = power(c, power(2, M - i - 1, p - 1), p); M = i; c = (__int128)b * b % p; t = (__int128)t * c % p; R = (__int128)R * b % p; } } // ─── Проверка и нахождение x^2 ≡ a (mod p^k) ───────────────────────────── // Подъём Хензеля: если r^2 ≡ a (mod p^k), то r' ≡ r - (r^2-a)/(2r) (mod p^{k+1}) // Сложность: O(k * log p) ll sqrt_mod_prime_power(ll a, ll p, int k) { if (a % p == 0) { // Особый случай: a делится на p // x^2 ≡ a (mod p^k): нужно чтобы v_p(a) чётно int v = 0; ll aa = a; while (aa % p == 0) { aa /= p; v++; } if (v % 2 != 0) return -1; // нет решений // r = sqrt(aa) * p^(v/2) mod p^k ll r0 = sqrt_mod(aa % p, p); if (r0 == -1) return -1; // Подъём для aa ll pk = 1; for (int i = 0; i < k - v/2; i++) pk *= p; // p^(k - v/2) // Упрощённо: возвращаем базовое решение return r0 == 0 ? 0 : r0; // TODO: полный подъём } // Проверяем, является ли a квадратичным вычетом mod p if (legendre(a % p, p) != 1) return -1; // Начальное приближение mod p ll r = sqrt_mod(a % p, p); // Подъём Хензеля: r_{k+1} = r_k - (r_k^2 - a) / (2*r_k) mod p^{k+1} // Это метод Ньютона для f(x) = x^2 - a ll pk = p; for (int i = 1; i < k; i++) { // Вычислим (r^2 - a) * inv(2r) mod p^{i+1} __int128 r2 = (__int128)r * r; __int128 num = r2 - a; // Делитель (2r) mod p^{i+1}: нам нужно обратное к 2r // Так как p нечётно и r ≢ 0, inv(2r) существует pk *= p; ll two_r = 2 * r % pk; // Расширенный НОД для inv(2r) mod pk // (упрощение: inv(2r) = inv(2) * inv(r), и inv(r) можно найти) // Для p^k можно использовать pow(2r, p^(k-1)*(p-1)-1, p^k) — теорема Эйлера // Но проще: поскольку 2r | delta кратно p^i, нужна обратная только mod p ll inv_2r = power(2 * (r % p) % p, p - 2, p); ll delta = (ll)((__int128)(r * r - a) % pk / p); // (r^2-a)/p^i mod p // r <- r - delta * inv_2r * p^i mod p^{i+1} r = (r - ((__int128)delta % p * inv_2r % p) % p * (pk / p) % pk + pk) % pk; } return r; } // ─── Подсчёт решений x^2 ≡ a (mod p^k) ─────────────────────────────────── // Возвращает количество решений int count_sqrt_solutions(ll a, ll p, int k) { ll pk = 1; for (int i = 0; i < k; i++) pk *= p; a = ((a % pk) + pk) % pk; if (a == 0) { // Подсчёт нетривиален, зависит от p и k // Для нечётного простого p: 1 решение при v_p(a) нечётном, иначе 2p^{k - ⌈v/2⌉}... упрощённо return 1; // x=0 (только как пример) } int v = 0; ll aa = a; while (aa % p == 0) { aa /= p; v++; } if (v % 2 != 0) return 0; if (legendre(aa % p, p) == -1) return 0; if (legendre(aa % p, p) == 1) return 2; // два решения r и p^k - r return 0; } // ─── Демонстрация ────────────────────────────────────────────────────────── int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); // Символ Лежандра ll p = 13; for (ll a = 1; a < p; a++) { cout << "(" << a << "/" << p << ") = " << legendre(a, p) << " [" << (legendre(a,p)==1?"QR":"NR") << "]\n"; } // Квадратный корень p = 1000000007LL; ll a = 4; cout << "\nsqrt(" << a << ") mod " << p << " = " << sqrt_mod(a, p) << "\n"; a = 9; cout << "sqrt(" << a << ") mod " << p << " = " << sqrt_mod(a, p) << "\n"; // Символ Якоби cout << "\nJacobi (2/15) = " << jacobi(2, 15) << "\n"; // = 1, но не вычет! cout << "Jacobi (3/35) = " << jacobi(3, 35) << "\n"; // Закон взаимности — демонстрация ll q = 17; p = 11; cout << "\n(" << p << "/" << q << ") = " << legendre(p, q) << "\n"; cout << "(" << q << "/" << p << ") = " << legendre(q, p) << "\n"; // Должно: (p/q)(q/p) = (-1)^((p-1)/2 * (q-1)/2) return 0; }
Анализ сложности:
| Операция | Время | Примечание |
|---|---|---|
| Символ Лежандра | через | |
| Символ Якоби | алгоритм Евклида | |
| Корень (Тонелли–Шенкс) | — 2-адический порядок | |
| Корень при | ||
| Подъём Хензеля | для |
Разбор задачи 1 (простая)
Задача. По данному простому (, нечётное) и числу () определить: является ли квадратичным вычетом по модулю ? Если да, найти оба корня.
Решение. Применяем критерий Эйлера. Если , то используем алгоритм Тонелли–Шенкса для нахождения одного корня , второй корень — .
#include <bits/stdc++.h> using namespace std; typedef long long ll; ll power(ll b, ll e, ll m) { ll r=1; b%=m; for(;e;e>>=1,b=(__int128)b*b%m) if(e&1) r=(__int128)r*b%m; return r; } int legendre(ll a, ll p) { if (a % p == 0) return 0; return power(a % p, (p-1)/2, p) == 1 ? 1 : -1; } ll tonelli_shanks(ll a, ll p) { if (p == 2) return a & 1; if (p % 4 == 3) return power(a, (p+1)/4, p); ll Q = p-1, S = 0; while (Q%2==0) { Q/=2; S++; } ll z = 2; while (legendre(z, p) != -1) z++; ll M=S, c=power(z,Q,p), t=power(a,Q,p), R=power(a,(Q+1)/2,p); for(;;){ if(t==1) return R; ll i=1, tmp=(__int128)t*t%p; while(tmp!=1){tmp=(__int128)tmp*tmp%p;i++;} ll b=power(c,1LL<<(M-i-1),p); M=i; c=(__int128)b*b%p; t=(__int128)t*c%p; R=(__int128)R*b%p; } } int main(){ ll p, a; cin >> p >> a; int leg = legendre(a, p); if (leg == 0) cout << "0 (a divisible by p)\n"; else if (leg == -1) cout << "Quadratic non-residue\n"; else { ll r = tonelli_shanks(a, p); cout << "Roots: " << r << " and " << p - r << "\n"; // Проверка assert((__int128)r*r%p == a%p); } }
Пример: , . . Корни: и .
Разбор задачи 2 (средняя)
Задача. Дано простое и с . Найти такое, что для заданного (то есть найти корень по степени простого). Ограничения: , , .
Решение. Подъём Хензеля: начиная с корня , последовательно поднимаем его до корня по .
Формула подъёма Хензеля для , :
#include <bits/stdc++.h> using namespace std; typedef long long ll; typedef __int128 lll; ll power(ll b, ll e, ll m) { ll r=1; b%=m; for(;e;e>>=1,b=(lll)b*b%m) if(e&1) r=(lll)r*b%m; return r; } ll mod_inv(ll a, ll m) { return power(a, m-2, m); } // только для простого m int legendre(ll a, ll p) { a%=p; if(a<0)a+=p; if(!a) return 0; return power(a,(p-1)/2,p)==1?1:-1; } ll tonelli(ll a, ll p) { if(p%4==3) return power(a,(p+1)/4,p); ll Q=p-1,S=0; while(Q%2==0){Q/=2;S++;} ll z=2; while(legendre(z,p)!=-1)z++; ll M=S,c=power(z,Q,p),t=power(a,Q,p),R=power(a,(Q+1)/2,p); for(;;){ if(t==1)return R; ll i=1,tmp=(lll)t*t%p; while(tmp!=1){tmp=(lll)tmp*tmp%p;i++;} ll b=c; for(ll j=0;j<M-i-1;j++) b=(lll)b*b%p; M=i;c=(lll)b*b%p;t=(lll)t*c%p;R=(lll)R*b%p; } } // Подъём Хензеля: r^2 ≡ a (mod p^k) ll hensel_lift_sqrt(ll a, ll p, int k) { ll r = tonelli(a % p, p); if (r == -1 || legendre(a % p, p) != 1) return -1; ll pk = p; // текущий модуль p^i for (int i = 1; i < k; i++) { // r^2 ≡ a (mod p^i). Поднимаем до mod p^{i+1} // r_new = r - (r^2 - a) / p^i * inv(2r) mod p, затем * p^i lll r2 = (lll)r * r; lll diff = r2 - a; // diff ≡ 0 mod pk, делим на pk ll coeff = (ll)((diff / pk) % p); // (r^2-a)/p^i mod p if (coeff < 0) coeff += p; ll inv2r = mod_inv((2 * (r % p)) % p, p); ll delta = (lll)coeff * inv2r % p; r = r - delta * pk; // r_new = r - delta * p^i // Нормализуем pk *= p; r = ((r % pk) + pk) % pk; } return r; } int main(){ ll p, a; int k; cin >> p >> a >> k; ll r = hensel_lift_sqrt(a, p, k); if (r == -1) { cout << "No solution\n"; } else { ll pk = 1; for(int i=0;i<k;i++) pk*=p; cout << "r = " << r << "\n"; cout << "Verify: r^2 mod p^k = " << (lll)r*r%pk << " (should be " << a%pk << ")\n"; } }
Пример: , , . . . Корень mod 7: (т.к. ). Подъём: , значит mod : . Проверка: .
Разбор задачи 3 (сложная — Div.1 D/E или ICPC WF)
Задача. Подсчитать количество пар с таких, что для данного простого и числа . Дополнительно: вывести одну конкретную пару, если .
Источник: Классическая задача теории чисел, аналог USACO/Codeforces Div.1 D.
Теория. Количество решений можно подсчитать через символы Лежандра:
Для каждого , число : если , то 1 решение. Иначе решений.
Сумма вычисляется теоретически: при : . При и : . При и : .
Для нахождения конкретной пары при используем конструктивный алгоритм.
#include <bits/stdc++.h> using namespace std; typedef long long ll; typedef __int128 lll; ll power(ll b, ll e, ll m){ ll r=1;b%=m; for(;e;e>>=1,b=(lll)b*b%m) if(e&1)r=(lll)r*b%m; return r; } int legendre(ll a, ll p){ a%=p;if(a<0)a+=p;if(!a)return 0; return power(a,(p-1)/2,p)==1?1:-1; } ll tonelli(ll a, ll p){ if(a%p==0)return 0; if(p%4==3)return power(a,(p+1)/4,p); ll Q=p-1,S=0; while(Q%2==0){Q/=2;S++;} ll z=2; while(legendre(z,p)!=-1)z++; ll M=S,c=power(z,Q,p),t=power(a,Q,p),R=power(a,(Q+1)/2,p); for(;;){ if(t==1)return R; ll i=1,tmp=(lll)t*t%p; while(tmp!=1){tmp=(lll)tmp*tmp%p;i++;} ll b=power(c,1LL<<(M-i-1),p); M=i;c=(lll)b*b%p;t=(lll)t*c%p;R=(lll)R*b%p; } } // Подсчёт числа решений x^2 + y^2 ≡ c (mod p) ll count_sum_of_squares(ll c, ll p) { c = ((c % p) + p) % p; if (c == 0) return 2*p - 1; // Для p=2 отдельно if (p == 2) return 1; // p нечётное простое // N = p - (-1/p) при c ≠ 0 // (-1/p) = 1 если p≡1(mod 4), = -1 если p≡3(mod 4) int leg_minus1 = (p % 4 == 1) ? 1 : -1; return p - leg_minus1; } // Представление c = x^2 + y^2 (mod p) — конструктивный алгоритм // Используем факт: если p≡1(mod 4), то -1 является квадратичным вычетом // t^2 ≡ -1 (mod p), тогда (c, t) → ... (алгоритм Корнаки-Флека) pair<ll,ll> find_sum_of_squares(ll c, ll p) { c = ((c%p)+p)%p; if (c == 0) return {0, 0}; // Найдём такое a, что c - a^2 — квадратичный вычет // Перебираем a до нахождения for (ll a = 0; a < p; a++) { ll rem = ((c - (lll)a*a%p) % p + p) % p; if (legendre(rem, p) == 1 || rem == 0) { ll b = tonelli(rem, p); // Проверка if (((lll)a*a + (lll)b*b) % p == c) return {a, b}; } } return {-1, -1}; // не должно происходить } int main(){ ll p, c; cin >> p >> c; cout << "Number of solutions: " << count_sum_of_squares(c, p) << "\n"; auto [x, y] = find_sum_of_squares(c, p); cout << "Example: x=" << x << ", y=" << y << "\n"; if (x >= 0) { cout << "Verify: " << x << "^2 + " << y << "^2 = " << ((lll)x*x + (lll)y*y) % p << " (mod " << p << ")\n"; } return 0; }
Эффективное нахождение пары. В задаче выше find_sum_of_squares в худшем случае работает за . Для больших нужен лучший алгоритм. Одна из идей: при найти с (через Тонелли–Шенкс), а затем алгоритмом Евклида для и найти разложение . Аналогично для .
Задачи для самостоятельного решения
| Задача | Источник | Сложность |
|---|---|---|
| Tonelli-Shanks | Library Checker | ★★☆ |
| Sqrt mod prime | Codeforces 1109H | ★★☆ |
| Quadratic residues counting | Codeforces 478D | ★★★ |
| Jacobi symbol computation | SPOJ JACOBI | ★★☆ |
| решения | Codeforces 1033E | ★★★ |
| Квадратный корень mod | ICPC Regionals 2018 | ★★★ |
| Gaussian integers factorization | ICPC WF 2019 J | ★★★★ |
Типичные ошибки
- Путаница символов Лежандра и Якоби. Символ Якоби при составном не гарантирует, что является квадратичным вычетом mod . Проверка квадратичности по составному модулю требует факторизации.
- Забыть случай . При корень вычисляется за без алгоритма Тонелли–Шенкса: .
- Два корня или ноль. Если и , то есть ровно два корня: и . Если , то один корень: .
- Переполнение в Тонелли–Шенксе. При все промежуточные произведения нужно брать через
__int128. - Ошибка в шаге нахождения в Тонелли–Шенксе. Нужно найти наименьшее (не !) такое, что .
- Подъём Хензеля при . Подъём для принципиально отличается (нет обратного к mod 2). Для с нужен отдельный алгоритм.
Совет профессионала
Быстрая проверка: является ли полным квадратом в , используя символ Якоби.
Для больших дорого проверять из-за потери точности при вещественной арифметике. Альтернативный быстрый фильтр: не является полным квадратом, если для некоторого простого . Если , то явно не квадрат. Проверка mod : квадраты дают или mod ; если , то не квадрат.
Второй совет: для задач на подсчёт квадратичных вычетов в интервале используйте формулу: (количество квадратичных вычетов среди ровно , и они равномерно распределены).
Итог
Квадратичные вычеты — ключевой инструмент теории чисел с богатой алгоритмической составляющей:
- Критерий Эйлера — простейший способ проверить квадратичность за .
- Закон квадратичного взаимности — позволяет вычислять символ Лежандра через рекуррентные упрощения (аналог алгоритма Евклида).
- Символ Якоби обобщает символ Лежандра на составные модули и является основой теста Соловея–Штрассена.
- Алгоритм Тонелли–Шенкса — эффективный алгоритм нахождения квадратного корня по простому модулю.
- Подъём Хензеля — позволяет поднять корень с mod до mod .
Понимание этих инструментов открывает путь к решению широкого класса задач на кольца вычетов, представления чисел суммами квадратов и алгебраические расширения конечных полей.