题目大意
求长度为 \(N\) 的排列使得 \(\sum_{i = 1}^{N - 1} |A_{p_{i + 1}} - A_{p_i}| \leq L\) 有多少个,对 \(10^9 + 7\) 取模。
\(1 \leq N \leq 100\) ,\(1 \leq L, A_i \leq 1000\) 。
思路分析
对于此题比较简单的能看出是插入 dp(分析及做题经验详见插入 dp 学习笔记),定义也简单,难在如何优化。
先来做暴力 dp ,首先因为绝对值不好处理,所以要排序(从大到小、从小到大都行,笔者是从大到小),然后根据插入 dp 的模板定义 \(dp_{i, j}\) 表示将前 \(i\) 个数插入了序列中,形成了 \(j\) 个连续段的方案数,同时此题要求和不超过 \(L\) ,所以再加一维 \(k\) 表示目前和为多少,即 \(dp_{i, j, k}\) 表示 \(i\) 个数,\(j\) 个段,和为 \(k\) 。
考虑转移,发现我们将 \(i\) 插入序列中时对于相邻 \(0/1/2\) 个数是不知道的,而这些数要么是以前加的要么是未来要加的,于是进行费用提前计算。
在插入 \(i\) 时判断 \(i\) 是新建 \(/\) 拓展 \(/\) 合并,分别对应贡献 \(2 A_i / 0 / -2 A_i\) ,为什么呢?因为新建意味着 \(i\) 左右两边要插的数都在未来,我们又排了序,所以都比 \(A_i\) 小,贡献 \(2 A_i\) ;拓展说明一边比 \(A_i\) 小,一边比 \(A_i\) 大,贡献抵消;合并说明两边都已安排好了,都比 \(A_i\) 大,要减去 \(A_i\) ,所以贡献 \(-2 A_i\) 。但------真的是这样吗?仔细考虑可以发现若加在首尾,则有一边是没有的,所以还要在状态中加两维 \(0/1\) 表示首尾确定 \(/\) 没确定,同时在转移中按贡献计算方法特殊处理即可。
恭喜你,成功 \(\text{TLE}\) or \(RE\) or \(MLE\) 了此题,在 \(k\) 这一维中,由于贡献存在负数,所以可能加得很大很大再减回来,或是减成负数又加回可接受范围,聪明的你肯定想到加偏移量吧,再次恭喜你,时空来到 \(O(N^3V)\) ,即使滚动时间也会爆炸,所以要对此进行优化。
转移优化
我们发现问题出在 \(k\) 这一维有正有负,所以 \(\leq L\) 这个限制没起到作用,若全是正数的话就可以不管 \(k > L\) 的状态了,那怎么才能做到呢?
如果你对费用提前计算中正负统一这一类优化经验丰富,那你肯定能想到差分优化,将我们在首尾一加一减两次贡献拆成若干个段之和,即将化为原序列差分数组,通过把 \(A_i - A_j\) 变为 \(\sum_{k = i}^{j - 1} A_k - A_{k + 1}\) 来去掉负号,果然!我束手无策了......
符号去掉了,但如何维护新的这一求和又成了个问题(费用提前计算跟没计算一样QAQ),转换一下思路,我们不对于 \(j\) 求两边的 \(\sum\) ,我们分析 \(A_i - A_{i + 1}\) 对总和的贡献。可以发现在状态 \(dp_{i, j, k, p, q}\) 时,无论 \(j\) 个段什么时候合并,最终都会变成 \(1\) 个段,并且都是靠第 \(> i\) 个数合并的,这意味着什么,这意味着 \(j - 1\) 次合并时求 \(\sum\) 一定包含有 \(A_i - A_{i + 1}\) 这一项啊!同时对于首尾,若还可以加,就会在以后关闭时计算到 \(A_i - A_{i + 1}\) 。所以从此状态无论转移到哪一个状态时,都 \(k \leftarrow k + 2(j - 1)(A_i - A_{i + 1}) + p + q\) 。
形式化的说,假设 \(j\) 个连续段中第 \(o\) 个段和第 \(o + 1\) 个段会在加入第 \(u_o\) 个数时合并,那我们对于这 \(j\) 个段对总和的贡献即为 \(\sum_{o = 1}^{j - 1} \sum_{v = i}^{u_o - 1} 2(A_v - A_{v + 1})\) (首尾特殊情况特殊处理),转化一下变成 \(\sum_{v = i}^n 2(A_v - A_{v + 1}) \sum_{o = 1}^{j - 1} [v \leq u_o - 1]\) ,我们发现 \(\sum_{o = 1}^{j - 1} [v \leq u_o - 1]\) 在 \(dp_v\) 这一层时 \(v > u_o - 1\) 的都已经被合并了,所以处于 \(dp_v\) 这一层时上式变成了 \(dp_{v, j, \dots}\) 中的 \(j\) 。故这 \(j\) 个段的贡献为 \(\sum_{v = i}^n 2 j_v (A_v - A_{v + 1})\)(\(j_v\) 表示这 \(j\) 个段到 \(dp_v\) 这一层状态时有多少个连续段),显而易见,\(2 j_v (A_v - A_{v + 1})\) 就不是我们现在该管的,而是应下放到 \(dp_v\) 这一层时处理的,所以本层只需处理 \(A_i - A_{i + 1}\) ,贡献即 \(2(j - 1)(A_i - A_{i + 1}) + p + q\) 。
solution
到这里我们就全部做完啦!但此题传承了插入 dp 一贯的转移方程多,所以要注重一下细节,或者通过循环找内在逻辑来减少手搓的转移方程以简化代码。
cpp
/*
address:https://www.becoder.com.cn/problem/4744
AC 2025/8/13 14:06
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 105, V = 1005;
const int mod = 1e9 + 7;
inline void trans(int& x, int y) { x = (x + y) % mod; }
LL dp[N][N][V][2][2];
inline void trans(LL& x, LL y) { x = (x + y) % mod; }
int n, m;
int a[N];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1;i <= n;++i) scanf("%d", &a[i]);
if (n == 1) return puts("1"), 0; //注意要特判 n = 1
sort(a + 1, a + n + 1, greater<int>());
dp[1][1][0][1][0] = dp[1][1][0][0][1] = dp[1][1][0][1][1] = 1;
for (int i = 1;i < n;++i)
for (int j = 1;j <= i;++j)
for (int k = 0;k <= m;++k)
for (int p = 0;p < 2;++p)
for (int q = 0;q < 2;++q) {
const int v = k + (a[i] - a[i + 1]) * ((j - 1 << 1) + p + q);
if (v <= m) {
trans(dp[i + 1][j][v][p][q], dp[i][j][k][p][q] * ((j - 1 << 1) + p + q)); //拓展且不改变首尾闭不闭合
trans(dp[i + 1][j + 1][v][p][q], dp[i][j][k][p][q] * (j - 1)); //新建非首尾的段
if (j > 1) trans(dp[i + 1][j - 1][v][p][q], dp[i][j][k][p][q] * (j - 1)); //合并段
if (p) {
trans(dp[i + 1][j][v][!p][q], dp[i][j][k][p][q]); //拓展且闭合头部
trans(dp[i + 1][j + 1][v][p][q], dp[i][j][k][p][q]);
trans(dp[i + 1][j + 1][v][!p][q], dp[i][j][k][p][q]);
//新建在头部
}
if (q) {
trans(dp[i + 1][j][v][p][!q], dp[i][j][k][p][q]);
trans(dp[i + 1][j + 1][v][p][q], dp[i][j][k][p][q]);
trans(dp[i + 1][j + 1][v][p][!q], dp[i][j][k][p][q]);
//同理
}
}
}
LL ans = 0;
for (int i = 0;i <= m;++i) trans(ans, dp[n][1][i][0][0]);
printf("%lld", ans);
return 0;
}