首先我们考虑对序列从小到大排序,那么一个集合的极差就可以用两端点之差来描述。
贡献显然是可以拆的,那么我们从左往右做动态规划,经典方式是维护一个“未闭合”的集合数量(即未维护右端点贡献),状态 $f(i,j,k)$ 表示维护到 $i$,$j$ 个区间未闭合,总贡献为 $k$。
转移分四类来做:
- 与一个未闭合区间合并并闭合
- 与一个未闭合区间合并但不闭合
- 新开一个区间且闭合
- 新开一个区间但不闭合
状态转移方程即为:
$$
\begin{aligned}
f(i,j,k) &= (j+1) \cdot f(i-1,j+1,k-a(i)) \\
&+ j \cdot f(i-1,j,k) \\
&+ f(i-1,j,k) \\
&+ f(i-1,j-1,k+a(i))
\end{aligned}
$$
但是由于最后一位的值域是 $\sum a_i$ 级别的,所以时间复杂度上不可取。
考虑到最后答案是 $\sum_{k=0}^m f(n,0,k)$,所以我们希望最后一维的值域在 $m$ 级别。
注意到如果我们保证最后一维转移递增,那么我们就达到了这个目的。
考虑不同的拆贡献方案,可以做一个差分再区间和起来,这样我们保证转移的最后一维递增。
设 $d=a(i) - a(i-1)$。
类似的转移:
$$
\begin{aligned}
f(i,j,k) &= (j+1) \cdot f(i-1,j+1,k-(j+1) \cdot d) \\
&+ j \cdot f(i-1,j,k-j \cdot d) \\
&+ f(i-1,j,k-j \cdot d) \\
&+ f(i-1,j-1,k-(j-1) \cdot d)
\end{aligned}
$$
滚动数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include<bits/stdc++.h> using namespace std; #define int long long const int maxn=200; const int maxm=1000; const int mod=998244353; int n,m; int a[maxn+5]; int f[2][maxn+5][maxm+5]; int ans; signed main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=n;i++) scanf("%lld",&a[i]); sort(a+1,a+n+1); f[0][0][0]=1; for(int i=1;i<=n;i++){ int d=a[i]-a[i-1]; for(int j=0;j<=i;j++){ for(int k=0;k<=m;k++){ f[1][j][k]=0; if(k-(j+1)*d>=0&&j+1<=i-1){ f[1][j][k]=(f[1][j][k]+(j+1)*f[0][j+1][k-(j+1)*d])%mod; } if(k-j*d>=0){ f[1][j][k]=(f[1][j][k]+j*f[0][j][k-j*d])%mod; f[1][j][k]=(f[1][j][k]+f[0][j][k-j*d])%mod; } if(k-(j-1)*d>=0&&j-1>=0){ f[1][j][k]=(f[1][j][k]+f[0][j-1][k-(j-1)*d])%mod; } if(i==n&&j==0) ans=(ans+f[1][j][k])%mod; } } swap(f[0],f[1]); } printf("%lld",ans); return 0; }
|