FLINT3.7.0-dev
Tutorial

Decoding Reed–Solomon codes

Reed–Solomon codes [RS1960] protect data against corruption by adding redundant symbols computed from polynomial arithmetic over a finite field. They are used in CDs, DVDs and QR codes over F28\mathbb{F}_{2^8}, and the PDF417 barcode format uses them over the prime field F929\mathbb{F}_{929}, which makes it a natural setting for nmod_poly.h. This tutorial writes a complete encoder and decoder. A codeword of length NN carries KK message symbols and N−K=2TN - K = 2T check symbols, and the decoder corrects any TT or fewer corrupted symbols, without knowing where they are. Along the way we use polynomial division, multipoint evaluation, the Berlekamp–Massey algorithm, root finding and power series arithmetic.

Throughout, pp is a prime and α\alpha an element of order at least NN in Fp×\mathbb{F}_p^\times; we start with p=929p = 929, α=3\alpha = 3 (a primitive root), N=40N = 40, K=24K = 24 and T=8T = 8.

Step 1: encoding#

A word c=(c0,…,cN−1)c = (c_0, \ldots, c_{N-1}) is identified with the polynomial c(x)=∑cixic(x) = \sum c_i x^i. The code consists of the polynomials of degree less than NN that vanish at α,α2,…,α2T\alpha, \alpha^2, \ldots, \alpha^{2T}, that is, the multiples of the generator polynomial

g(x)=(x−α)(x−α2)⋯(x−α2T).g(x) = (x - \alpha)(x - \alpha^2) \cdots (x - \alpha^{2T}).

Two different codewords differ in at least 2T+12T + 1 positions: their difference is a nonzero multiple of gg of degree less than NN, and a polynomial with at most 2T2T nonzero coefficients cannot vanish at 2T2T distinct powers of α\alpha (the corresponding Vandermonde matrix is invertible). So TT errors can be corrected.

In systematic encoding the message m(x)m(x) of length KK appears unchanged in the high coefficients: c=mx2T−(mx2T mod g)c = m x^{2T} - (m x^{2T} \bmod g), which is divisible by gg. FLINT gives us gg with nmod_poly_product_roots_nmod_vec and the remainder with nmod_poly_rem:

Exampletutorial_rs_encode.cchecked by make checkdoc
#include <flint/flint.h>
#include <flint/nmod.h>
#include <flint/nmod_vec.h>
#include <flint/nmod_poly.h>

#define P 929       /* the field GF(929) of PDF417 barcodes */
#define ALPHA 3     /* a primitive root modulo 929 */
#define N 40        /* codeword length */
#define K 24        /* message length */
#define T 8         /* errors that can be corrected: N - K = 2T */

int main(void)
{
    nmod_t mod;
    nmod_poly_t g, m, c;
    ulong pts[2 * T];
    slong i;

    nmod_init(&mod, P);
    nmod_poly_init_mod(g, mod);
    nmod_poly_init_mod(m, mod);
    nmod_poly_init_mod(c, mod);

    /* generator polynomial g = (x - a)(x - a^2)...(x - a^2T) */
    pts[0] = ALPHA;
    for (i = 1; i < 2 * T; i++)
        pts[i] = nmod_mul(pts[i - 1], ALPHA, mod);
    nmod_poly_product_roots_nmod_vec(g, pts, 2 * T);

    /* message: the symbols 1, 2, ..., K */
    for (i = 0; i < K; i++)
        nmod_poly_set_coeff_ui(m, i, i + 1);

    /* systematic encoding: c = m x^2T - (m x^2T mod g) */
    nmod_poly_shift_left(c, m, 2 * T);
    nmod_poly_rem(g, c, g);
    nmod_poly_sub(c, c, g);

    flint_printf("check symbols:");
    for (i = 0; i < 2 * T; i++)
        flint_printf(" %wu", nmod_poly_get_coeff_ui(c, i));
    flint_printf("\n");

    {
        ulong s[2 * T];
        nmod_poly_evaluate_nmod_vec(s, c, pts, 2 * T);
        flint_printf("c(a^i) = 0 for i = 1..2T: %d\n", _nmod_vec_is_zero(s, 2 * T));
    }

    nmod_poly_clear(g);
    nmod_poly_clear(m);
    nmod_poly_clear(c);
    return 0;
}
Output
check symbols: 382 684 808 907 581 514 884 383 661 205 350 74 800 634 107 338
c(a^i) = 0 for i = 1..2T: 1

