Непрерывные дроби и уравнение Пелля
Мотивация и контекст
Рассмотрим задачу: найти целые такие, что и максимально при . Это уравнение Пелля — одна из центральных задач теории чисел. Его решения связаны с наилучшими рациональными приближениями к — именно здесь в игру вступают непрерывные дроби.
Непрерывные дроби — это представление чисел в виде . Этот инструмент возник ещё в работах Евклида (алгоритм НОД — это и есть разложение в непрерывную дробь) и сегодня используется в задачах:
- Наилучшие рациональные приближения: дроби-подходящие — лучшие приближения к иррациональному числу при заданной ограниченности знаменателя.
- Уравнение Пелля: фундаментальное решение находится через период непрерывной дроби .
- Криптоанализ: атака Вистра на RSA использует непрерывные дроби для нахождения малого закрытого показателя.
- Задачи на точные вычисления: представление сравнимых иррациональных через подходящие дроби.
- Задачи на цепные рекуррентности: нахождение рациональной точки на кривой, ближайшей к заданной.
Теория
Конечные непрерывные дроби
Определение. Конечной непрерывной дробью называется выражение вида: где , при .
Теорема. Каждое рациональное число имеет ровно два представления в виде конечной непрерывной дроби: с и .
Алгоритм разложения. Для рационального ():
- , (дробная часть)
- Если , конец.
- , , , ...
По существу, это алгоритм Евклида:
Пример: . Проверка: , . ✓
Подходящие дроби
Определение. -я подходящая дробь (конвергент) числа определяется как:
Рекуррентные формулы.
Доказательство. Индукция по . База: . Шаг: . Применяя рекуррентное соотношение с заменённым на :
Теорема (основные свойства подходящих дробей).
- (тождество Кэши).
- (числитель и знаменатель взаимно просты).
- Подходящие дроби чётного номера монотонно возрастают, нечётного — убывают.
- Все подходящие дроби «зажимают» значение : .
Доказательство (п.1). Индукция: при : . При переходе :
Теорема (наилучшее приближение). Пусть — иррациональное число. Подходящая дробь является наилучшим приближением к из всех дробей со знаменателем : для любого рационального с и :
Бесконечные непрерывные дроби
Теорема. Каждое иррациональное число представляется единственной бесконечной непрерывной дробью.
Алгоритм разложения иррационального числа. Для :
Периодические непрерывные дроби и квадратичные иррациональности
Теорема Лагранжа. Непрерывная дробь периодична тогда и только тогда, когда число является квадратичной иррациональностью, то есть имеет вид с , не являющимся полным квадратом.
Структура разложения . Разложение имеет вид , где и период начинается с .
Алгоритм разложения . Поддерживаем представление ... точнее, :
- , ,
Период заканчивается, когда .
Лемма. Все — целые числа, .
Доказательство. Индукция: при : . Шаг: . Раскрывая: . Первое слагаемое целое по индукционному предположению, остальные целые.
Уравнение Пелля
Уравнение Пелля: , где — натуральное число, не являющееся полным квадратом.
Теорема (Лагранж). Уравнение Пелля всегда имеет решение в натуральных числах. Если период разложения в непрерывную дробь имеет длину , то:
- При чётном : фундаментальное решение .
- При нечётном : фундаментальное решение .
Здесь — подходящие дроби разложения .
Доказательство (схема). Из свойств подходящих дробей: (доказывается индукцией через рекуррентные соотношения). Когда (конец периода), , и . При чётном (длина периода): , , получаем первое решение. При нечётном : берём , .
Генерация всех решений. Если — фундаментальное решение, то все решения даются формулой:
Или рекуррентно:
Доказательство. Если и , то . Значит произведение двух решений (в кольце ) тоже является решением.
Чтобы убедиться, что формула даёт все решения, используется факт: все решения образуют циклическую группу, порождённую фундаментальным решением .
Связанное уравнение: имеет решение тогда и только тогда, когда период разложения нечётный. Минимальное решение: .
Обобщённое уравнение Пелля: при решается через подходящие дроби. Решение существует тогда и только тогда, когда для некоторого .
Ключевые формулы (сводная таблица)
| Объект | Формула |
|---|---|
| Рекуррентность числителя | , |
| Рекуррентность знаменателя | , |
| Тождество Кэши | |
| Шаг разложения | , |
| Фунд. решение Пелля (чётный период) | |
| Фунд. решение Пелля (нечётный период) | |
| Генерация решений Пелля | , |
| Проверка решения Пелля |
Реализация на C++20 (с анализом сложности)
#include <bits/stdc++.h> using namespace std; typedef long long ll; typedef unsigned long long ull; typedef __int128 lll; // ─── Структура для работы с большими числами через __int128 ──────────────── // Для задач Пелля числа могут быть очень большими — используем __int128 // при D до 10^6, или Python-style big int при бОльших значениях. // ─── Разложение рационального p/q в непрерывную дробь ───────────────────── // Возвращает коэффициенты [a0; a1, ..., an] // Сложность: O(log(max(p, q))) — как алгоритм Евклида vector<ll> rational_cf(ll p, ll q) { vector<ll> cf; while (q != 0) { cf.push_back(p / q); p %= q; swap(p, q); } return cf; } // ─── Вычисление подходящих дробей по коэффициентам ───────────────────────── // Возвращает вектор пар (p_k, q_k) // Сложность: O(n) vector<pair<ll,ll>> convergents(const vector<ll>& cf) { vector<pair<ll,ll>> conv; ll p_prev = 1, p_curr = cf[0]; ll q_prev = 0, q_curr = 1; conv.push_back({p_curr, q_curr}); for (int i = 1; i < (int)cf.size(); i++) { ll p_next = cf[i] * p_curr + p_prev; ll q_next = cf[i] * q_curr + q_prev; conv.push_back({p_next, q_next}); p_prev = p_curr; p_curr = p_next; q_prev = q_curr; q_curr = q_next; } return conv; } // ─── Разложение sqrt(D) в непрерывную дробь ─────────────────────────────── // Возвращает (a0, [a1, a2, ..., period]) // Сложность: O(sqrt(D)) — длина периода O(sqrt(D)) struct SqrtCF { ll a0; // целая часть sqrt(D) vector<ll> period; // периодическая часть SqrtCF(ll D) { a0 = (ll)sqrt((double)D); while ((a0+1)*(a0+1) <= D) a0++; // корректируем из-за погрешности while (a0*a0 > D) a0--; if (a0 * a0 == D) return; // D — полный квадрат ll m = 0, d = 1, a = a0; do { m = d * a - m; d = (D - m * m) / d; a = (a0 + m) / d; period.push_back(a); } while (a != 2 * a0); } // Получить k-й коэффициент (0-индексированный среди всех) // a_0 = a0, a_k = period[(k-1) % period.size()] при k >= 1 ll get(ll k) const { if (k == 0) return a0; if (period.empty()) return -1; // D — квадрат return period[(k - 1) % period.size()]; } }; // ─── Решение уравнения Пелля x^2 - D*y^2 = 1 ───────────────────────────── // Возвращает фундаментальное решение (x1, y1) // При D — полный квадрат, решений нет (кроме x=1, y=0) // Сложность: O(sqrt(D)) — поиск периода pair<lll, lll> pell_fundamental(ll D) { ll a0 = (ll)sqrt((double)D); while ((a0+1)*(a0+1) <= D) a0++; while (a0*a0 > D) a0--; if (a0 * a0 == D) return {1, 0}; // D — квадрат // Разложение sqrt(D) ll m = 0, d = 1, a = a0; lll p_prev = 1, p_curr = a0; lll q_prev = 0, q_curr = 1; for (int iter = 0; iter < 1000000; iter++) { m = d * a - m; d = (D - m * m) / d; a = (a0 + m) / d; lll p_next = a * p_curr + p_prev; lll q_next = a * q_curr + q_prev; p_prev = p_curr; p_curr = p_next; q_prev = q_curr; q_curr = q_next; // Проверяем x^2 - D*y^2 = 1 if (p_prev * p_prev - (lll)D * q_prev * q_prev == 1) { return {p_prev, q_prev}; } // Конец периода: a == 2*a0 // Следующая итерация продолжит до второго конца периода если нужно } return {-1, -1}; // не должно происходить } // ─── Структура для уравнения Пелля (все решения) ────────────────────────── struct PellSolver { ll D; lll x1, y1; // фундаментальное решение PellSolver(ll D) : D(D) { auto [fx, fy] = pell_fundamental(D); x1 = fx; y1 = fy; } // Генерация n-го решения pair<lll, lll> nth(int n) const { lll xn = 1, yn = 0; lll base_x = x1, base_y = y1; // Быстрое возведение в степень в кольце Z[sqrt(D)] int e = n; while (e > 0) { if (e & 1) { lll new_x = xn * base_x + (lll)D * yn * base_y; lll new_y = xn * base_y + yn * base_x; xn = new_x; yn = new_y; } lll new_bx = base_x * base_x + (lll)D * base_y * base_y; lll new_by = 2 * base_x * base_y; base_x = new_bx; base_y = new_by; e >>= 1; } return {xn, yn}; } // Итератор решений: генерируем (x_{n+1}, y_{n+1}) из (x_n, y_n) pair<lll, lll> next_solution(lll xn, lll yn) const { return {x1 * xn + (lll)D * y1 * yn, x1 * yn + y1 * xn}; } }; // ─── Наилучшие приближения: найти все (p, q) с q ≤ Q, p/q ≈ x ───────────── // Возвращает вектор подходящих дробей с q ≤ Q // Для x = p0/q0 (рациональное) vector<pair<ll,ll>> best_approximations(ll p0, ll q0, ll Q) { auto cf = rational_cf(p0, q0); auto conv = convergents(cf); vector<pair<ll,ll>> result; for (auto [p, q] : conv) { if (q <= Q) result.push_back({p, q}); } return result; } // ─── Демонстрация ────────────────────────────────────────────────────────── int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); // 1. Разложение рационального числа { ll p = 355, q = 113; auto cf = rational_cf(p, q); cout << p << "/" << q << " = ["; for (int i = 0; i < (int)cf.size(); i++) { if (i) cout << "; "; cout << cf[i]; } cout << "]\n"; auto conv = convergents(cf); for (auto [pp, qq] : conv) cout << " " << pp << "/" << qq << "\n"; } // 2. Разложение sqrt(D) { for (ll D : {2, 3, 5, 6, 7, 13, 23}) { SqrtCF scf(D); cout << "sqrt(" << D << ") = [" << scf.a0 << "; ("; for (int i = 0; i < (int)scf.period.size(); i++) { if (i) cout << ","; cout << scf.period[i]; } cout << ")]\n"; } } // 3. Уравнение Пелля { for (ll D : {2, 3, 5, 6, 7, 13, 23, 61}) { PellSolver ps(D); // Выводим через __int128 (приходится через строку) auto print128 = [](lll n) { if (n < 0) { cout << "-"; n = -n; } string s; if (n == 0) { cout << "0"; return; } while (n > 0) { s += ('0' + (int)(n%10)); n /= 10; } reverse(s.begin(), s.end()); cout << s; }; cout << "x^2 - " << D << "*y^2 = 1: x1="; print128(ps.x1); cout << ", y1="; print128(ps.y1); cout << ", verify: "; print128(ps.x1*ps.x1 - (lll)D*ps.y1*ps.y1); cout << "\n"; } } // 4. Генерация нескольких решений уравнения Пелля для D=2 { PellSolver ps(2); cout << "\nSolutions of x^2 - 2y^2 = 1:\n"; lll x = 1, y = 0; for (int i = 0; i < 8; i++) { auto [nx, ny] = ps.next_solution(x, y); x = nx; y = ny; cout << " (" << (ll)x << ", " << (ll)y << ")\n"; } } return 0; }
Анализ сложности:
| Операция | Время | Память |
|---|---|---|
| Разложение в НД | ||
| Подходящие дроби | ||
| Разложение | — длина периода | |
| Фундаментальное решение Пелля | ||
| -е решение Пелля | (умножение в ) |
Разбор задачи 1 (простая)
Задача. Дано натуральное число . Найти дробь (), ближайшую к .
Решение. Используем подходящие дроби разложения .
Подходящие дроби:
Генерируем подходящие дроби, пока знаменатель , и берём последнюю.
#include <bits/stdc++.h> using namespace std; typedef long long ll; int main(){ ll n; cin >> n; // sqrt(2) = [1; 2, 2, 2, ...] ll p_prev = 1, p_curr = 1; // p_{-1}=1, p_0=1 ll q_prev = 0, q_curr = 1; // q_{-1}=0, q_0=1 // Первый коэффициент a_0 = 1 // Все последующие a_k = 2 while (true) { ll p_next = 2 * p_curr + p_prev; ll q_next = 2 * q_curr + q_prev; if (q_next > n) break; p_prev = p_curr; p_curr = p_next; q_prev = q_curr; q_curr = q_next; } cout << p_curr << "/" << q_curr << "\n"; // Погрешность: |sqrt(2) - p/q| < 1/(q * q_next) return 0; }
Пример: . Последняя подходящая дробь с : (т.к. ). Погрешность: .
Разбор задачи 2 (средняя)
Задача. Найти наименьшее натуральное решение уравнения .
Решение. Применяем теорему Лагранжа.
Шаг 1: Разложение :
- (т.к. )
- , ,
- , ,
- , ,
- , ,
- , ,
Период: , длина (нечётная).
Подходящие дроби:
Нечётный период: фундаментальное решение — ... но используем прямую проверку:
При нечётном : , . Проверяем: . Значит решает .
Для уравнения : берём -й конвергент, или применяем .
Проверка: . ✓
#include <bits/stdc++.h> using namespace std; typedef long long ll; typedef __int128 lll; void print128(lll n) { if (n == 0) { cout << "0"; return; } string s; while (n > 0) { s += ('0' + (int)(n%10)); n /= 10; } reverse(s.begin(), s.end()); cout << s; } int main(){ ll D; cin >> D; ll a0 = (ll)sqrt((double)D); while ((a0+1)*(a0+1) <= D) a0++; while (a0*a0 > D) a0--; if (a0*a0 == D) { cout << "D is a perfect square\n"; return 0; } ll m = 0, d = 1, a = a0; lll p_prev = 1, p_curr = a0; lll q_prev = 0, q_curr = 1; for (int iter = 0; iter < 2000000; iter++) { m = d * a - m; d = (D - m * m) / d; a = (a0 + m) / d; lll p_next = a * p_curr + p_prev; lll q_next = a * q_curr + q_prev; p_prev = p_curr; p_curr = p_next; q_prev = q_curr; q_curr = q_next; lll check = p_prev * p_prev - (lll)D * q_prev * q_prev; if (check == 1) { cout << "x1 = "; print128(p_prev); cout << ", y1 = "; print128(q_prev); cout << "\nVerify: x^2 - " << D << "*y^2 = "; print128(check); cout << "\n"; return 0; } if (check == -1) { cout << "x^2 - " << D << "y^2 = -1 has solution: "; cout << "("; print128(p_prev); cout << ", "; print128(q_prev); cout << ")\n"; // Продолжаем ещё один период для = 1 } } return 0; }
Разбор задачи 3 (сложная — Div.1 D / ICPC WF)
Задача. Дано и (, ). Найти -е решение уравнения по модулю .
Источник: Подобные задачи встречаются на Codeforces Div.1 D и ICPC Regional.
Решение.
- Находим фундаментальное решение через разложение .
- Используем быстрое возведение в степень в кольце для вычисления .
Умножение в : .
#include <bits/stdc++.h> using namespace std; typedef long long ll; typedef __int128 lll; const ll MOD = 1e9 + 7; // Кольцо Z[sqrt(D)] mod MOD struct ZSqrtD { ll a, b; // a + b*sqrt(D) ll D; ZSqrtD(ll a, ll b, ll D) : a(a%MOD), b(b%MOD), D(D) {} ZSqrtD operator*(const ZSqrtD& o) const { ll na = ((__int128)a*o.a + (__int128)D*b%MOD*o.b) % MOD; ll nb = ((__int128)a*o.b + (__int128)b*o.a) % MOD; return {na, nb, D}; } }; ZSqrtD power(ZSqrtD base, ll exp) { ZSqrtD result{1, 0, base.D}; for (; exp; exp >>= 1) { if (exp & 1) result = result * base; base = base * base; } return result; } // Нахождение фундаментального решения Пелля через __int128 pair<lll, lll> pell_fund(ll D) { ll a0 = (ll)sqrt((double)D); while ((a0+1)*(a0+1) <= D) a0++; while (a0*a0 > D) a0--; if (a0*a0 == D) return {1, 0}; ll m = 0, d = 1, a = a0; lll pp = 1, pc = a0, qp = 0, qc = 1; for (int iter = 0; iter < 4000000; iter++) { m = d*a - m; d = (D - m*m)/d; a = (a0+m)/d; lll pn = a*pc+pp, qn = a*qc+qp; pp=pc; pc=pn; qp=qc; qc=qn; if (pp*pp - (lll)D*qp*qp == 1) return {pp, qp}; } return {-1,-1}; } int main(){ ll D, N; cin >> D >> N; auto [x1, y1] = pell_fund(D); if (x1 == -1) { cout << "No solution\n"; return 0; } // x1 и y1 могут быть большими — берём mod MOD ll x1m = (ll)(x1 % MOD); ll y1m = (ll)(y1 % MOD); // (x1 + y1*sqrt(D))^N mod MOD ZSqrtD base{x1m, y1m, D % MOD}; ZSqrtD res = power(base, N); cout << "x_" << N << " = " << res.a << " (mod " << MOD << ")\n"; cout << "y_" << N << " = " << res.b << " (mod " << MOD << ")\n"; return 0; }
Важный нюанс. При очень большом (до ) или большом периоде фундаментального решения числа могут быть астрономически большими, и для взятия по модулю нужно работать с ними непосредственно в алгоритме поиска (храня , и в __int128 для проверки).
Задачи для самостоятельного решения
| Задача | Источник | Сложность |
|---|---|---|
| Pell Equation | Library Checker | ★★☆ |
| Best rational approximation | Codeforces 977F | ★★★ |
| Continued Fraction | SPOJ CFRAC | ★★☆ |
| Pell | Codeforces 364E | ★★★ |
| Большие решения Пелля по модулю | Codeforces 504E | ★★★ |
| Generalized Pell | ICPC NEERC 2017 | ★★★★ |
| Approximation on Stern-Brocot tree | Codeforces 1603E | ★★★★ |
Типичные ошибки
-
Ошибка в начальных условиях рекуррентности. Правильно: , , , . Частая ошибка: начать с , .
-
Потеря точности при вычислении . Стандартный
sqrtв C++ даёт погрешность для больших . Всегда корректируйте:ll a0 = (ll)sqrt((double)D); while ((a0+1)*(a0+1) <= D) a0++; while (a0*a0 > D) a0--; -
Бесконечный цикл при — полный квадрат. Перед алгоритмом Пелля проверьте .
-
Неправильное определение конца периода. Период заканчивается при , но это может произойти на первом же шаге (например, — нет, ).
-
Неучёт нечётного периода. Если длина периода нечётна, подходящая дробь решает , а не . Для уравнения нужно взять .
-
Переполнение в проверке. Числа растут экспоненциально. При фундаментальное решение может иметь сотни цифр. Используйте
__int128или BigInteger.
Совет профессионала
Атака Вистра на RSA и её применение в CTF. В задачах CTF-стиля (и иногда в олимпиадах) встречается следующий сценарий: даны и (публичный ключ RSA), причём (малый секретный показатель). Тогда из соотношения следует , то есть является подходящей дробью разложения в непрерывную дробь. Перебирая подходящие дроби и проверяя , , находим факторизацию.
// Атака Вистра (концептуально) auto cf = rational_cf(e, N); auto conv = convergents(cf); for (auto [k, d] : conv) { if (k == 0) continue; if ((e * d - 1) % k == 0) { ll phi = (e * d - 1) / k; // N = pq, phi = (p-1)(q-1) = N - (p+q) + 1 // p + q = N - phi + 1 // дискриминант: (p+q)^2 - 4N должен быть полным квадратом ll s = N - phi + 1; ll disc = s*s - 4*N; if (disc > 0) { ll sq = (ll)sqrt((double)disc); if (sq*sq == disc) { cout << "p = " << (s + sq)/2 << ", q = " << (s - sq)/2 << "\n"; } } } }
Итог
Непрерывные дроби — элегантный мост между числами рациональными, иррациональными и целыми диофантовыми задачами:
- Разложение в НД вычисляется через алгоритм Евклида за .
- Подходящие дроби дают наилучшие рациональные приближения и вычисляются за линейное время через рекуррентность.
- Периодические НД характеризуют квадратичные иррациональности; разложение вычисляется за .
- Уравнение Пелля всегда имеет решения (при не квадрате), которые выражаются через подходящие дроби .
- Все решения уравнения Пелля порождаются фундаментальным через умножение в кольце , а -е решение вычисляется за .