Counting Arrays
Counting Arrays
题意简述
VegiCat 给你两个整数 \(n\) 和 (m)。考虑一个长度为 (n) 的整数数组 (a),满足:
- (a_1 = 0)
- (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]) 的和:
在区间内,数组元素只能加 (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] 自带 (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})。
易错细节(避坑指南)
-
首段与后续段的“自由长度”不同:
- 对于第一段(从起点到 (p_1)),因为固定了 (a_1 = 0),所以实际区间长度 (d = p_1),但可发生 (+1) 的自由位置只有 (p_1 - 1) 个。
- 对于后续段,区间长度 (d = p_i - p_{i-1}),由于起点 (a_{last}) 是枚举的变量,该区间内每一个位置都可以发生 (+1),所以自由位置就是 (d) 个。
-
comp数组的“偏移一位”魔法:- 你的
comp数组定义里,comp[i]其实包含的是 (i-1) 个自由位置。 - 所以,当你有 (len) 个自由位置时,你需要查的是
comp[len + 1]。 - 代码里千万不要写成
if(comp[len] != 0)但实际加comp[len+1],会导致全选的情况被误判为 0 而漏解!
- 你的
-
DP 转移的基础总和计算:
- 剩余要求 (R = \Delta v - j \times d)。这里必须乘实际区间长度 (d),不能乘自由位置数量。
-
越界与溢出处理:
- 转移时判断 (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;
}

浙公网安备 33010602011771号