The check that cc vanishes at α,…,α2T\alpha, \ldots, \alpha^{2T} uses nmod_poly_evaluate_nmod_vec, which for short polynomials evaluates point by point and for long ones uses a subproduct tree (Multipoint evaluation and interpolation).

Step 2: syndromes and the error locator#

Suppose the received word is r=c+er = c + e, where the error ee is nonzero in ν≤T\nu \le T positions e1,…,eνe_1, \ldots, e_\nu with values Y1,…,YνY_1, \ldots, Y_\nu. Write Xk=αekX_k = \alpha^{e_k} for the error locators. Since cc vanishes at the powers of α\alpha, the syndromes

si=r(αi)=e(αi)=∑k=1νYkXki,1≤i≤2T,s_i = r(\alpha^i) = e(\alpha^i) = \sum_{k=1}^{\nu} Y_k X_k^i, \qquad 1 \le i \le 2T,

depend only on the error. All of them are zero if and only if no error is detected.

The sequence sis_i is a sum of ν\nu geometric sequences, so it satisfies the linear recurrence whose characteristic polynomial is

V(x)=(x−X1)(x−X2)⋯(x−Xν),V(x) = (x - X_1)(x - X_2) \cdots (x - X_\nu),

and no shorter one. Finding the shortest recurrence of a sequence is exactly what the Berlekamp–Massey algorithm [Ber1968] [Mas1969] does, and 2ν≤2T2\nu \le 2T terms suffice to determine a recurrence of order ν\nu. FLINT's nmod_berlekamp_massey_t implements it as a partial extended Euclidean algorithm [SKHN1975], switching to the half-GCD algorithm for long sequences, and returns VV (up to a constant factor) with nmod_berlekamp_massey_V_poly. Its documentation explains the second polynomial RR: if deg⁡R≥deg⁡V\deg R \ge \deg V, the recurrence does not explain the whole sequence, which means that more than TT errors occurred.

The error locators are the roots of VV, which are distinct and nonzero. nmod_poly_find_distinct_nonzero_roots finds them by splitting VV with GCDs with (x+δ)(p−1)/2−1(x + \delta)^{(p-1)/2} - 1 [Rab1980b], and fails if VV does not have deg⁡V\deg V distinct roots in Fp\mathbb{F}_p — another sign of too many errors. Classical decoders search all NN powers of α\alpha instead (Chien search [Chi1964]), which is fine for short codes. The position eke_k is the discrete logarithm of XkX_k to the base α\alpha, which we look up in a table.

Step 3: the error values#

With Λ(x)=∏k(1−Xkx)\Lambda(x) = \prod_k (1 - X_k x), the reversal of VV, and the syndrome series S(x)=∑i=02T−1si+1xiS(x) = \sum_{i=0}^{2T-1} s_{i+1} x^i, we have

S(x)≡∑kYkXk1−Xkx(modx2T),Ω(x)=S(x)Λ(x) mod x2T=∑kYkXk∏j≠k(1−Xjx).S(x) \equiv \sum_k \frac{Y_k X_k}{1 - X_k x} \pmod{x^{2T}}, \qquad \Omega(x) = S(x) \Lambda(x) \bmod x^{2T} = \sum_k Y_k X_k \prod_{j \ne k} (1 - X_j x).

Evaluating Ω\Omega and Λ′(x)=−∑kXk∏j≠k(1−Xjx)\Lambda'(x) = -\sum_k X_k \prod_{j \ne k} (1 - X_j x) at Xk−1X_k^{-1} gives Forney's formula [For1965]

