7.2 Integer relations: finding a minimal polynomial with LLL
Given a few decimal digits of a number, recover the exact polynomial it satisfies.
Suppose you computed and suspect it is algebraic. Is there a polynomial with small integer coefficients that vanishes at ? This is an integer relation problem, and it is the first embedding in this series where a single choice, the scaling factor , decides between the right answer and garbage. We follow the recipe from 7.1 Embedding basics and solve it with LLL from g6k-rs.
The problem
Given real numbers , find integers , not all zero and preferably small, with
For a minimal polynomial we take . A relation is then a polynomial with . We only know the to finite precision, so the relation holds approximately in our data and exactly for the true numbers.
Building the lattice
Step 1 of the recipe asks for the small object: the coefficient vector . Step 2 writes the relation as a linear form, , which should be zero. Real numbers cannot go into an integer lattice, so we scale them by a large integer and round. Using the row convention (rows of are basis vectors), row is the unit vector followed by one extra coordinate:
The integer combination with coefficients is the lattice vector
For the true relation, , so is only the accumulated rounding error: when the are exact. A vector that is not a relation has , which is large. The last column acts as a penalty that makes every non-relation long. The target norm is
How large must N be?
The lattice has rank in ambient dimension . It is not full rank, so its determinant is . The Gram matrix is the identity plus a rank-one term, which gives
Step 5 of the recipe compares the target with the Gaussian heuristic:
Read the right side as a rule of thumb: the number of digits of N grows linearly with the degree, roughly . Doubling the degree roughly doubles the digits you need. This is a heuristic for random-looking lattices, not a proof, and the examples below show it is a useful but not sharp guide.
Digits rule
Each entry must be correct to within about one unit, so you need the to about digits after the decimal point. If you only know itself, the error in is amplified by about , so add digits on top. A larger without more digits only scales noise.
Example 1: √2 + √3, degree 4
The number satisfies , so the target is with . Here and , so the rule above gives . The program below builds the basis in f64 and runs LLL for several values of . Add this to Cargo.toml:
[dependencies]
g6k-rs = "=0.1.0"
# only needed for the high-precision example
rug = { version = "1.26", default-features = false, features = ["integer", "float"] }// Recover the minimal polynomial of alpha = sqrt(2) + sqrt(3) with LLL.
// Rows of the basis are lattice vectors: row i = (e_i | round(N * alpha^i)).
use g6k_rs::{LLLParams, LatticeBasis, lll_reduce};
fn main() {
let alpha = 2f64.sqrt() + 3f64.sqrt();
let degree = 4;
let dim = degree + 1;
for exp in [2, 4, 6, 8] {
let n_scale = 10f64.powi(exp);
let rows: Vec<Vec<i64>> = (0..dim)
.map(|i| {
let mut row = vec![0i64; dim + 1];
row[i] = 1;
row[dim] = (n_scale * alpha.powi(i as i32)).round() as i64;
row
})
.collect();
let mut basis = LatticeBasis::from_integers(&rows);
let result = lll_reduce(&mut basis, &LLLParams::default());
// The first reduced row is the shortest vector found: (c_0, ..., c_4, residual).
let v: Vec<i64> = basis.basis[0].iter().map(|&x| x.round() as i64).collect();
println!(
"N = 1e{exp} converged = {:<5} c = {:?} residual = {}",
result.converged, &v[..dim], v[dim]
);
}
}Output with g6k-rs 0.1.0:
N = 1e2 converged = true c = [-1, 2, -1, -3, 1] residual = -3
N = 1e4 converged = true c = [1, 0, -10, 0, 1] residual = -2
N = 1e6 converged = true c = [1, 0, -10, 0, 1] residual = 5
N = 1e8 converged = false c = [1, 0, 0, 0, 0] residual = 100000000The table adds from the same program run on a finer grid (10f64.powf(exp)), with the heuristic values computed from the formula above.
| N | First reduced vector | Is it the minimal polynomial? | ||
|---|---|---|---|---|
| 3.4 | 0.34 | (−1, 2, −1, −3, 1), r = −3 | No: norm 5 beats the target | |
| 6.9 | 0.68 | (1, 4, 4, −8, 2), r = 0 | No: norm √101 < √102 | |
| 8.6 | 0.85 | (1, 0, −10, 0, 1), r = −2 | Yes | |
| 21.7 | 2.15 | (1, 0, −10, 0, 1), r = 5 | Yes | |
| 54.5 | 5.39 | unreduced row | LLL refused: converged = false |
Three lessons are in that table. For small the penalty column is too cheap, and LLL correctly returns a vector shorter than our target: is small but not zero, and at that is good enough to look like a relation. Near the heuristic threshold the answer appears (here already at , a bit below the predicted , which is normal for a heuristic). And at the floating-point reducer gives up.
Why f64 stops working
The floating-point LatticeBasis stores entries as f64 and computes the Gram–Schmidt data in double precision. With the last column holds values near , squared lengths near , while the Gram–Schmidt lengths we care about are about 1. Double precision keeps only about 16 significant digits, so those small quantities vanish in cancellation. g6k-rs detects the non-positive Gram–Schmidt length and returns the basis unreduced with converged = false instead of a wrong answer. Always check that flag.
With f64 the working window for this example is roughly . That window closes as the degree grows, because the lower end rises with the degree and the upper end is fixed by double precision. For anything beyond a toy, use exact integers.
Example 2: 2^(1/3) + 3^(1/3), degree 9, with exact integers
Let . Cubing gives , and eliminating the cube root gives , that is
The target has norm , the rank is , and . The heuristic now asks for , and the largest entry is around . That no longer fits in an i64 (maximum about ) and is far beyond the integers that f64 represents exactly. We switch to MpzLatticeBasis, which stores GMP integers and runs LLL with exact row operations, and compute with MPFR at 256 bits.
// Degree-9 integer relation with exact integers: alpha = 2^(1/3) + 3^(1/3).
// The scaled column needs about 24 digits, beyond i64 and f64, so use MpzLatticeBasis.
use g6k_rs::{MpzLatticeBasis, MpzLllOptions, mpz_lll_reduce_with_options};
use rug::ops::Pow;
use rug::{Float, Integer};
fn main() {
let prec = 256; // bits of working precision for alpha, about 77 decimal digits
let alpha = Float::with_val(prec, 2u32).cbrt() + Float::with_val(prec, 3u32).cbrt();
let degree = 9;
let dim = degree + 1;
for digits in [15u32, 18, 20, 22, 24] {
let n_scale = Float::with_val(prec, Integer::from(10u32).pow(digits));
let basis_rows: Vec<Vec<Integer>> = (0..dim)
.map(|i| {
let mut row = vec![Integer::new(); dim + 1];
row[i] = Integer::from(1);
let scaled = Float::with_val(prec, &n_scale * alpha.clone().pow(i as u32));
row[dim] = scaled.round().to_integer().unwrap();
row
})
.collect();
let mut basis = MpzLatticeBasis { rows: dim, cols: dim + 1, basis: basis_rows };
let result = mpz_lll_reduce_with_options(&mut basis, MpzLllOptions::default());
let v = &basis.basis[0];
println!(
"N = 1e{digits} converged = {} c = {:?} residual = {}",
result.converged,
v[..dim].iter().map(|x| x.to_i64().unwrap_or(i64::MAX)).collect::<Vec<_>>(),
v[dim]
);
}
}Output with g6k-rs 0.1.0:
N = 1e15 converged = true c = [-27, -19, 37, -11, -25, 1, 4, -45, -24, 15] residual = -18
N = 1e18 converged = true c = [-125, 0, 0, -87, 0, 0, -15, 0, 0, 1] residual = 23
N = 1e20 converged = true c = [-125, 0, 0, -87, 0, 0, -15, 0, 0, 1] residual = 23
N = 1e22 converged = true c = [-125, 0, 0, -87, 0, 0, -15, 0, 0, 1] residual = 25
N = 1e24 converged = true c = [-125, 0, 0, -87, 0, 0, -15, 0, 0, 1] residual = 4At (planting ratio ) LLL returns a shorter spurious relation. From on (ratio about 0.78 and rising) the minimal polynomial appears. Again the true threshold sits a little below the heuristic one, and again the safe choice is a margin above it.
When you only know some digits
In the program above is known to 77 digits. In practice the input is a measured or computed value with limited accuracy. Truncating to decimal digits and fixing gives:
| Known digits of α | Result at N = 10²² |
|---|---|
| 18, 20, 22 | Spurious relations such as (1, −145, 112, −7, −196, 204, −37, 39, 146, −60) |
| 24, 26, 30 | (−125, 0, 0, −87, 0, 0, −15, 0, 0, 1) |
This matches the digits rule: plus suggests about 26 digits, and the relation appears from 24. With fewer digits, rounding noise in the last column is large enough that LLL finds vectors that fit the noise.
Checking the answer
LLL always returns some short vector, so a result is a candidate, not a proof. Evaluate it at much higher precision than you used to find it. A genuine relation drops to the new precision limit; a spurious one stays at the level of the old noise:
from decimal import Decimal as D, getcontext
getcontext().prec = 60
alpha = D(2) ** (D(1) / 3) + D(3) ** (D(1) / 3)
for c in ([-125, 0, 0, -87, 0, 0, -15, 0, 0, 1], # found with 24 known digits
[1, -145, 112, -7, -196, 204, -37, 39, 146, -60]): # found with 22 known digits
print(c, "%.3E" % sum(D(ci) * alpha**i for i, ci in enumerate(c)))
# [-125, 0, 0, -87, 0, 0, -15, 0, 0, 1] -2.000E-56
# [1, -145, 112, -7, -196, 204, -37, 39, 146, -60] -9.411E-18Two further checks turn a candidate into a statement about the minimal polynomial. The candidate must be irreducible over the integers (any computer algebra system can factor it), and you need an independent argument that is algebraic of at most that degree, like the cube-root elimination above. The lattice alone only says that no relation shorter than a certain bound was found.
PSLQ, the classical alternative
The PSLQ algorithm of Ferguson and Bailey solves the same problem without choosing : it works directly with the real numbers and, run with enough precision, finds a relation or proves a lower bound on the norm of any relation. It is the standard tool in experimental mathematics (mpmath ships pslq and findpoly). The lattice approach is worth knowing because it generalizes: the same penalty-column construction handles several equations at once, weighted coordinates, and extra congruence constraints, which is where the next guides go.
Common mistakes
- N too small. The penalty column becomes cheap, and LLL returns a genuine shorter vector that is not a relation. Use the heuristic threshold plus a margin.
- N larger than your digits support. Scaling by numbers that are only correct to 18 digits multiplies noise, not information.
- Floating-point overflow of precision.
f64bases break once squared entries exceed about times the small Gram–Schmidt lengths. Checkconvergedand move toMpzLatticeBasis. - Forgetting the constant term. Include . Without it, only relations with can be found, and the minimal polynomial of has .
- Trusting the first row blindly. Verify at higher precision and check irreducibility before calling a result the minimal polynomial.
Next, 7.3 Simultaneous Diophantine approximation turns the construction around: instead of one equation among many numbers, it asks for one denominator that fits many numbers at once, and introduces coordinate weights.
Further reading
Lenstra, Lenstra and Lovász, Factoring polynomials with rational coefficients (1982), where LLL is introduced for exactly this kind of problem. Kannan, Lenstra and Lovász, Polynomial factorization and nonrandomness of bits of algebraic and some transcendental numbers (1988), for the precision bounds. Ferguson, Bailey and Arno, Analysis of PSLQ, an integer relation finding algorithm (1999). For the reduction side, see the LLL chapter.