Convolutions beyond the 1D sum rule: 2D, digit-wise in any base (generalised XOR / OR / AND), arbitrary-length DFT and evaluation at a geometric progression (chirp-z / Bluestein), with convexity, and arithmetic in . Uses ntt, convolution, Poly, polyMul from NTT.

VariantMethodCost
2D: flatten rows with stride , one 1D product
digit-wise sum mod (base- “XOR”)-point DFT per digit in , reduce by
digit-wise max / min (base- OR / AND)prefix / suffix sums per digit (zeta), multiply, differences
DFT of any length; evaluation at chirp-z: , one convolution
, both convexmerge slopes
, one convexmonotone minima (divide and conquer)
, generalno truly subquadratic algorithm known
polynomials over / carry-less multiplication, GF64 per product

Grid pattern matching with wildcards uses three 2D convolutions of terms, exactly like 1D wildcard matching (Other Matching); an arbitrary modulus replaces convolution with convMod (FFT).

vector<vector<mint>> convolution2D(const vector<vector<mint>>& a, const vector<vector<mint>>& b) {
    if (a.empty() || b.empty() || a[0].empty() || b[0].empty()) return {};
    int n1 = sz(a), m1 = sz(a[0]), n2 = sz(b), m2 = sz(b[0]), N = n1 + n2 - 1, M = m1 + m2 - 1;
    Poly fa(n1 * M), fb(n2 * M);
    for (int i = 0; i < n1; i++) for (int j = 0; j < m1; j++) fa[i * M + j] = a[i][j];
    for (int i = 0; i < n2; i++) for (int j = 0; j < m2; j++) fb[i * M + j] = b[i][j];
    Poly fc = polyMul(fa, fb); fc.resize(N * M);
    vector<vector<mint>> c(N, vector<mint>(M));
    for (int i = 0; i < N; i++) for (int j = 0; j < M; j++) c[i][j] = fc[i * M + j];
    return c;
}
 
Poly chirpZ(const Poly& a, mint z, int m) {               // a(z^k) for k = 0..m-1, O((n+m) log)
    int n = sz(a); if (n == 0 || m == 0) return Poly(m, 0);
    auto tri = [&](ll t) { return z.pow(t * (t - 1) / 2 % 998244352); };   // z^{C(t,2)} (exponent mod p-1, z != 0)
    if (z == mint(0)) { Poly r(m, a[0]); if (m) { r[0] = 0; for (auto x : a) r[0] += x; } return r; }
    Poly A(n), B(n + m - 1);
    for (int j = 0; j < n; j++) A[j] = a[j] / tri(j);        // a_j z^{-C(j,2)}
    for (int t = 0; t < n + m - 1; t++) B[t] = tri(t);       // z^{C(t,2)}
    reverse(all(A));
    Poly C = polyMul(A, B), r(m);
    for (int k = 0; k < m; k++) r[k] = C[n - 1 + k] / tri(k);   // sum_j a_j z^{C(j+k,2) - C(j,2) - C(k,2)}
    return r;
}
DFT lengthUse
power of twoCooley–Tukey directly
smooth (small prime factors)mixed-radix Cooley–Tukey
coprimeGood–Thomas
primeRader (cyclic convolution over ) or Bluestein
arbitrary, or a partial spectrum / zoom FFTBluestein (chirp-z): about 3–6× a native transform

In contests you rarely need a DFT of a fixed non-power-of-two length (zero-padding a linear convolution is simpler); chirp-z earns its place for cyclic convolution of exact length when wraparound matters, and for evaluating at a geometric progression (“evaluate at powers of ” problems, -analogues), which arbitrary multipoint evaluation (Evaluation and Interpolation) does only in .

Digit-wise convolutions

For indices written in base : where adds digits mod without carry (: XOR), and the digit-wise max / min versions (: OR / AND). The base- DFT needs a primitive -th root of unity; instead of requiring , compute in with (multiplying by is a cyclic shift) and reduce the final values modulo the cyclotomic polynomial , where is a genuine primitive -th root; each result is a constant there. Examples: CodeChef MDSWIN (ternary), XORTREEH (base with powers).

