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.
| Variant | Method | Cost |
|---|---|---|
| 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 convex | merge slopes | |
| , one convex | monotone minima (divide and conquer) | |
| , general | no 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;
}Proof: 2D flattening and the chirp identity
2D. Entry is placed at with . A product term lands at , and , so no term crosses into the next row: the 1D product read with stride is the 2D convolution. Chirp-z. , so and : with reversed, is coefficient of the product. This uses only integer exponents, so no square root of is needed (the classical Bluestein form needs , i.e. a -th root, over ). With a primitive -th root of unity it is a DFT of arbitrary length . Exponents are reduced mod (Fermat). Checked against direct evaluation (and ).
| DFT length | Use |
|---|---|
| power of two | Cooley–Tukey directly |
| smooth (small prime factors) | mixed-radix Cooley–Tukey |
| coprime | Good–Thomas |
| prime | Rader (cyclic convolution over ) or Bluestein |
| arbitrary, or a partial spectrum / zoom FFT | Bluestein (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); }Proof: digit-wise transforms
Digit-wise sum. The group has characters for a primitive -th root of unity ; the DFT turns the group convolution into a pointwise product, and the inverse uses and division by (orthogonality ). The transform factorises digit by digit, which is the loop over
len. In we use ; all identities used except orthogonality hold in , and the final values, mapped to where is a primitive -th root of unity, satisfy orthogonality there ( for , since but and are coprime modulo when ). So the reduction of each result modulo is the true integer convolution value, a constant; dividing by needs . Powers exponentiate each transformed coordinate. Digit-wise max: prefix sums along every digit give (digit-wise ); iff and , so , and taking differences along each digit inverts the prefix sums (min symmetrically with suffix sums). Checked against brute force for .
(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;
}Proof: the two (min, +) algorithms
Convex + convex. A pair is reached by taking unit steps from , each increasing or ; the cost increases by the slope taken. Since both slope sequences are non-decreasing, taking the smaller next slope first is optimal for every prefix length (exchange argument: any other order of the same multiset of steps has a prefix sum that is at least as large), so is plus the smallest available slopes in merge order, which the loop computes. Convex + arbitrary. For and (both feasible), convexity of gives when the indices are in range (the four indices satisfy with equal sums), i.e. is Monge on its feasible band. Hence a leftmost-tie optimal column of row is non-decreasing in (if row preferred strictly, adding the Monge inequality would contradict the optimality of in row ), and restricting rows left of to and rows right of it to loses nothing; the band constraint is itself monotone in . Each recursion level scans columns: . Checked against the definition on random convex / arbitrary inputs.
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(); }
};Proof: GF(2^64) arithmetic
is a field iff is irreducible; is the standard irreducible (indeed primitive) pentanomial-type modulus of degree 64, so the 64-bit masks form . Carry-less multiplication is polynomial multiplication over (XOR adds coefficients). Reduction: , so the high word contributes , a polynomial of degree ; its bits above 63 ( of them) are reduced once more, and has degree , so it fits. Inverse: the multiplicative group has order , so (Lagrange). Verified: , distributivity and associativity on random elements, and
reduceagainst bitwise long division.
Related notes
- Bitwise Convolutions (XOR / OR / AND, subset convolution)
- Evaluation and Interpolation
- Aliens Trick