Online (relaxed) convolution returns as soon as and are known, so and may depend on : the tool for sequences defined by a convolution with their own prefix (, rooted-tree counts, / of something defined implicitly). Cost ( in about 0.6 s), versus naively. Uses ntt/convolution/Poly from NTT.
| Setting | Method | Cost |
|---|---|---|
| , known in advance (semi-relaxed) | CDQ divide and conquer: finish the left half, add its contribution to the right half with one convolution, recurse | |
| and both depend on the prefix of (fully relaxed) | RelaxedConvolution (blocks of size along the anti-diagonals) | |
| the equation is algebraic in a known series (, inverse, log of a known ) | Newton (Power Series) |
Poly cdqSelfConvolution(const Poly& g, mint f0, int n) { // f_0 = f0, f_i = sum_{j<i} f_j g_{i-j} (i.e. f = f0 / (1 - g) with g_0 ignored)
Poly f(n, 0); if (n == 0) return f; f[0] = f0;
auto rec = [&](auto&& self, int l, int r) -> void { // on entry f[l] is final; contributions from [0, l) already added to f[l, r)
if (r - l == 1) return;
int m = (l + r) / 2; self(self, l, m);
Poly a(f.begin() + l, f.begin() + m), b(min(r - l, sz(g)), 0);
for (int i = 1; i < sz(b); i++) b[i] = g[i];
Poly c = polyMul(a, b);
for (int i = m; i < r; i++) if (i - l < sz(c)) f[i] += c[i - l];
self(self, m, r);
};
rec(rec, 0, n);
return f;
}
template <int P> struct RelaxedConvolution {
using Z = Mint<P>; static constexpr int NAIVE = 32;
vector<Z> f, g, h; vector<vector<Z>> fBand, gBand; // NTT (size 2s) of f[s-1, 2s-1) and g[s-1, 2s-1), s = 2^z
Z push(Z a, Z b) { // h_p = sum_{j + k = p} f_j g_k, with f_p = a, g_p = b
int p = sz(f); f.push_back(a); g.push_back(b);
if (sz(h) < 2 * p + 2) h.resize(2 * p + 2);
for (int z = 0, s = 1; 2 * s <= p + 2 && (p + 2) % s == 0; z++, s *= 2) {
int x = p + 1 - s; bool diag = x == s - 1; // the square [s-1, 2s-1)^2, counted once
if (s <= NAIVE) {
for (int j = x; j < x + s; j++) for (int k = s - 1; k < 2 * s - 1; k++) h[j + k] += diag ? f[j] * g[k] : f[j] * g[k] + g[j] * f[k];
continue;
}
vector<Z> u(f.begin() + x, f.begin() + x + s), v(g.begin() + x, g.begin() + x + s);
u.resize(2 * s); v.resize(2 * s); ntt(u); ntt(v);
if (diag) { fBand.resize(z + 1); gBand.resize(z + 1); fBand[z] = u; gBand[z] = v; for (int i = 0; i < 2 * s; i++) u[i] *= v[i]; }
else for (int i = 0; i < 2 * s; i++) u[i] = u[i] * gBand[z][i] + v[i] * fBand[z][i];
ntt(u, true);
for (int i = 0; i < 2 * s - 1; i++) h[p + i] += u[i];
}
return h[p];
}
};
vector<mint> rootedTrees(int n) { // unlabelled rooted trees a_1..a_n (index = size), relaxed
vector<mint> a(n + 1, 0), b(n + 1, 0), iv(n + 1, 1); if (n >= 1) a[1] = 1;
for (int i = 2; i <= n; i++) iv[i] = -mint(998244353 / i) * iv[998244353 % i];
RelaxedConvolution<998244353> rc; // h_m = sum_{k=1}^{m} b_k a_{m+1-k}: push (b_{i+1}, a_{i+1})
for (int m = 1; m < n; m++) {
for (int d = 1; d * d <= m; d++) if (m % d == 0) { b[m] += mint(d) * a[d]; if (d * d != m) b[m] += mint(m / d) * a[m / d]; }
mint h = rc.push(b[m], a[m]); // after push number m - 1 (0-indexed): sum_{j+k = m-1} b_{j+1} a_{k+1}
a[m + 1] = h * iv[m];
}
return a;
}Rooted trees. gives with (Graph Counting): needs , and needs the convolution up to , a relaxed product of the shifted sequences and .
Proof: CDQ and the relaxed block schedule
CDQ. Invariant: when
rec(l, r)is entered,f[l]is final and every contribution with , has been added. The left recursion keeps the invariant for (contributions from are already in, those from are added inside), after whichf[l..m)are final. The convolution then adds exactly the contributions with to every (the kernel uses , which covers ), restoring the invariant for . Each level does convolutions of total length : . With excluded this computes , i.e. .Relaxed schedule. Partition the index pairs into squares: the diagonal squares and, for every , the off-diagonal blocks (and their mirror images) with , . These tile the quadrant exactly once (a dyadic decomposition: each pair lies in the block determined by the largest power of two for which both coordinates fit). The block is processed at step , when are known up to index : its smallest sum is , so its contribution is added before is returned, and it uses only indices . The band transforms of on are computed at the diagonal step and reused. Per there are blocks costing , so per level and in total. Both methods were checked against the naive recurrences (including depending on ), against
polyInvfor , and the rooted-tree counts against OEIS A000081.