Holonomic Sequences and Automated Recurrence Guessing
Difference between en1 and en2, changed 0 character(s)
Hello everyone!↵
↵
In CP, linear recurrences with **constant coefficients** are a standard topic. Such problems are typically solved by building a transition matrix and using binary exponentiation to find the $N$-th term in $O(\log N)$ time (or by applying the Bostan-Mori algorithm for larger orders).↵
↵
However, the approach must change when the coefficients are **not** constant and instead depend on $n$.↵
↵
## What is a holonomic sequence?↵
↵
A sequence $a_n$ is called **holonomic** (or **P-recursive**) if it satisfies a linear recurrence relation where the coefficients are *polynomials* in $n$.↵
↵
Formally, there exist polynomials $P_0(x), P_1(x), \dots, P_k(x)$ (where $P_k \neq 0$) such that for all $n$:↵
↵
$$ P_k(n) a_{n+k} + P_{k-1}(n) a_{n+k-1} + \dots + P_0(n) a_n = 0 $$↵
↵
The integer $k$ is the **order** of the recurrence, and the maximum degree among the polynomials $P_i$ is the **degree** of the recurrence.↵
↵
**Classic Examples:**↵
↵
- **Factorials**: $1 \cdot a_n - n \cdot a_{n-1} = 0$. (Order 1, Degree 1).↵
- **Catalan numbers**: $(n+2) \cdot C_{n+1} - (4n+2) \cdot C_n = 0$. (Order 1, Degree 1).↵
- **Derangements**: $1 \cdot D_n - (n-1) \cdot D_{n-1} - (n-1) \cdot D_{n-2} = 0$. (Order 2, Degree 1).↵
↵
## Applications in CP↵
↵
A key property of holonomic sequences is their **closure** under various operations. ↵
If sequences $A$ and $B$ are holonomic, then the following sequences are *also* holonomic:↵
↵
1. $A + B$↵
2. $A \cdot B$↵
3. Prefix sums: $\sum_{i=0}^n A_i$↵
↵
This implies that for complex sums such as $\sum_{k=0}^n \binom{n}{k}^2 \binom{n+k}{k}^2$ (the Apery numbers), a polynomial recurrence is mathematically guaranteed to exist, as binomial coefficients and factorials are themselves holonomic.↵
↵
Analytically, these recurrences can be found using **Zeilberger's Algorithm** (Creative Telescoping), which algebraically simplifies the summation. ↵
↵
However, implementing Zeilberger's algorithm during a contest is highly impractical. Since it is theoretically guaranteed that a recurrence exists, one can bypass the analytical derivation and instead **guess the recurrence** programmatically.↵
↵
## Automated Guessing with Gaussian Elimination↵
↵
Consider a scenario where the $N$-th term of a sequence must be found in $O(N)$ time (for $N \le 10^7$), but the available direct calculation or dynamic programming approach takes $O(N^2)$. Linear algebra can be used to automatically deduce the underlying recurrence.↵
↵
Assume the recurrence has a small order $R$ (e.g., $R=2$) and polynomial degree $D$ (e.g., $D=1$). The equation takes the following form:↵
↵
$$ (c_{2,1} n + c_{2,0}) a_n + (c_{1,1} n + c_{1,0}) a_{n-1} + (c_{0,1} n + c_{0,0}) a_{n-2} = 0 $$↵
↵
Given that the values of $a_n, a_{n-1}, a_{n-2}$, and $n$ are known, the only unknowns are the constants $c_{i,j}$.↵
There are exactly $U = (R+1) \times (D+1)$ unknown constants. In this example, $U = 3 \times 2 = 6$.↵
↵
## Reference Implementation (C++)↵
↵
Below is a reference **solution for the problem 506E (Mr. Kitayuta's Gift)** utilizing this algorithm.↵
↵
**Problem 506E:** *Given a string $S$ ($|S| \le 200$), calculate the number of valid ways to insert exactly $N$ characters ($N \le 10^9$) such that the resulting string is a palindrome. All calculations should be modulo $10007$.*↵
↵
**How my solution works:**↵
↵
1. **Naive DP:** We write a simple $O(|S|^2 \cdot K)$ DP to compute the answers for small lengths.↵
2. **Parity Split (Crucial):** Even and odd length palindromes behave like two independent sequences. Mixing them inflates the recurrence order to $\sim 600$, causing a Time Limit Exceeded (TLE) during matrix exponentiation. By exclusively computing terms for the target parity $(|S| + N) \pmod 2$, we halve the recurrence order to $\le 305$.↵
3. **Automated Guessing:** We generate $K = 800$ terms and feed them into the Gaussian elimination algorithm with polynomial degree $D=0$ (which effectively finds a constant-coefficient recurrence).↵
4. **Optimized Matrix Exponentiation:** We construct the transition matrix and raise it to the power of $N/2$. Since $MOD = 10007$, we can safely omit the modulo operation in the innermost loop of the matrix multiplication, allowing the solution to comfortably run in $\sim 0.3s$.↵
↵
***↵
↵
<spoiler summary="Code for 506E">↵
↵
~~~~~↵
#include <bits/stdc++.h>↵
using namespace std;↵
↵
using ll = long long;↵
using ull = unsigned long long;↵
using ld = long double;↵
using pii = pair<int, int>;↵
using pll = pair<ll, ll>;↵
using vi = vector<int>;↵
using vl = vector<ll>;↵
using vvi = vector<vector<int>>;↵
using vvl = vector<vector<ll>>;↵
↵
#define all(x) (x).begin(), (x).end()↵
#define rall(x) (x).rbegin(), (x).rend()↵
#define sz(x) (int)(x).size()↵
#define pb push_back↵
#define mp make_pair↵
#define fi first↵
#define se second↵
#define FOR(i, a, b) for (int i = (a); i < (b); ++i)↵
#define RFOR(i, a, b) for (int i = (b)-1; i >= (a); --i)↵
#define each(x, a) for (auto &x : a)↵
↵
const int MOD = 10007;↵
const int INF = 1e9;↵
const ll LINF = 1e18;↵
↵
ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; }↵
ll lcm(ll a, ll b) { return a / gcd(a, b) * b; }↵
↵
ll bp(ll a, ll b, ll mod = MOD) {↵
    ll r = 1;↵
    a %= mod;↵
    while (b > 0) {↵
        if (b % 2 == 1) r = (r * a) % mod;↵
        a = (a * a) % mod;↵
        b /= 2;↵
    }↵
    return r;↵
}↵
↵
ll ext_inv(ll a, ll m) {↵
    ll m0 = m, y = 0, x = 1;↵
    if (m == 1) return 0;↵
    while (a > 1) {↵
        ll q = a / m;↵
        ll t = m;↵
        m = a % m, a = t;↵
        t = y;↵
        y = x - q * y;↵
        x = t;↵
    }↵
    if (x < 0) x += m0;↵
    return x;↵
}↵
↵
vvl guess(const vl& a, int R, int D) {↵
    int u = (R + 1) * (D + 1);↵
    int eq = sz(a) - R;↵
    if (eq < u) return {};↵
    vvi mt(eq, vi(u, 0));↵
    FOR(i, 0, eq) {↵
        int n = i + R;↵
        int c = 0;↵
        FOR(o, 0, R + 1) {↵
            ll p = 1;↵
            FOR(d, 0, D + 1) {↵
                mt[i][c] = (p * a[n - o]) % MOD;↵
                c++;↵
                p = (p * n) % MOD;↵
            }↵
        }↵
    }↵
    int r = 0;↵
    vi piv;↵
    FOR(c, 0, u) {↵
        if (r >= eq) break;↵
        int p = r;↵
        while (p < eq && mt[p][c] == 0) p++;↵
        if (p == eq) continue;↵
        swap(mt[r], mt[p]);↵
        ll iv = ext_inv(mt[r][c], MOD);↵
        FOR(j, c, u) mt[r][j] = (mt[r][j] * iv) % MOD;↵
        FOR(k, 0, eq) {↵
            if (k == r || mt[k][c] == 0) continue;↵
            int f = mt[k][c];↵
            for (int j = c; j < u; ++j) {↵
                mt[k][j] = (mt[k][j] - f * mt[r][j]) % MOD;↵
                if (mt[k][j] < 0) mt[k][j] += MOD;↵
            }↵
        }↵
        piv.pb(c);↵
        r++;↵
    }↵
    if (r == u) return {};↵
    vl sol(u, 0);↵
    int fv = -1;↵
    FOR(c, 0, u) {↵
        bool ip = false;↵
        each(p, piv) if (p == c) ip = true;↵
        if (!ip) { fv = c; break; }↵
    }↵
    sol[fv] = 1;↵
    FOR(i, 0, r) sol[piv[i]] = (MOD - mt[i][fv]) % MOD;↵
↵
    vvl res(R + 1, vl(D + 1, 0));↵
    int c = 0;↵
    FOR(o, 0, R + 1) {↵
        FOR(d, 0, D + 1) {↵
            res[o][d] = sol[c++];↵
        }↵
    }↵
    return res;↵
}↵
↵
ll gp(int l, int r, const vvi& pr, ll pm) {↵
    if (l > r) return pm;↵
    return pr[l][r];↵
}↵
↵
vvl mul(const vvl& A, const vvl& B, int ms) {↵
    vvl r(ms, vl(ms, 0));↵
    FOR(i, 0, ms) {↵
        FOR(k, 0, ms) {↵
            if (A[i][k] == 0) continue;↵
            FOR(j, 0, ms) {↵
                r[i][j] += A[i][k] * B[k][j];↵
            }↵
        }↵
        FOR(j, 0, ms) {↵
            r[i][j] %= MOD;↵
        }↵
    }↵
    return r;↵
}↵
↵
void solve() {↵
    string s; ll n; cin >> s >> n;↵
    int L = sz(s);↵
    int R_max = 350;↵
    int K = 800;↵
    int target_parity = (L + n) % 2;↵
    vvi pr(L, vi(L, 0));↵
    ll pm = 0;↵
    if (target_parity == 0) {↵
        pm = 1;↵
        FOR(l, 0, L) pr[l][l] = 0;↵
    } else {↵
        pm = 26;↵
        FOR(l, 0, L) pr[l][l] = 1;↵
    }↵
    vl A; A.reserve(K);↵
    int len = target_parity;↵
    while (sz(A) < K) {↵
        if (len >= L) {↵
            A.pb(pr[0][L - 1]);↵
        }↵
        vvi cr(L, vi(L, 0));↵
        ll cm = (26LL * pm) % MOD;↵
        FOR(l, 0, L) {↵
            FOR(r, l, L) {↵
                if (s[l] == s[r]) {↵
                    ll v = gp(l + 1, r - 1, pr, pm);↵
                    v = (v + 25LL * pr[l][r]) % MOD;↵
                    cr[l][r] = v;↵
                } else {↵
                    ll v = gp(l + 1, r, pr, pm);↵
                    v = (v + gp(l, r - 1, pr, pm)) % MOD;↵
                    v = (v + 24LL * pr[l][r]) % MOD;↵
                    cr[l][r] = v;↵
                }↵
            }↵
        }↵
        pr.swap(cr);↵
        pm = cm;↵
        len += 2;↵
    }↵
    vvl cf = guess(A, R_max, 0);↵
    int tR = -1;↵
    if (!cf.empty()) {↵
        RFOR(i, 0, R_max + 1) {↵
            if (cf[i][0] != 0) {↵
                tR = i;↵
                break;↵
            }↵
        }↵
    }↵
    if (tR <= 0 || cf[0][0] == 0) {↵
        cout << "0";↵
        return;↵
    }↵
    vl C(tR + 1, 0);↵
    ll ic0 = ext_inv(cf[0][0], MOD);↵
    FOR(o, 1, tR + 1) {↵
        C[o] = (MOD - cf[o][0]) % MOD * ic0 % MOD;↵
    }↵
    ll m = n / 2;↵
    if (m < K) {↵
        cout << A[m];↵
        return;↵
    }↵
    int ms = tR;↵
    vvl M(ms, vl(ms, 0));↵
    FOR(i, 0, ms) M[0][i] = C[i + 1];↵
    FOR(i, 1, ms) M[i][i - 1] = 1;↵
    vvl res(ms, vl(ms, 0));↵
    FOR(i, 0, ms) res[i][i] = 1;↵
    ll pw = m - (tR - 1);↵
    vvl bs = M;↵
    while (pw > 0) {↵
        if (pw % 2 == 1) res = mul(res, bs, ms);↵
        bs = mul(bs, bs, ms);↵
        pw /= 2;↵
    }↵
    ll ans = 0;↵
    FOR(i, 0, ms) ans = (ans + res[0][i] * A[tR - 1 - i]) % MOD;↵
    cout << ans << "\n";↵
}↵
↵
int main() {↵
    ios::sync_with_stdio(false);↵
    cin.tie(nullptr);↵
↵
    int t = 1; // cin >> t;↵
    while (t--) solve();↵
    return 0;↵
}↵
~~~~~↵
↵
</spoiler>↵
↵
***↵
↵
If there are any questions regarding this method or its implementation, please feel free to leave a comment or send a direct message.↵

History

 
 
 
 
Revisions
 
 
  Rev. Lang. By When Δ Comment
ru1 Russian Math_Tereegor 2026-07-27 15:06:40 10392 Первая редакция перевода на Русский
en2 English Math_Tereegor 2026-07-27 14:20:58 0 (published)
en1 English Math_Tereegor 2026-07-27 14:20:30 10117 Initial revision (saved to drafts)