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
}

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).

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;
}

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;
}

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, divisorMobius

Typical 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.

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);
}

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;
    }
};

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 mask iff 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”.