题解:P9197 [JOI Open 2016] 摩天大楼 / Skyscraper

显然的套路是连续段 dp。
但是要记录这个当前的加和不好做,这样的话记录的东西就到 \(O(n^3L)\) 级别。
我们希望这个东西是单增的,这样就可以压到 L。

我们考虑将一个点的贡献拆开算,即,我一个点如果是小的,那么我一直计算其贡献直到这个点被配对。

那这个怎么做呢?
考虑一个小没有配对的的点,定然是一个段的两侧,那么我每次更新,就要加上 \(更新前段数 * 2 * (a_i - a_{i-1})\)

这也即提前计算贡献,为什么对呢?因为我们有:$$a_i - a_j = \sum_{k=i+1}^j{a_k - a_{k-1}}$$

还要处理边界,如果放到边界,那么一者不考虑后面的贡献,二者段数边界要减去。

这道题让我真正意识到了,有些时候往外推状态真的会比往内算优,有时候式子特别复杂可以考虑换一下。

#include <bits/stdc++.h>
using namespace std;

#define int long long

const int Mod = 1e9 + 7;
const int N = 105;
const int M = 1e3 + 5;

int a[N];
int f[N][N][2][2][M];

void add(int &x, int y) {
	if (1ll * x + y > Mod) x = x + y - Mod;
	else x = x + y;
} 

signed main() {
	int n, l;
	cin >> n >> l;
	for (int i = 1;i <= n;i++) {
		cin >> a[i];
	}
	sort(a + 1, a + n + 1);
	a[0] = a[1];
	
	if (n == 1) {
		cout << 1;
		return 0;
	}
	
	for (int p = 0;p <= 1;p++) for (int q = 0;q <= 1;q++) f[1][1][p][q][0] = 1;
	for (int i = 1;i < n;i++) {
		for (int j = 1;j <= i;j++) {
			for (int p = 0;p <= 1;p++) {
				for (int q = 0;q <= 1;q++) {
					for (int k = 0;k <= l;k++) {
						int kk = k + (2 * j - p - q) * (a[i + 1] - a[i]);						
						
						if (kk > l) continue;
						
						add(f[i + 1][j + 1][p][q][kk], f[i][j][p][q][k] * (j - 1) % Mod);
						add(f[i + 1][j][p][q][kk], f[i][j][p][q][k] * (2 * j - 2) % Mod);
						add(f[i + 1][j - 1][p][q][kk], f[i][j][p][q][k] * (j - 1) % Mod);
						
						if (!p) {
							add(f[i + 1][j + 1][0][q][kk], f[i][j][p][q][k]);
							add(f[i + 1][j + 1][1][q][kk], f[i][j][p][q][k]);
						}
						if (!q) {
							add(f[i + 1][j + 1][p][0][kk], f[i][j][p][q][k]);
							add(f[i + 1][j + 1][p][1][kk], f[i][j][p][q][k]);
						}
						
						if (!p) {
							add(f[i + 1][j][0][q][kk], f[i][j][p][q][k]);
							add(f[i + 1][j][1][q][kk], f[i][j][p][q][k]);
						}
						if (!q) {
							add(f[i + 1][j][p][0][kk], f[i][j][p][q][k]);
							add(f[i + 1][j][p][1][kk], f[i][j][p][q][k]);
						}
						
//						int kk = k - (2 * j - p - q) * (a[i] - a[i - 1]);
//						int _k = k - (2 * (j - 1) - p - q) * (a[i] - a[i - 1]); 
//						int kkk = k - (2 * (j + 1) - p - q) * (a[i] - a[i - 1]); 
//						
////						if (kk >= 0) continue;
////						if (_k >= 0) continue;
////						if (kkk < 0) continue;
//						
//						if (_k >= 0) add(f[i][j][p][q][k], (j - 2) * f[i - 1][j - 1][p][q][_k] % Mod); // 加一段 
//						if (kk >= 0) add(f[i][j][p][q][k], (2 * j - 2) * f[i - 1][j][p][q][kk] % Mod);
//						if (kkk >= 0) add(f[i][j][p][q][k], j * f[i - 1][j + 1][p][q][kkk] % Mod);
//						
//						if (!p) {
//							if (kk >= 0) add(f[i][j][p][q][k], f[i - 1][j][p][q][kk]);
//							if (_k >= 0) add(f[i][j][p][q][k], f[i - 1][j - 1][p][q][_k]);
//						}
//						else {
//							if (_k >= 0) add(f[i][j][p][q][k], f[i - 1][j - 1][0][q][_k]);
//							if (kk >= 0) add(f[i][j][p][q][k], f[i - 1][j][0][q][kk]);
//						}
//						
//						if (!q)  {
//							if (kk >= 0) add(f[i][j][p][q][k], f[i - 1][j][p][q][kk]);
//							if (_k >= 0) add(f[i][j][p][q][k], f[i - 1][j - 1][p][q][_k]);
//						}
//						else {
//							if (_k >= 0) add(f[i][j][p][q][k], f[i - 1][j - 1][p][0][_k]);
//							if (kk >= 0) add(f[i][j][p][q][k], f[i - 1][j][p][0][kk]);
//						}
//						if (f[i][j][p][q][k]) cout << i << " " << j << " " << p << " " << q << " " << k << " : " << f[i][j][p][q][k] << "\n"; 
					}
				}
			}
		}
	}
	
	int ans = 0;
	for (int i = 1;i <= l;i++) (ans += f[n][1][1][1][i]) %= Mod;
	cout << ans;
	return 0;
}
/*
显然的套路是连续段 dp
但是要记录这个当前的加和不好做,这样的话记录的东西就到 n^3A 级别
我们希望这个东西是单增的,这样就可以压到 L
我们考虑将一个点的贡献拆开算,即,我一个点如果是小的,那么我一直计算其贡献直到这个点被配对
那这个怎么做呢?
考虑一个小的点,定然是一个段的两侧,那么我每次更新,就要加上 更新前段数 * 2 * (a[i] - a[i - 1])
还要处理边界,如果放到边界,那么一者不考虑后面的贡献,二者段数边界要减去 
*/
posted @ 2025-10-03 20:41  yanbinmu  阅读(7)  评论(0)    收藏  举报