template <class Z> struct DigitRing {                      // Z[x] / (x^k - 1), reduced modulo Phi_k at the end
    int k; vector<ll> phi;                                  // Phi_k, monic, integer coefficients
    explicit DigitRing(int base) : k(base) {
        auto cyclo = [&](auto&& self, int n) -> vector<ll> {
            vector<ll> p(n + 1, 0); p[0] = -1, p[n] = 1;    // x^n - 1, divided by Phi_d for d | n, d < n
            for (int d = 1; d < n; d++) if (n % d == 0) {
                auto q = self(self, d); int dq = sz(q) - 1; vector<ll> res(sz(p) - dq, 0);
                for (int i = sz(p) - 1; i >= dq; i--) { ll c = p[i]; res[i - dq] = c; for (int j = 0; j <= dq; j++) p[i - dq + j] -= c * q[j]; }
                p = res;
            }
            return p;
        };
        phi = cyclo(cyclo, k);
    }
    vector<Z> mul(const vector<Z>& a, const vector<Z>& b) const {
        vector<Z> c(k, Z(0)); for (int i = 0; i < k; i++) if (a[i] != Z(0)) for (int j = 0; j < k; j++) c[(i + j) % k] += a[i] * b[j]; return c;
    }
    Z reduce(vector<Z> a) const {
        int d = sz(phi) - 1;
        for (int i = k - 1; i >= d; i--) { Z c = a[i]; if (c == Z(0)) continue; for (int j = 0; j <= d; j++) a[i - d + j] -= c * Z(phi[j]); }
        return a[0];
    }
    void transform(vector<vector<Z>>& a, bool inverse) const {
        int n = sz(a); vector<vector<Z>> tmp(k, vector<Z>(k));
        for (int len = 1; len < n; len *= k) for (int i = 0; i < n; i += len * k) for (int j = 0; j < len; j++) {
            for (int p = 0; p < k; p++) {
                fill(all(tmp[p]), Z(0));
                for (int q = 0; q < k; q++) {
                    int e = p * q % k; if (inverse) e = (k - e) % k;
                    const auto& src = a[i + j + q * len];
                    for (int c = 0; c < k; c++) tmp[p][(c + e) % k] += src[c];
                }
            }
            for (int p = 0; p < k; p++) a[i + j + p * len] = tmp[p];
        }
    }
};
template <class Z> vector<Z> digitAddPower(vector<Z> a, ll e, int k, const vector<Z>& b = {}) {   // a^e (e >= 1), or a * b if b given
    int n = 1; while (n < max(sz(a), sz(b))) n *= k;
    DigitRing<Z> ring(k);
    auto lift = [&](const vector<Z>& v) { vector<vector<Z>> r(n, vector<Z>(k, Z(0))); for (int i = 0; i < sz(v); i++) r[i][0] = v[i]; ring.transform(r, false); return r; };
    auto A = lift(a);
    if (!b.empty()) { auto B = lift(b); for (int i = 0; i < n; i++) A[i] = ring.mul(A[i], B[i]); }
    else for (int i = 0; i < n; i++) { vector<Z> res(k, Z(0)), base = A[i]; res[0] = 1; for (ll t = e; t > 0; t >>= 1, base = ring.mul(base, base)) if (t & 1) res = ring.mul(res, base); A[i] = res; }
    ring.transform(A, true);
    Z invN = Z(1) / Z(n); vector<Z> c(n);
    for (int i = 0; i < n; i++) c[i] = ring.reduce(A[i]) * invN;
    return c;
}
template <class Z> vector<Z> digitAddConvolution(const vector<Z>& a, const vector<Z>& b, int k) { return digitAddPower(a, 1, k, b); }
template <class Z> vector<Z> digitMaxConvolution(vector<Z> a, vector<Z> b, int k, bool useMin = false) {   // digit-wise max (or min)
    int n = 1; while (n < max(sz(a), sz(b))) n *= k;
    a.resize(n, Z(0)), b.resize(n, Z(0));
    auto zeta = [&](vector<Z>& f, bool inverse) {
        for (int len = 1; len < n; len *= k) for (int i = 0; i < n; i += len * k) for (int j = 0; j < len; j++) {
            if (!useMin) { if (!inverse) for (int p = 1; p < k; p++) f[i + j + p * len] += f[i + j + (p - 1) * len];
                           else for (int p = k - 1; p > 0; p--) f[i + j + p * len] -= f[i + j + (p - 1) * len]; }
            else { if (!inverse) for (int p = k - 2; p >= 0; p--) f[i + j + p * len] += f[i + j + (p + 1) * len];
                   else for (int p = 0; p + 1 < k; p++) f[i + j + p * len] -= f[i + j + (p + 1) * len]; }
        }
    };
    zeta(a, false), zeta(b, false);
    for (int i = 0; i < n; i++) a[i] *= b[i];
    zeta(a, true); return a;
}
template <class Z> vector<Z> digitMinConvolution(const vector<Z>& a, const vector<Z>& b, int k) { return digitMaxConvolution(a, b, k, true); }

