Counting Arrays

Counting Arrays

题意简述

VegiCat 给你两个整数 \(n\) 和 (m)。考虑一个长度为 (n) 的整数数组 (a),满足:

  1. (a_1 = 0)
  2. (0 \le a_{i+1} - a_i \le 1)

令 (s_i) 为数组 (a) 的前缀和。现在给定 (m) 个限制条件 (s_{p_j} = v_j)。请求出满足所有限制条件的数组 (a) 的方案数,对 (998244353) 取模。

数据范围:(T \le 10^4),(n \le 100),(0 \le m \le n),(v_j \le 10^9)。

核心思路

1. 差分转化与区间独立

因为限制了前缀和 (s_p = v),相邻两个限制条件 (s_{p_{i-1}} = v_{i-1}) 和 (s_{p_i} = v_i) 之间,可以通过差分转化为一段区间 ([p_{i-1}, p_i]) 的和:

\[\Delta v = v_i - v_{i-1} \]

在区间内,数组元素只能加 (0) 或 (1)。由于 (a) 是单调不减的,且每次最多加 (1),这意味着在这个区间内,总共发生 (+1) 的次数,等于区间末尾的数值减去开头的数值。

2. 确定区间内的组合数

假设当前区间长度为 (d = p_i - p_{i-1})。区间起点的值为 (j = a_{p_{i-1}})。
在该区间内,基础的总和是 (d \times j)。若要使该区间的总和达到 (\Delta v),我们需要在区间内的一些位置发生 (+1)。
这等价于:从 (d) 个可发生 (+1) 的位置中,选出 (k) 个位置,使得这 (k) 个位置对总和的额外贡献为 (R = \Delta v - d \times j)。

这里的位置编号非常关键(类似于后缀和的概念):

  • 假设在倒数第 (x) 个位置发生 (+1),它对区间总和的贡献就是 (x)。
  • 也就是说,选出 (k) 个位置发生 (+1) 产生的总贡献,等价于从 ({1, 2, \dots, d}) 中选出 (k) 个不同的数,且和为 (R) 的方案数。

3. 预处理组合数(01 背包)

因为 (T) 高达 (10^4),我们必须在每次查询前 (O(1)) 或 (O(\log n)) 查表。因此需要预处理 comp 数组。
定义 comp[i][j][s] 表示从 ({1, 2, \dots, i-1}) 中选 (j) 个不同的数,使其和为 (s) 的方案数。
状态转移方程为(类似 01 背包):

\[comp[i][j][s] = comp[i-1][j][s-j] + comp[i-1][j-1][s-j] \]

注意:comp[i] 自带 (i-1) 个可选数字的特性。其第一维最大到 (100)。

4. DP 状态设计

定义 (dp[last][j]) 表示处理到上一个限制点(位置为 last),并且 (a_{last} = j) 时的方案数。

  • 初始状态:虚拟起点 (dp[0][0] = 1)。
  • 状态转移:对于限制点 (i),从 last 转移到 (p)。枚举上一段的结尾值 (j) 以及本段发生 (+1) 的次数 (k)。
  • 尾部处理:最后一段限制点 (p_m) 之后的 (n - p_m) 个位置完全没有限制,每个位置都可以自由选择 (+0) 或 (+1),所以最后总方案数要乘上 (2^{n - p_m})。

