L2 Reduction: LLL Algorithm With Quadratic Complexity
This is part of our series on LLL where we’ve coded
- Original 1982 LLL Reduction paper.
- Lattice reduction in 2-Dimensions.
- Gaussian lattice sieving .
- LLL Applied to Hermite Normal Forms and GCD.
- (we are here): L2 reduction, Floating-Point LLL Reduction With Quadratic Complexity.Subscribe to receive weekly paper implementations.
1.0 Paper Introduction
An LLL Algorithm With Quadratic Complexity (Nguyen & Stehle, 2009)1 introduces a floating-point variation of LLL reduction with quadratic, not cubic complexity.
This quadratic-complexity LLL variant is proven to terminate and is described as a generalization of the 2D Gauss-Lagrange reduction (Nguyen & Stehle, 2009).
The authors improve on the original LLL by:
- Using an exact initial Gram matrix to improve accuracy of Gram-Schmidt orthogonalization (GSO). This is the matrix G=(BTB) where B is the input basis.
- If B is a basis with these column vectors:
- Then the corresponding Gram matrix is:
- Adapting Babai’s nearest plane algorithm to floating-point arithmetic to stabilize size reduction.
(Nguyen & Stehle, 2009) introduce a new size reduction where reduction happens progressively, not all at once.
This new reduction doesn’t require high floating-point accuracy but requires more steps to achieve. Overall, it’s less expensive as it eliminates exact rational arithmetic of the GSO.
We provide the new size-reduction algorithm below:
2.0 Coding L2
Code is available on GitHub. We code an RNG to make our results reproducible across different languages.
In Python, the RNG resembles:
import numpy as np
import math
def NextRandLCG(state):
state[0] = (1664525 * state[0] + 1013904223) & 0xFFFFFFFF
return state[0]
def Matrix_Index2D(i, j, cols):
return i * cols + j
def RandomMatrixLCG(rows, cols, seed, low, high):
out = np.empty(rows * cols, dtype=np.int32)
for i in range(rows):
for j in range(cols):
idx = Matrix_Index2D(i, j, cols)
out[idx] = low + (NextRandLCG(seed) % (high - low + 1))
return out.reshape(rows, cols)The L2 algorithm is proven to terminate and is given below:
2.1 Gram Matrix
The Gram Matrix is the set of all possible inner products (Weisstein, 2026)2. It is found by:
Python code to find the Gram matrix resembles:
def FindGramMatrix(basis):
d, n = B.shape
G = np.zeros((n, n), dtype=object)
for j in range(n):
for i in range(j + 1):
s = sum(B[l, j] * B[l, i] for l in range(d))
G[j, i] = G[i, j] = s
return G
2.2 Initialize Helper Matrices
We init these matrices similar to typical LLL:
A Python function to init and return the largest gram-schmidt coefficient resembles:
def Init_SquareAndCoeffs(squareNorm, gramCoefficients, colIndex, G):
for i in range(colIndex + 1):
squareNorm[i, colIndex] = float(G[i, colIndex])
for j in range(i):
squareNorm[i, colIndex] -= squareNorm[j, colIndex] * gramCoefficients[j, i]
gramCoefficients[i, colIndex] = squareNorm[i, colIndex] / squareNorm[i, i]
largestCoeff = max(abs(gramCoefficients[j, colIndex]) for j in range(colIndex))
return largestCoeff*Note that LLL demands a full-rank matrix. A computational trick for this constraint is to ensure the number of rows is greater or equal to the number of cols.
2.3 Reduction Function
The reduction function works on a basis vector (each column) like this:
For each previous basis column:
- Find the nearest integer to the Gram-Schmidt coefficient.
- Stop if X is zero.
- If X is not zero:
- Update the current column:
- Update the gram matrix dot products to reflect the basis change.
- Update affected gram coefficients.
Python code for the reduction step resembles:
def PerformSizeReduction(colIndex, rows, cols, gramCoefficients, gramMatrix, basis):
for j in range(colIndex - 1, -1, -1):
#Find closest integer
X = math.floor(float(gramCoefficients[j, colIndex]) + 0.5)
if X != 0:
#Update basis
for i in range(rows):
basis[i, colIndex] -= X * basis[i, j]
#Update gram matrix
for i in range(cols):
dot = sum(basis[l, i] * basis[l, colIndex] for l in range(rows))
gramMatrix[colIndex, i] = gramMatrix[i, colIndex] = dot
#Update gram coefficients
for i in range(j):
gramCoefficients[i, colIndex] -= X * gramCoefficients[i, j] Note that size reduction is triggered unconditionally in original LLL.
L2 size reduction happens only when a coefficient in the current column is larger than expected.
2.4 Lovasz Condition Check
Original LLL tracks the Lovasz condition to ensure norms don’t fall fast. L2 has a variant of this condition and the check is triggered if reduction in step 2.3 did not happen.
We define the familiar LLL constants, delta and dd:
delta = 0.99
dd = (delta + 1.0) / 2.0
------ other l2 code ------ If the Lovasz test passes then we move to the next column:
if dd * squareNorm[currentCol-1, currentCol-1] < squareNorm[currentCol, currentCol] + gramCoefficients[currentCol-1, currentCol]**2 * squareNorm[currentCol-1, currentCol-1]:
currentCol += 1
else:
#Handle case lovasz failedIf it fails then we swap adjacent columns, recompute Gram Schmidt information and decrease the current column by 1:
Python code resembles:
else:
#Swap columns
B[:, [currentCol-1, currentCol]] = B[:, [currentCol, currentCol-1]]
#Recompute Gram schmidt values after swap
for m in [currentCol-1, currentCol]:
for i in range(cols):
dot = sum(B[l, i] * B[l, m] for l in range(rows))
G[m, i] = G[i, m] = dot
for col in [currentCol-1, currentCol]:
for i in range(col + 1):
squareNorm[i, col] = float(G[i, col])
for j in range(i):
squareNorm[i, col] -= squareNorm[j, col] * gramCoefficients[j, i]
gramCoefficients[i, col] = squareNorm[i, col] / squareNorm[i, i]
#Decrease to previous column
currentCol -= 1
if currentCol < 1:
currentCol = 12.5 Tying Everything Together
(Stehle, 2009)3 demonstrates how everything works in place. Python code resembles:
We can test it on a basis and compare the norm before and after reduction:
It works!
Subscribe if you made it this far
3.0 Bonus Content
3.1 Precomputing the Gram Matrix
Say you desire to draw your basis vectors from a fixed set. For instance, suppose you have 5 possible column vector in R3:
Say you desire to sample only 3 basis vectors at a time. For instance, your sampler may choose:
Your code would calculate:
Observe that we can precompute the entire Gram matrix and index to generate our desired subset:
That is, our sublattice spans:
and the indexed Gram matrix is:
3.2 Changing Final Vector
Gram-Schmidt is sequential. We can save computations by swapping out the final vector at every sampling stage. Swapping out the first vector is profoundly expensive.
Say we LLL reduced the basis:
Matrix uu and rr resemble:
Then we swap the final column vector to obtain:
Our new uu and rr resemble:
Observe that only the final column changes.
References
Nguyen, P. Q., & Stehlé, D. (2009). An LLL Algorithm With Quadratic Complexity. SIAM Journal on Computing, 39(3), 874–903. https://doi.org/10.1137/070705702
Weisstein, E. W. (n.d.). Gram Matrix. MathWorld—A Wolfram Web Resource. https://mathworld.wolfram.com/GramMatrix.html
Stehlé, D. (2009). Floating-Point LLL: Theoretical and Practical Aspects. In: Nguyen, P., Vallée, B. (eds) The LLL Algorithm. Information Security and Cryptography. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-642-02295-1_5