(min, +) convolution

. Both convex: is convex and its slope sequence is the merged sorted slopes of and (a Minkowski sum, Minkowski Sum). convex, arbitrary: the optimal for row is monotone in , so divide and conquer on rows with shrinking column ranges (monotone minima / SMAWK) works. Max-plus with concave inputs: negate. Typical use: merging knapsack-like DP layers where one side is convex (items of one kind sorted by value), and products inside Aliens’ trick (Aliens Trick).

template <class T> vector<T> minPlusConvexConvex(const vector<T>& a, const vector<T>& b) {
    int n = sz(a), m = sz(b); if (n == 0 || m == 0) return {};
    vector<T> c(n + m - 1); c[0] = a[0] + b[0];
    for (int k = 1, i = 0, j = 0; k < n + m - 1; k++) {
        if (j == m - 1 || (i < n - 1 && a[i + 1] - a[i] < b[j + 1] - b[j])) i++; else j++;
        c[k] = a[i] + b[j];
    }
    return c;
}
template <class T> vector<T> minPlusConvexArbitrary(const vector<T>& a, const vector<T>& b) {   // a convex
    int n = sz(a), m = sz(b); if (n == 0 || m == 0) return {};
    vector<T> c(n + m - 1);
    auto rec = [&](auto&& self, int kl, int kr, int jl, int jr) -> void {   // rows [kl, kr) have an optimal j in [jl, jr]
        if (kl >= kr) return;
        int k = (kl + kr) / 2, lo = max(jl, k - n + 1), hi = min(jr, k), best = lo;
        for (int j = lo + 1; j <= hi; j++) if (a[k - j] + b[j] < a[k - best] + b[best]) best = j;
        c[k] = a[k - best] + b[best];
        self(self, kl, k, jl, best); self(self, k + 1, kr, best, jr);
    };
    rec(rec, 0, n + m - 1, 0, m - 1);
    return c;
}

GF(2^64)

Elements are 64-bit masks = polynomials over modulo the irreducible : addition is XOR, multiplication is carry-less multiplication then reduction. A field of characteristic 2 (), so random evaluation of polynomials whose coefficients only matter mod 2 works (Schwartz–Zippel with error ), e.g. “is the number of perfect matchings odd” (CodeChef EVENPOLY): substitute random field elements into the Tutte / Edmonds matrix and test . Nim multiplication (Sprague–Grundy) is another field of size .

using u64 = unsigned long long; using u128 = unsigned __int128;
u128 clmul(u64 a, u64 b) { u128 r = 0; for (int i = 0; i < 64; i++) if (b >> i & 1) r ^= (u128)a << i; return r; }   // carry-less product
struct GF64 {
    u64 v = 0;
    GF64() {} explicit GF64(u64 x) : v(x) {}
    static u64 reduce(u128 x) {                              // mod x^64 + x^4 + x^3 + x + 1
        u64 hi = x >> 64, lo = (u64)x;
        u128 t = (u128)hi ^ ((u128)hi << 1) ^ ((u128)hi << 3) ^ ((u128)hi << 4);   // hi x^64 = hi (x^4 + x^3 + x + 1)
        u64 hi2 = t >> 64; lo ^= (u64)t;
        lo ^= hi2 ^ (hi2 << 1) ^ (hi2 << 3) ^ (hi2 << 4);   // hi2 has <= 4 bits, no further overflow
        return lo;
    }
    friend GF64 operator+(GF64 a, GF64 b) { return GF64(a.v ^ b.v); }
    friend GF64 operator-(GF64 a, GF64 b) { return GF64(a.v ^ b.v); }
    friend GF64 operator*(GF64 a, GF64 b) { return GF64(reduce(clmul(a.v, b.v))); }
    GF64& operator+=(GF64 o) { return *this = *this + o; }
    GF64& operator*=(GF64 o) { return *this = *this * o; }
    friend bool operator==(GF64 a, GF64 b) { return a.v == b.v; }
    GF64 pow(u64 e) const { GF64 r(1), b = *this; for (; e; e >>= 1, b *= b) if (e & 1) r *= b; return r; }
    GF64 inv() const { return pow(~0ULL - 1); }              // a^(2^64 - 2)
    GF64 operator/(GF64 o) const { return *this * o.inv(); }
};