GPT o4-mini 用 1m10s 战胜了 P9400

https://www.luogu.com.cn/record/216185866

思考动态规划解法

我在考虑递推式,其中dp[i]表示有效序列的数量,但需要处理无效的部分。虽然包含排除法可以用来解决这个问题,但感觉太麻烦了。实际上,典型的动态规划使用的是前缀和,即dp[i]可以通过sumDp[i]计算。我还在考虑其他可能的解法,像是将序列里的每个段看作一个L运行的部分,再拆分它们。这个问题有些难,但我会继续推理一下,试图找到更简洁的解法。

优化动态规划

我正在考虑一种新的动态规划解法,想通过滑动窗口优化求解速度,减少不必要的重复计算。关键是要将潜在的无效段处理成包含排除的方法,或者用段树卷积来优化。尽管这样可能提高了速度,复杂度依然很高。其实我还在考虑是否能借助有序窗口或矩阵指数加速计算,但具体如何实现并不太明确,得在细节上继续琢磨才能找到最佳方案。

优化动态规划方案

我在考虑如何优化这里的DP算法,主要是解决计算复杂度的问题,特别是当a很大时,O(n*a)会变得太慢。其中一种思路是,通过减去包含无效段的序列,也就是那些包含长度为a的H块,我们可以设法优化此过程。对于每个i,我可以根据之前的序列计算,并逐步构建出有效序列的数量。这个方法应该能更好地控制复杂度。

动态规划推导过程

我在调整动态规划的状态转移。重点在如何避免让已经违反条件的序列重新计算。如果序列在dp[i]生成时已经违反了规则,那么它的扩展应该被排除。特别是当我们延伸一个有H的子序列时,是否需要在之前就已经减去其中不符合条件的部分。这个推导过程稍显复杂,但通过仔细思考这些递归关系,我认为这个方法有可能是有效的。

已思考 1m 10s

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;

// fast exponentiation modulo MOD
ll modpow(ll a, ll e=MOD-2) {
    ll r = 1;
    while (e) {
        if (e & 1) r = r * a % MOD;
        a = a * a % MOD;
        e >>= 1;
    }
    return r;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, a;
    ll b;
    cin >> n >> a >> b;

    vector<ll> L(n+1), R(n+1), T(n+1);
    for(int i = 1; i <= n; i++){
        ll li, ri;
        cin >> li >> ri;
        // count of choices <= b
        ll up = min(ri, b);
        L[i] = (up >= li ? (up - li + 1) % MOD : 0);
        // count of choices > b
        ll lo = max(li, b+1);
        R[i] = (ri >= lo ? (ri - lo + 1) % MOD : 0);
        T[i] = (L[i] + R[i]) % MOD;
    }

    // dp-state array of size a, stored in a rolling circular buffer
    vector<ll> A(a, 0);
    vector<int> version(a, 0);
    int cur_ver = 1;

    ll S = 1;           // sum of dp[k] over k=0..a-1
    ll g = 1;           // global multiplier for dp-states
    ll inv_g = 1;       // inverse of g
    int head = 0;       // circular buffer head, A[head] holds dp[0] / g

    // initial dp: dp[0]=1
    A[0] = 1;
    version[0] = cur_ver;

    for(int i = 1; i <= n; i++){
        // retrieve dp_old[a-1]
        int idx_last = (head + (a - 1)) % a;
        ll old_dp_am1 = 0;
        if (version[idx_last] == cur_ver) {
            old_dp_am1 = A[idx_last] * g % MOD;
        }

        ll new_dp0 = S * L[i] % MOD;

        if (R[i] == 0) {
            // if R_i == 0, all H-extensions die => only dp[0] survives
            S = S * L[i] % MOD;
            // reset the dp-buffer
            cur_ver++;
            head = 0;
            g = 1;
            inv_g = 1;
            // store new dp[0]
            A[0] = new_dp0;
            version[0] = cur_ver;
        } else {
            // general case
            // update total sum S
            S = ( (S * T[i]) % MOD - (old_dp_am1 * R[i]) % MOD + MOD ) % MOD;

            // update global multiplier
            g = g * R[i] % MOD;
            ll inv_R = modpow(R[i]);
            inv_g = inv_g * inv_R % MOD;

            // shift head backwards by 1 in circular buffer
            head = (head - 1 + a) % a;
            // store new dp[0] as A[head] = new_dp0 / g
            A[head] = new_dp0 * inv_g % MOD;
            version[head] = cur_ver;
        }
    }

    cout << S << "\n";
    return 0;
}

说明:

  • 我们将状态分为 \(dp[k]\) 表示以恰好 \(k\) 个 “高亮” (亮度 \(>\!b\)) 结尾的方案权重,总共只需维护 \(k=0,1,\dots,a-1\)

  • \(S=\sum_{k=0}^{a-1}dp[k]\)。第 \(i\) 间宿舍可选 “低” (\(\le b\)) 的方案数为 \(L_i\),可选 “高” 的方案数为 \(R_i\),总数 \(T_i=L_i+R_i\)

  • 转移:

    • 新的 \(dp[0]\)(以“低”结尾)来自任意前缀后选“低”,权重 \(S \times L_i\)
    • 对于 \(k\ge1\),新的 \(dp[k]\)(以 \(k\) 个“高”结尾)来自旧的 \(dp[k-1]\) 再选“高”,权重乘以 \(R_i\)
    • 超过 \(a-1\) 个“高”即违规,不纳入。
  • 维护一个环形缓冲区存储 \(\{dp[0],\dots,dp[a-1]\}\) 除以一个全局缩放因子 \(g\),这样“批量乘以 \(R_i\)”只需更新 \(g\) 即可,按指针移动实现整体右移,插入新 \(dp[0]\),并从总和 \(S\) 中减去最旧的那条“将要越界”的状态贡献。

  • 如果某步 \(R_i=0\),所有以“高”结尾的方案都归零,只剩以“低”结尾的 \(dp[0]\),此时直接重置缓冲即可。

posted @ 2025-05-06 18:33  Shunpower  阅读(87)  评论(0)    收藏  举报