Every real has a continued fraction (, then recurse on ); for rationals it is Euclid’s quotient sequence. The convergents (, same for ) are the best rational approximations, and they are exactly the turning points of the Stern–Brocot tree, the binary search tree containing every positive rational once. Quadratic irrationals have periodic expansions, which solve Pell’s equation .
| Task | Tool | Cost |
|---|---|---|
| convergents, best approximation with denominator | continued fraction / Stern–Brocot | |
| smallest fraction with a monotone property, | binary search on the Stern–Brocot tree with run-length jumps | predicate calls |
| simplest fraction strictly between two fractions | Stern–Brocot LCA / continued-fraction comparison | |
| period of ; fundamental solution generates all | (period ) | |
| solvable iff the period length is odd | same |
The library (all with exact i64 arithmetic):
vector<i64> a = continuedFraction(p, q); // p / q = a0 + 1 / (a1 + 1 / (a2 + ...)), q > 0
vector<Frac> c = convergents(a); // c[i] = [a0; a1, ..., ai] in lowest terms
auto [lo, hi] = boundingFractions(p, q, N); // lo <= p / q <= hi, denominators <= N, closest on each side
Frac b = bestApproximation(p, q, N); // closest to p / q with denominator <= N (lower on ties)
auto [no, yes] = fracBinarySearch([&](Frac x) { return x.p * x.p >= 2 * x.q * x.q; }, N);
// yes = smallest fraction with p, q <= N and f true
auto path = sternBrocotEncode(p, q); // runs {('L' | 'R'), count} from the root 1/1
Frac f = sternBrocotDecode(path);
Frac l = sternBrocotLca(p1, q1, p2, q2);
optional<Frac> a = sternBrocotAncestor(p, q, depth); // depth of 1/1 is 0
auto [lo, hi] = sternBrocotRange(p, q); // descendants = fractions strictly between lo and histruct Frac {
i64 p, q;
};
i64 floorDiv(i64 a, i64 b) { return a / b - ((a % b != 0) && ((a < 0) != (b < 0))); }
vector<i64> continuedFraction(i64 p, i64 q) {
vector<i64> a;
while (q != 0) {
i64 t = floorDiv(p, q);
a.push_back(t);
p -= t * q;
swap(p, q);
}
return a;
}
vector<Frac> convergents(const vector<i64>& a) {
vector<Frac> res;
Frac prev{1, 0}, prev2{0, 1};
for (i64 x : a) {
Frac cur{x * prev.p + prev2.p, x * prev.q + prev2.q};
res.push_back(cur);
prev2 = prev;
prev = cur;
}
return res;
}
pair<Frac, Frac> boundingFractions(i64 p, i64 q, i64 N) {
Frac prev{1, 0}, prev2{0, 1};
while (true) {
i64 a = floorDiv(p, q);
if (prev.q > 0 && a > (N - prev2.q) / prev.q) {
i64 t = (N - prev2.q) / prev.q;
Frac c = prev, s{t * prev.p + prev2.p, t * prev.q + prev2.q};
if ((i128)c.p * s.q > (i128)s.p * c.q) swap(c, s);
return {c, s};
}
Frac cur{a * prev.p + prev2.p, a * prev.q + prev2.q};
prev2 = prev;
prev = cur;
p -= a * q;
swap(p, q);
if (q == 0) return {cur, cur};
}
}
Frac bestApproximation(i64 p, i64 q, i64 N) {
auto [lo, hi] = boundingFractions(p, q, N);
// lo and hi are adjacent, so below * hi.q + above * lo.q = q: no overflow.
i128 below = (i128)p * lo.q - (i128)lo.p * q, above = (i128)hi.p * q - (i128)p * hi.q;
return below * hi.q <= above * lo.q ? lo : hi;
}
template <class F>
pair<Frac, Frac> fracBinarySearch(F f, i64 N) {
Frac lo{0, 1}, hi{1, 0};
for (int dir = 0, stuck = 0; stuck < 2; dir ^= 1) {
Frac& a = dir ? hi : lo; // dir 0 moves lo towards hi, dir 1 moves hi towards lo
Frac b = dir ? lo : hi;
i64 tmax = LLONG_MAX;
if (b.p > 0) tmax = min(tmax, (N - a.p) / b.p);
if (b.q > 0) tmax = min(tmax, (N - a.q) / b.q);
auto ok = [&](i64 t) { return t <= tmax && f(Frac{a.p + t * b.p, a.q + t * b.q}) == (dir == 1); };
i64 t = 0, step = 1;
while (ok(t + step)) {
t += step;
step *= 2;
}
while (step > 1) {
step /= 2;
if (ok(t + step)) t += step;
}
a = Frac{a.p + t * b.p, a.q + t * b.q};
stuck = t > 0 ? 0 : stuck + 1;
}
return {lo, hi};
}
// Stern-Brocot tree (root 1/1): the path to p/q as runs of 'L' (smaller) / 'R' (larger) moves.
vector<pair<char, i64>> sternBrocotEncode(i64 p, i64 q) { // p, q >= 1, gcd 1
vector<i64> a = continuedFraction(p, q);
a.back()--;
vector<pair<char, i64>> path;
for (int i = 0; i < (int)a.size(); i++) {
if (a[i] > 0) path.push_back({i % 2 ? 'L' : 'R', a[i]});
}
return path;
}
// walks a path from the root; returns {node, left bound, right bound} (descendants lie strictly between).
array<Frac, 3> sternBrocotWalk(const vector<pair<char, i64>>& path) {
Frac lo{0, 1}, hi{1, 0};
for (auto [c, k] : path) {
if (c == 'R') lo = Frac{lo.p + k * hi.p, lo.q + k * hi.q};
else hi = Frac{hi.p + k * lo.p, hi.q + k * lo.q};
}
return {Frac{lo.p + hi.p, lo.q + hi.q}, lo, hi};
}
Frac sternBrocotDecode(const vector<pair<char, i64>>& path) { return sternBrocotWalk(path)[0]; }
i64 sternBrocotDepth(i64 p, i64 q) {
i64 d = 0;
for (auto [c, k] : sternBrocotEncode(p, q)) d += k;
return d;
}
Frac sternBrocotLca(i64 p1, i64 q1, i64 p2, i64 q2) {
auto a = sternBrocotEncode(p1, q1), b = sternBrocotEncode(p2, q2);
vector<pair<char, i64>> common;
for (int i = 0; i < (int)min(a.size(), b.size()); i++) {
if (a[i].first != b[i].first) break;
common.push_back({a[i].first, min(a[i].second, b[i].second)});
if (a[i].second != b[i].second) break;
}
return sternBrocotDecode(common);
}
optional<Frac> sternBrocotAncestor(i64 p, i64 q, i64 depth) { // the ancestor at depth (root 1/1 = 0)
vector<pair<char, i64>> prefix;
for (auto [c, k] : sternBrocotEncode(p, q)) {
if (depth == 0) break;
prefix.push_back({c, min(k, depth)});
depth -= min(k, depth);
}
if (depth > 0) return nullopt;
return sternBrocotDecode(prefix);
}
pair<Frac, Frac> sternBrocotRange(i64 p, i64 q) { // subtree of p/q = fractions strictly in (first, second)
auto w = sternBrocotWalk(sternBrocotEncode(p, q));
return {w[1], w[2]};
}Pell’s equation
vector<ll> cfSqrt(ll D) { // [a0; period...] of sqrt(D), empty if D is a square
ll a0 = (ll)sqrtl((long double)D); while (a0 * a0 > D) a0--; while ((a0 + 1) * (a0 + 1) <= D) a0++;
if (a0 * a0 == D) return {};
vector<ll> cf{a0}; ll m = 0, d = 1, a = a0;
while (a != 2 * a0) { m = d * a - m; d = (D - m * m) / d; a = (a0 + m) / d; cf.push_back(a); }
return cf; // cf[1..] is one full period (it ends with 2 a0)
}
pair<i128, i128> pellFundamental(ll D) { // least x, y > 0 with x^2 - D y^2 = 1 (values may need i128)
auto cf = cfSqrt(D); int r = sz(cf) - 1; // period length
i128 p0 = 1, q0 = 0, p1 = cf[0], q1 = 1;
for (int i = 1; i < 2 * r; i++) { // the (r-1)-th convergent (or (2r-1)-th if r is odd) solves it
if (p1 * p1 - (i128)D * q1 * q1 == 1) return {p1, q1};
ll a = cf[(i - 1) % r + 1];
i128 p2 = a * p1 + p0, q2 = a * q1 + q0; p0 = p1, q0 = q1, p1 = p2, q1 = q2;
}
return {p1, q1};
}
// all solutions: x_k + y_k sqrt(D) = (x_1 + y_1 sqrt(D))^k, i.e. x_{k+1} = x_1 x_k + D y_1 y_k, y_{k+1} = x_1 y_k + y_1 x_k| Fact | Statement |
|---|---|
| convergent recurrences | , , |
| approximation quality | $\left |
| best approximations | every best approximation of the second kind is a convergent; of the first kind, a convergent or an intermediate fraction |
| Stern–Brocot | mediant insertion; the path to is (last run shortened by one) |
| quadratic irrationals | eventually periodic expansions (Lagrange); with a palindromic body |
| Pell | fundamental solution is the convergent ( even) or ( odd); solvable iff odd |
| Farey neighbours | adjacent in a Farey sequence iff ; the mediant is the simplest fraction between them |
Proof: convergents, the determinant identity, and approximation bounds
The recurrences follow by induction from being a Möbius transformation in its last argument. The determinant identity: , with initial value . Hence consecutive convergents differ by , they alternate around , and lies between and , giving . Legendre’s criterion: if but is not a convergent, place between ; then while both are within and of , which leads to a contradiction (standard algebra). Stern–Brocot: the tree inserts mediants between neighbours with determinant 1, which preserves determinant 1 and lowest terms and reaches every positive rational; descending toward moves right while exceeds the current node, and the runs of equal moves are the continued-fraction digits.
Proof sketch: Pell's equation
The convergents of satisfy where are the denominators of the expansion algorithm, which equal 1 exactly at the ends of periods. So : a solution of for even , of for odd (then squaring, the convergent at index solves ). Every solution with satisfies , so it is a convergent (Legendre), which is why the first period end gives the smallest one. The solutions form a cyclic group under : if were a solution between two consecutive powers of the fundamental , then would be a solution with , contradicting minimality. Solutions grow exponentially, so they overflow 64 bits quickly (use
i128or big integers).