易错细节(避坑指南)

  1. 首段与后续段的“自由长度”不同:

    • 对于第一段(从起点到 (p_1)),因为固定了 (a_1 = 0),所以实际区间长度 (d = p_1),但可发生 (+1) 的自由位置只有 (p_1 - 1) 个。
    • 对于后续段,区间长度 (d = p_i - p_{i-1}),由于起点 (a_{last}) 是枚举的变量,该区间内每一个位置都可以发生 (+1),所以自由位置就是 (d) 个。
  2. comp 数组的“偏移一位”魔法:

    • 你的 comp 数组定义里,comp[i] 其实包含的是 (i-1) 个自由位置。
    • 所以,当你有 (len) 个自由位置时,你需要查的是 comp[len + 1]。
    • 代码里千万不要写成 if(comp[len] != 0) 但实际加 comp[len+1],会导致全选的情况被误判为 0 而漏解!
  3. DP 转移的基础总和计算:

    • 剩余要求 (R = \Delta v - j \times d)。这里必须乘实际区间长度 (d),不能乘自由位置数量。
  4. 越界与溢出处理:

    • 转移时判断 (k + j \le n) 防止数组越界。
    • 乘法时强转 long long,防止爆 int。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const ll MOD = 998244353;
const ll INF = 1e14;

int comp[105][105][5050];
int pow2[105]; // 长度为len时的可能
void init() {
    comp[1][0][0] = 1;
    for(int i = 2; i <= 100; i ++) { // 长度,也是处理完第i位的意思,a1 = 0固定
        for(int j = 0; j < i; j ++) { // j不会超过i
            for(int s = j; s <= 4950; s ++) { // 99 * 100 / 2
                ll y = comp[i - 1][j][s - j] + (j > 0 ? comp[i - 1][j - 1][s - j] : 0);
                //if(y == 0) break; // 这里的s没更新,后续也不会更新,进行剪枝
                //错误剪枝
                comp[i][j][s] = (y + comp[i][j][s]) % MOD;
            }
        }
    }
    pow2[0] = 1;
    for(int i = 1; i <= 100; i ++) {
        ll k = 2LL * pow2[i - 1] % MOD;
        pow2[i] = k;
    }
}

void solve() {
    int n, m; cin >> n >> m;
    vector<pii> lim;
    for(int i = 0; i < m; i ++) {
        int p, v; cin >> p >> v;
        lim.push_back({p, v});
    }
    for(int i = m - 1; i >= 1; i --) lim[i].second -= lim[i - 1].second;
    if(m == 0) {
        cout << pow2[n - 1] << "\n";
        return;
    }

    // dp[i][j], 前i个限制以j结尾的数量,vi = vi - v(i-1),这是数量限制,vi - len * j,这是剩下的要求,小于这个的都不行

    // ?? 前i个总和为s?,1e8复杂度可行
    //如何处理可能组合
    //预处理长度为len,总和为s的可能性?
    //这样对于res = vi - len*j可以快速求解,但是要知道新的结尾,len 100, s 10000, j 100,空间很勉强

    vector<vector<ll>> dp(n + 1, vector<ll>(n + 1, 0)); dp[0][0] = 1;
    for(int i = 0; i < m; i ++) {
        auto [p, v] = lim[i];
        int last = (i == 0 ? 0 : lim[i - 1].first);
        int d = p - last; // 实际长度
        int len = p - last - (i == 0); //自由长度。a1 = 0固定,i = 0是 len - 1
        for(int j = 0; j < n; j ++) {
            if(dp[last][j] == 0) continue;
            int res = v - j * d;
            if(res < 0) break;

            for(int k = 0; k <= len; k ++) {
                // comp中已经把首位固定,需要len位自由位,所以需要len + 1
                if(comp[len + 1][k][res] != 0  && k + j <= n) 
                    dp[p][k + j] = (dp[p][k + j] + dp[last][j] * comp[len + 1][k][res] % MOD) % MOD;
            }
        }
    }

    int rare = lim[m - 1].first;
    int e = n - rare;

    // 后续自由部分每一位都有两种可能,跟随上一位,增加1
    ll ans = 0;
    for(int j = 0; j < n; j ++) ans = (pow2[e] * dp[rare][j] + ans) % MOD;
    cout << ans << "\n";
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    init();
    int _ = 1; cin >> _;
    while(_--) solve();
    return 0;
}
posted @ 2026-11-03 21:55  thenrify  阅读(2)  评论(0)    收藏  举报