P9197 [JOI Open 2016] 摩天大楼 - 题解
广告:笔者的《浅谈一类转移过程中依赖已插入元素的具体信息的序列计数问题 - 连续段 dp》
注意到绝对值求和的形式,因此考虑将权值
记当前所有相邻项的绝对值之和为
比较容易发现,每个未经过合并的连续段都是一个单谷序列(即
-
“边界”可以在合并两连续段时被添加,此时
k' \gets k + 2a_i ,因为它同时作为了左侧连续段的右边界及右侧连续段的左边界。或者将插入元素强行作为边界插入到到目前最左侧 / 最右侧的连续段的左端 / 右端上,此时k' \gets k + a_i 。 -
“中间”元素其实就是一个连续段中第一个被插入的元素,我们只需要在新建连续段时,
k' \gets k - 2a_i 。(注意在实现时必须分类讨论这个“中间”元素是否作为连续段的边界,如果“中间”元素同时作为边界的话,k' \gets k - a_i 即可。)
这样一来,我们完成了关于
- 作为一个新的连续段插入到不为边界的间隙中,即
f_{i + 1, j + 1, k - 2a_{i + 1}, d} \gets f_{i, j, k, d} \times (j + 1 - d) 。 -
-
-
-
最终答案为
然而,分析一下时间复杂度,记
瓶颈在哪里呢?前面提到
考虑一种差分的思想:
形式化一下,记
它其实就是个差分。但是从贡献的角度考虑也能归为一种叫微元贡献法的 trick。其本质为,面对多个不同的贡献,但我们可以用若干个微元组合出所有不同贡献时,可以考虑此类微元贡献法。
由于上文提到
由于确定一个边界后无法再向边界处插入新元素/连续段,因此边界的数量会降低此后新元素对总贡献的增量,仍然需要单独记录边界的数量
设计 dp 状态为
最后,考虑连续段 dp 的几种转移即可。记
- 作为一个新的连续段插入到不为边界的间隙中,即
f_{i + 1, j + 1, k', d} \gets f_{i, j, k, d} \times (j + 1 - d) 。 -
-
-
-
答案显然为
值得一提的是,上文提及的两种 dp 方法中,
k 都并非仅考虑a 中这前i 个元素互相之间的总贡献,而是提前计算了这些元素之后所能产生的一切贡献,从而规避了后效性。这里也分享一篇介绍这种贡献提前计算的好文。
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr int N = 100 + 10, MOD = 1e9 + 7, M = 1000 + 10;
inline void pl(int &x, i64 y) {x = (y + x) % MOD;}
int f[N][N][M][3], a[N];
void solve(){
int n, l; cin >> n >> l;
for(int i = 1; i <= n; i++)
cin >> a[i];
sort(a + 1, a + n + 1);
f[0][0][0][0] = 1;
for(int i = 0; i < n; i++) for(int j = 0; j <= i; j++)
for(int k = 0; k <= l; k++) for(int d = 0; d < 3; d++){
i64 p = k + (a[i + 1] - a[i]) * (2 * j - d), t = f[i][j][k][d];
if(!t) continue;
if(p > l) continue;
pl(f[i + 1][j + 1][p][d], t * (j + 1 - d));
if(j >= 2) pl(f[i + 1][j - 1][p][d], t * (j - 1));
if(j >= 1) pl(f[i + 1][j][p][d], t * (2 * j - d));
if(d < 2) pl(f[i + 1][j + 1][p][d + 1], t * (2 - d));
if(d < 2 && j >= 1) pl(f[i + 1][j][p][d + 1], t * (2 - d));
}
int ans = 0;
for(int i = 0; i <= l; i++)
pl(ans, f[n][1][i][2]);
cout << (n == 1 ? 1 : ans) << "\n";
}
UPD 2023.11.18: 感谢 @Missa 指出的一些细节错误。