Yk=−Ω(Xk−1)Λ′(Xk−1).Y_k = -\frac{\Omega(X_k^{-1})}{\Lambda'(X_k^{-1})}.

In FLINT this is a truncated product (nmod_poly_mullow), a derivative and two evaluations per error. The complete decoder corrects 3 and 6 errors, and detects that 9 errors are too many:

Exampletutorial_rs_decode.cchecked by make checkdoc
#include <flint/flint.h>
#include <flint/nmod.h>
#include <flint/nmod_vec.h>
#include <flint/nmod_poly.h>

#define P 929
#define ALPHA 3
#define N 40
#define K 24
#define T 8

/* Decode the received word r (length N) in place. Returns the number of
   corrected errors, or -1 if more than T errors were detected. */
slong rs_decode(nmod_poly_t r, nmod_t mod, int verbose)
{
    ulong pts[2 * T], s[2 * T], X[T], inv, num, den, logtab[P];
    nmod_berlekamp_massey_t B;
    nmod_poly_t V, Lambda, S, Omega, dLambda;
    slong i, nu, result;

    /* syndromes s_i = r(a^i), i = 1..2T */
    pts[0] = ALPHA;
    for (i = 1; i < 2 * T; i++)
        pts[i] = nmod_mul(pts[i - 1], ALPHA, mod);
    nmod_poly_evaluate_nmod_vec(s, r, pts, 2 * T);
    if (_nmod_vec_is_zero(s, 2 * T))
        return 0;

    /* error locator: minimal polynomial of the syndrome sequence */
    nmod_berlekamp_massey_init(B, mod.n);
    nmod_berlekamp_massey_add_points(B, s, 2 * T);
    nmod_berlekamp_massey_reduce(B);

    nmod_poly_init_mod(V, mod);
    nmod_poly_init_mod(Lambda, mod);
    nmod_poly_init_mod(S, mod);
    nmod_poly_init_mod(Omega, mod);
    nmod_poly_init_mod(dLambda, mod);
    nmod_poly_make_monic(V, nmod_berlekamp_massey_V_poly(B));
    nu = nmod_poly_degree(V);
    result = -1;

    if (verbose)
    {
        flint_printf("  V = "); nmod_poly_print_pretty(V, "x"); flint_printf("\n");
    }

    /* the roots of V are the error locators X_k = a^(e_k) */
    if (nu > T || nmod_poly_degree(nmod_berlekamp_massey_R_poly(B)) >= nu
               || !nmod_poly_find_distinct_nonzero_roots(X, V))
        goto cleanup;

    /* Forney: Lambda = rev(V), Omega = S Lambda mod x^2T,
       Y_k = -Omega(1/X_k) / Lambda'(1/X_k) */
    nmod_poly_reverse(Lambda, V, nu + 1);
    for (i = 0; i < 2 * T; i++)
        nmod_poly_set_coeff_ui(S, i, s[i]);
    nmod_poly_mullow(Omega, S, Lambda, 2 * T);
    nmod_poly_derivative(dLambda, Lambda);

    for (i = 0; i < P; i++)
        logtab[i] = N;                      /* positions >= N are invalid */
    for (i = 0, num = 1; i < N; i++, num = nmod_mul(num, ALPHA, mod))
        logtab[num] = i;

    for (i = 0; i < nu; i++)
    {
        slong e = logtab[X[i]];
        if (e >= N)
            goto cleanup;                   /* locator outside the word */
        inv = nmod_inv(X[i], mod);
        num = nmod_poly_evaluate_nmod(Omega, inv);
        den = nmod_poly_evaluate_nmod(dLambda, inv);
        num = nmod_neg(nmod_div(num, den, mod), mod);
        if (verbose)
            flint_printf("  error %wu at position %wd\n", num, e);
        nmod_poly_set_coeff_ui(r, e,
            nmod_sub(nmod_poly_get_coeff_ui(r, e), num, mod));
    }
    result = nu;

cleanup:
    nmod_berlekamp_massey_clear(B);
    nmod_poly_clear(V);
    nmod_poly_clear(Lambda);
    nmod_poly_clear(S);
    nmod_poly_clear(Omega);
    nmod_poly_clear(dLambda);
    return result;
}

int main(void)
{
    nmod_t mod;
    nmod_poly_t g, c, r;
    ulong pts[2 * T];
    slong i, nerr;
    slong pos[9] = { 3, 7, 11, 17, 22, 25, 30, 38, 39 };
    ulong val[9] = { 100, 1, 928, 5, 77, 300, 2, 40, 9 };

    nmod_init(&mod, P);
    nmod_poly_init_mod(g, mod);
    nmod_poly_init_mod(c, mod);
    nmod_poly_init_mod(r, mod);

    pts[0] = ALPHA;
    for (i = 1; i < 2 * T; i++)
        pts[i] = nmod_mul(pts[i - 1], ALPHA, mod);
    nmod_poly_product_roots_nmod_vec(g, pts, 2 * T);

    for (i = 0; i < K; i++)
        nmod_poly_set_coeff_ui(c, i, i + 1);
    nmod_poly_shift_left(c, c, 2 * T);
    nmod_poly_rem(r, c, g);
    nmod_poly_sub(c, c, r);

    for (nerr = 3; nerr <= 9; nerr += 3)
    {
        nmod_poly_set(r, c);
        for (i = 0; i < nerr; i++)
            nmod_poly_set_coeff_ui(r, pos[i],
                nmod_add(nmod_poly_get_coeff_ui(r, pos[i]), val[i], mod));
        flint_printf("%wd errors:\n", nerr);
        i = rs_decode(r, mod, nerr == 3);
        flint_printf("  decoder returns %wd, codeword restored: %d\n", i, nmod_poly_equal(r, c));
    }

    nmod_poly_clear(g);
    nmod_poly_clear(c);
    nmod_poly_clear(r);
    return 0;
}
Output
3 errors:
  V = x^3+865*x^2+618*x+68
  error 1 at position 7
  error 100 at position 3
  error 928 at position 11
  decoder returns 3, codeword restored: 1
6 errors:
  decoder returns 6, codeword restored: 1
9 errors:
  decoder returns -1, codeword restored: 0

The monic VV printed for three errors is (x−37)(x−33)(x−311)=(x−329)(x−27)(x−637)(x - 3^7)(x - 3^3)(x - 3^{11}) = (x - 329)(x - 27)(x - 637). With more than TT errors, detection is likely but not certain: the received word can also be within distance TT of another codeword, and is then "corrected" to the wrong one. The goto cleanup checks catch the other cases: a locator polynomial of degree above TT, one that does not explain all syndromes, one without enough roots, or a root that is not a position of the word.

Step 4: long codes#

Over F65537\mathbb{F}_{65537}, with α=9=32\alpha = 9 = 3^2 of order 2152^{15}, we can build codes of length N=32768N = 32768. With 2T=40962T = 4096 check symbols the decoder handles 2048 errors, and every step must now be quasi-linear:

Exampletutorial_rs_long.cchecked by make checkdoc
#include <flint/flint.h>
#include <flint/nmod.h>
#include <flint/nmod_vec.h>
#include <flint/nmod_poly.h>

#define P 65537     /* Fermat prime 2^16 + 1 */
#define R 3         /* primitive root; the code uses alpha = R^2 */
#define N 32768     /* order of alpha = 9 */
#define T 2048      /* N - K = 2T */

int main(void)
{
    flint_rand_t state;
    nmod_t mod;
    nmod_poly_t g, c, r, V, Lambda, S, Omega, dLambda;
    nmod_berlekamp_massey_t B;
    nn_ptr pts, s, X, Xinv, num, den;
    slong * logtab;
    slong i, nu;
    ulong alpha, t;

    flint_rand_init(state);
    nmod_init(&mod, P);
    nmod_poly_init_mod(g, mod);
    nmod_poly_init_mod(c, mod);
    nmod_poly_init_mod(r, mod);
    nmod_poly_init_mod(V, mod);
    nmod_poly_init_mod(Lambda, mod);
    nmod_poly_init_mod(S, mod);
    nmod_poly_init_mod(Omega, mod);
    nmod_poly_init_mod(dLambda, mod);
    pts = _nmod_vec_init(2 * T + 1);
    s = _nmod_vec_init(2 * T + 1);
    X = _nmod_vec_init(2 * T);
    Xinv = _nmod_vec_init(2 * T);
    num = _nmod_vec_init(2 * T);
    den = _nmod_vec_init(2 * T);
    logtab = flint_malloc(P * sizeof(slong));

    /* encode a random message */
    alpha = nmod_mul(R, R, mod);
    pts[0] = alpha;
    for (i = 1; i < 2 * T; i++)
        pts[i] = nmod_mul(pts[i - 1], alpha, mod);
    nmod_poly_product_roots_nmod_vec(g, pts, 2 * T);
    nmod_poly_rand(c, state, N - 2 * T);
    nmod_poly_shift_left(c, c, 2 * T);
    nmod_poly_rem(r, c, g);
    nmod_poly_sub(c, c, r);

    /* T errors at distinct random positions */
    nmod_poly_set(r, c);
    for (i = 0; i < T; i++)
    {
        slong e;
        do e = n_randint(state, N);
        while (nmod_poly_get_coeff_ui(r, e) != nmod_poly_get_coeff_ui(c, e));
        t = 1 + n_randint(state, P - 1);
        nmod_poly_set_coeff_ui(r, e, nmod_add(nmod_poly_get_coeff_ui(r, e), t, mod));
    }

    /* syndromes r(alpha^i), i = 0..2T, at a geometric progression */
    nmod_poly_evaluate_geometric_nmod_vec_fast(s, r, R, 2 * T + 1);

    /* error locator from s_1..s_2T */
    nmod_berlekamp_massey_init(B, P);
    nmod_berlekamp_massey_add_points(B, s + 1, 2 * T);
    nmod_berlekamp_massey_reduce(B);
    nmod_poly_set(V, nmod_berlekamp_massey_V_poly(B));
    nu = nmod_poly_degree(V);
    flint_printf("degree of the error locator: %wd\n", nu);

    if (!nmod_poly_find_distinct_nonzero_roots(X, V))
        flint_printf("decoding failure\n");
    else
    {
        /* Forney's formula at all error locators at once */
        nmod_poly_reverse(Lambda, V, nu + 1);
        nmod_poly_fit_length(S, 2 * T);
        _nmod_vec_set(S->coeffs, s + 1, 2 * T);
        _nmod_poly_set_length(S, 2 * T);
        _nmod_poly_normalise(S);
        nmod_poly_mullow(Omega, S, Lambda, 2 * T);
        nmod_poly_derivative(dLambda, Lambda);
        _nmod_vec_invert(Xinv, X, nu, mod);
        nmod_poly_evaluate_nmod_vec(num, Omega, Xinv, nu);
        nmod_poly_evaluate_nmod_vec(den, dLambda, Xinv, nu);

        for (i = 0, t = 1; i < N; i++, t = nmod_mul(t, alpha, mod))
            logtab[t] = i;
        for (i = 0; i < nu; i++)
        {
            slong e = logtab[X[i]];
            ulong y = nmod_neg(nmod_div(num[i], den[i], mod), mod);
            nmod_poly_set_coeff_ui(r, e, nmod_sub(nmod_poly_get_coeff_ui(r, e), y, mod));
        }
        flint_printf("codeword restored: %d\n", nmod_poly_equal(r, c));
    }

    nmod_berlekamp_massey_clear(B);
    nmod_poly_clear(g); nmod_poly_clear(c); nmod_poly_clear(r);
    nmod_poly_clear(V); nmod_poly_clear(Lambda); nmod_poly_clear(S);
    nmod_poly_clear(Omega); nmod_poly_clear(dLambda);
    _nmod_vec_clear(pts); _nmod_vec_clear(s); _nmod_vec_clear(X);
    _nmod_vec_clear(Xinv); _nmod_vec_clear(num); _nmod_vec_clear(den);
    flint_free(logtab);
    flint_rand_clear(state);
    return 0;
}
Output
degree of the error locator: 2048
codeword restored: 1

The whole program takes a few tens of milliseconds. A decoder that is quadratic in NN would take seconds.

Going further#

  • The error locator can also be found from the extended Euclidean algorithm applied to x2Tx^{2T} and S(x)S(x), stopped when the remainder has degree less than TT [SKHN1975]; then the remainder is Ω\Omega up to a constant, and Λ\Lambda is the cofactor of SS. nmod_poly_hgcd performs the first half of this computation in one step.
  • Gao's decoder [Gao2003] works directly with the received values in the evaluation view of the code, using interpolation (nmod_poly_interpolate_nmod_vec_fast) and a partial GCD.
  • Erasures, errors at known positions, are handled by multiplying the syndrome series by the polynomial ∏(1−αex)\prod (1 - \alpha^{e} x) of the known positions; then up to 2T2T erasures can be corrected.

References#

  • [RS1960] I. S. Reed and G. Solomon. Polynomial codes over certain finite fields. Journal of the Society for Industrial and Applied Mathematics 8 (1960), 300–304.
  • [Ber1968] E. R. Berlekamp. Algebraic Coding Theory. McGraw-Hill, 1968.
  • [Mas1969] J. L. Massey. Shift-register synthesis and BCH decoding. IEEE Transactions on Information Theory 15 (1969), 122–127.
  • [SKHN1975] Y. Sugiyama, M. Kasahara, S. Hirasawa and T. Namekawa. A method for solving key equation for decoding Goppa codes. Information and Control 27 (1975), 87–99.
  • [Rab1980b] M. O. Rabin. Probabilistic algorithms in finite fields. SIAM Journal on Computing 9 (1980), 273–280.
  • [Chi1964] R. T. Chien. Cyclic decoding procedures for Bose–Chaudhuri–Hocquenghem codes. IEEE Transactions on Information Theory 10 (1964), 357–363.
  • [For1965] G. D. Forney. On decoding BCH codes. IEEE Transactions on Information Theory 11 (1965), 549–557.
  • [Blu1970] L. I. Bluestein. A linear filtering approach to the computation of discrete Fourier transform. IEEE Transactions on Audio and Electroacoustics 18:4 (1970), 451–455. https://doi.org/10.1109/TAU.1970.1162132
  • [Gao2003] S. Gao. A new algorithm for decoding Reed–Solomon codes. In Communications, Information and Network Security, Kluwer, 2003, 55–68.
Cite this page

The FLINT team, “Decoding Reed–Solomon codes,” FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev, 2026. https://flintlib.org/doc/tutorials/algebra/reed-solomon.html (generated 2026-11-02).