Techniques for counting objects (mod a prime) where a direct DP would need too much state. The recurring ideas: insert elements in a clever order so the state stays small (relative ranks, connected blocks), compress huge numeric states to their distinct floor values, factor the lattice of divisors or subsets into chains, and double the range when is astronomically large. Generating-function methods are in Generating Functions and Counting with Power Series.
Permutations by relative order
Counting permutations with constraints between neighbouring positions (an up/down signature, conditions): process positions left to right and remember only the rank of the last element among those placed so far.
// # permutations of 1..n with p_i < p_{i+1} where s[i] == '<' and > where '>' (|s| = n - 1)
vector<mint> signature(const string& s) {
int n = sz(s) + 1; vector<mint> dp{1}; // dp[j]: last element has rank j among the first i
for (int i = 1; i < n; i++) {
vector<mint> pre(i + 1), nd(i + 1);
for (int j = 0; j < i; j++) pre[j + 1] = pre[j] + dp[j];
for (int j = 0; j <= i; j++) nd[j] = s[i - 1] == '<' ? pre[j] : pre[i] - pre[j];
dp = nd;
}
return dp; // sum of dp = answer
}Proof: ranks are a complete state
Map the first values to their relative order (a permutation of ). Appending a new value whose rank among the first is shifts all old ranks up by one and leaves the relative order of the old elements unchanged; every pair (old pattern, ) arises from exactly one pattern of values. The new last element is larger than the previous last iff the previous last had old rank . So the count of patterns ending in rank depends only on the counts for the previous ranks, which is the recurrence; prefix sums make each layer .
Connected-component (insertion) DP
For constraints involving values of neighbours (zigzag permutations, costs , “buildings visible”), insert values in increasing order into a set of blocks: contiguous pieces of the final permutation whose relative order is fixed and whose gaps will be filled by larger values later. The new value can
- start a new block (in one of the gaps),
- attach to an end of an existing block ( ends), or
- merge two neighbouring blocks ( ways).
Each move fixes the value’s final neighbours relative to larger values: a merging value is larger than both neighbours, a new block’s value is smaller than both. Fixed endpoints restrict which gaps are available.
mint kangaroo(int n, int s, int t) { // zigzag permutations with p_1 = s, p_n = t (CEOI 2016)
if (s == t) return n == 1;
vector<vector<mint>> dp(n + 1, vector<mint>(n + 2)); dp[0][0] = 1;
for (int i = 1; i <= n; i++) for (int j = 1; j <= i; j++) {
if (i == s || i == t) dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; // new border block or extend the border block
else {
int gaps = j - (i > s) - (i > t); // gaps not blocked by an already placed end
dp[i][j] = dp[i - 1][j - 1] * max(gaps, 0) + dp[i - 1][j + 1] * j; // new block (local min) or merge (local max)
}
}
return dp[n][1];
}Costs like : when value is inserted, each of its open sides (sides that will later touch a larger value) contributes and each closed side ; track the running sum as an extra dimension (often bounded by “sum of open ends × remaining gap”, which keeps it small).
Proof: the block moves enumerate each permutation once
Fix a final permutation and read it while inserting values . After inserting , the positions holding these values form maximal runs (blocks) separated by positions holding larger values; their left-to-right order is determined. Value either has no neighbour among (new block, in a determined gap), exactly one (attaches to a determined end), or two (merges two determined blocks). So each permutation gives exactly one sequence of moves, and each move sequence ending in one block gives exactly one permutation. In the zigzag problem, an inner value is either a local max (both final neighbours smaller, hence already placed: merge) or a local min (both larger: new block); the ends , are forced to the borders, which removes the outer gaps once they are placed.
Huge numeric states: floor values
A state like “remaining budget ” takes only distinct values, because and has at most distinct values. Index small values directly and large ones by .
mint countProductAtMost(const vector<ll>& a, ll K) { // # subsets (incl. empty) with product <= K
if (K < 1) return 0;
ll S = sqrtl(K); while ((S + 1) * (S + 1) <= K) S++; while (S * S > K) S--;
int M = 2 * S + 2; auto id = [&](ll q) { return q <= S ? int(q) : int(S + K / q); };
vector<ll> val(M); for (ll q = 0; q <= S; q++) val[q] = q; for (ll d = 1; d <= S; d++) val[S + d] = K / d;
vector<mint> cnt(M); cnt[id(K)] = 1; // state = floor(K / product so far)
for (ll x : a) { auto nx = cnt; for (int i = 0; i < M; i++) if (val[i] / x >= 1) nx[id(val[i] / x)] += cnt[i]; cnt = nx; }
mint t = 0; for (auto& c : cnt) t += c; return t;
}Proof: nested floors and the bound
Write with ; then with , so with , and dividing by gives . Hence “product rest ” depends on the product only through . For there are at most values ; for the value is . So at most distinct values.
Bounded XOR tuples
Count with and in . At the highest bit set in some : if some such variable puts 0 at bit , its lower bits become completely free, so it can absorb any XOR of the lower bits (take the first such variable; it contributes factor 1, others contribute their counts). Otherwise all of them keep bit ; strip it and recurse.
ll countXor(vector<ll> a, ll X, ll mod) {
ll res = 0;
for (int k = 62; k >= 0; k--) {
bool any = false; for (ll x : a) any |= x >> k & 1;
if (!any) { if (X >> k & 1) return res; continue; }
ll dp[2][2] = {{1, 0}, {0, 0}}, low = (1LL << k) % mod; // [parity at bit k][free variable chosen]
for (ll x : a) {
ll nd[2][2] = {};
for (int p = 0; p < 2; p++) for (int f = 0; f < 2; f++) if (ll c = dp[p][f]) {
if (x >> k & 1) {
nd[p ^ 1][f] = (nd[p ^ 1][f] + c * ((x - (1LL << k) + 1) % mod)) % mod; // keep bit k
nd[p][1] = (nd[p][1] + c * (f ? low : 1)) % mod; // drop it: lower bits free
} else nd[p][f] = (nd[p][f] + c * ((x + 1) % mod)) % mod;
}
memcpy(dp, nd, sizeof dp);
}
res = (res + dp[X >> k & 1][1]) % mod;
int ones = 0; for (ll x : a) ones += x >> k & 1;
if ((ones & 1) != (X >> k & 1)) return res;
for (ll& x : a) if (x >> k & 1) x ^= 1LL << k;
X &= ~(1LL << k);
}
return (res + (X == 0)) % mod;
}Proof: the free variable absorbs the lower bits
Split the tuples at bit by whether some variable with bit available chooses 0 there. In that case let be the first such variable: its value is with arbitrary. Fix every other variable’s value (counted with their multiplicities: ways to keep the bit, ways to drop it, ways for variables without bit ). Then exactly one makes the XOR of the bits below equal to ‘s, and the bit- parity is determined by the others. So the count is the DP value with factor 1 for the first free variable. Otherwise every variable with bit available keeps it, the parity at bit is fixed, and the remaining freedom is exactly the problem on lower bits with . Bits above the top bit must be 0 in .
Divisor lattice DP
Functions on divisors of (or on with divisibility) are functions on a product of chains (one per prime), so zeta/Möbius transforms are prefix sums/differences along each prime, exactly like SOS DP along each bit (SOS).
template<class T> void divisorZeta(vector<T>& a, const vector<int>& primes) { // a[i] = sum_{d | i} a[d], 1..n
int n = sz(a) - 1; for (int p : primes) for (int i = 1; (ll)i * p <= n; i++) a[i * p] += a[i];
}
template<class T> void divisorMobius(vector<T>& a, const vector<int>& primes) {
int n = sz(a) - 1; for (int p : primes) for (int i = n / p; i >= 1; i--) a[i * p] -= a[i];
}
template<class T> void multipleZeta(vector<T>& a, const vector<int>& primes) { // a[i] = sum_{i | j} a[j]
int n = sz(a) - 1; for (int p : primes) for (int i = n / p; i >= 1; i--) a[i] += a[i * p];
}
template<class T> void multipleMobius(vector<T>& a, const vector<int>& primes) {
int n = sz(a) - 1; for (int p : primes) for (int i = 1; (ll)i * p <= n; i++) a[i] -= a[i * p];
}
// gcd convolution c[k] = sum_{gcd(i,j)=k} a[i] b[j]: multipleZeta both, multiply pointwise, multipleMobius
// lcm convolution (k <= n): divisorZeta both, multiply, divisorMobiusTypical uses: number of pairs with (multiple-Möbius of , where = count of multiples of ), , counting arrays with a given gcd or lcm, necklaces (Burnside). For a single , do the same over the sorted divisor list of with steps.
Proof: prime-by-prime prefix sums give the divisor zeta transform
Write . After processing primes , holds over divisors of that agree with on the exponents of all primes except (where they may be smaller). Processing in increasing adds (already updated for ) to , which is a prefix sum along the -exponent chain; by induction this extends the set of allowed divisors to also vary the exponent. After all primes, . The Möbius version undoes each prefix sum by a difference in reverse order. The cost is . Gcd convolution works because (both and iff ).
Doubling over huge
If the answer for can be updated by “append element ” and by “combine with ”, compute it from the binary expansion of in combines. Example: elementary symmetric sums for all (these are unsigned Stirling numbers of the first kind ), mod a prime , in :
vector<mint> elemSym(ll n, int K) { // e_0..e_K of {1, ..., n}
vector<mint> inv(K + 2, 1); for (int i = 1; i <= K + 1; i++) inv[i] = mint(1) / i;
auto rec = [&](auto&& self, ll m) -> vector<mint> {
vector<mint> ans(K + 1); if (m == 0) { ans[0] = 1; return ans; }
if (m & 1) { auto l = self(self, m - 1); for (int i = 0; i <= K; i++) ans[i] = l[i] + (i ? l[i - 1] * mint(m) : 0); return ans; }
ll p = m / 2; auto l = self(self, p); vector<mint> r(K + 1), pw(K + 1, 1);
for (int i = 1; i <= K; i++) pw[i] = pw[i - 1] * mint(p);
for (int j = 0; j <= K && j <= p; j++) {
mint c = 1; // C(p - j, t)
for (int t = 0; j + t <= K && j + t <= p; t++) { r[j + t] += l[j] * pw[t] * c; c *= mint(p - j - t) * inv[t + 1]; }
}
for (int i = 0; i <= K; i++) for (int j = 0; i + j <= K; j++) ans[i + j] += l[i] * r[j];
return ans;
};
return rec(rec, n);
}Proof: the shift formula
over . Expanding each product chooses a subset whose elements contribute and the rest contribute : the term is . Each of size lies in sets of size . Summing gives the formula. Then as polynomials (truncated at degree ).
Dynamic submask counting
A multiset of -bit masks with insert/erase and “how many elements are submasks of ” in per operation: split bits into halves; table[hi][lo] = number of elements whose high half equals hi and whose low half is a submask of lo. Insertion enumerates supermasks of its low half; a query enumerates submasks of its high half.
template<int B> struct DynSubmask {
static constexpr int H = B / 2, L = B - H, FL = (1 << L) - 1;
vector<int> t = vector<int>(1 << B);
void update(int x, int d) { int hi = x >> L, lo = x & FL; for (int m = lo;; m = (m + 1) | lo) { t[hi << L | m] += d; if (m == FL) break; } }
int countSub(int s) const { // # elements a with (a & s) == a
int hi = s >> L, lo = s & FL, r = 0;
for (int m = hi;; m = (m - 1) & hi) { r += t[m << L | lo]; if (!m) break; }
return r;
}
};Proof: the split table
is a submask of iff and . The query enumerates every candidate and reads , which counts elements with high half exactly and low half a submask of (insertions added 1 to every supermask of their low half). Both loops enumerate at most masks.
More counting patterns
- Counting distinct objects, not constructions: fix a canonical construction (e.g. the lexicographically smallest way, or “the first occurrence”) and count only those. Distinct subsequences: .
- First bad event: count sequences avoiding something by subtracting, for each possible first violation, (ways to reach it cleanly) × (anything after) (Classic DP, grid paths).
- Moments over all subsets: to sum over all subsets, carry (count, , ) per state; adding an element with value maps . Higher powers via binomial expansion.
- Union sizes of all families of sets in : an element misses the union of
maskiff its membership set is a submask of the complement, so , a subset zeta transform (intersections use the superset transform instead). Typical use: choose a family of sets of minimum cost covering at least elements. - Counting labelled structures by their connected pieces: fix the component containing vertex 1 (Graph Counting).
- Permutations by inserting values in increasing order into growing contiguous blocks (a different “connected-component DP” from the graph-counting one above): = ways to place values as blocks (contiguous pieces of the eventual permutation, order between blocks fixed, gaps filled later). Value can start a new block, extend an existing block’s end, or merge two adjacent blocks; which moves are legal, and their multiplicity (number of available ends/gaps), comes from the problem’s adjacency constraint. . Same pattern underlies permutations with a cost on , “zigzag permutations with fixed endpoints” (fixed endpoints can only create/extend the outermost blocks), and problems like CF 1515E (Phoenix and Computers) and JOI’s “Skyscraper”.