[JOI Open 2016] 摩天大楼

一、题目

点此看题

二、解法

这种绝对值求和可以当成一种模型来积累了,套路是微元贡献法(在 \(\tt CF\) 的一道网络流题也出现过)

我们先把权值离散化,对于离散化后的 \(i<j\)\(|v_j-v_i|=\sum_{k=i}^{j-1}v_{k+1}-v_{k}\),那么 \(v_{k+1}-v_k\) 的贡献是满足 \(i\leq k,k+1\leq j\) 的对数。根据这个转化题意,我们按从小到大插入 \(a\) 构成序列,那么 \(v_{k+1}-v_k\) 的贡献次数就是前 \(k\) 个数构成连续段的端点个数,因为端点旁边以后一定会插入比 \(k\) 大的数,就会产生贡献,有点费用提前计算的感觉了。

这提示我们可以使用连续段 \(dp\) 来解决这个问题,因为边界(端点是 \(1/n\))并不会产生贡献所以我们要把它单独记录,设 \(f[i][j][k][d]\) 表示插入了 \(i\) 个数,有 \(j\) 个连续段,贡献和是 \(k\),有 \(d\) 个端点的方案数,每次增量法考虑一个新数的插入,新的贡献和为 \(k'=k+(v_{i+1}-v_i)\times(2j-d)\)

  • 作为一个新的连续段插入到不为边界的空隙中:\(f[i+1][j+1][k'][d]\leftarrow f[i][j][k][d]\times (j+1-d)\)
  • 合并两个连续段:\(f[i+1][j-1][k'][d]\leftarrow f[i][j][k][d]\times(j-1)\)
  • 添加到某个连续段非边界的端点处:\(f[i+1][j][k'][d]\leftarrow f[i][j][k][d]\times(2j-d)\)
  • 作为一个新的连续段,插到边界处:\(f[i+1][j+1][k'][d+1]\leftarrow f[i][j][k][d]\times (2-d)\)
  • 添加到某个连续段作为边界:\(f[i+1][j][k'][d+1]\leftarrow f[i][j][k][d]\times(2-d)\)

时间复杂度 \(O(n^2k)\),最后答案显然是 \(\sum f[n][1][i][2]\),但是要特判 \(n=1\)

三、总结

对于大小关系之类的限制(如绝对值),可以离散化之后取微元再贡献回去。

连续段 \(dp\) 是解决序列计数问题的利器,学会主动使用它。

#include <cstdio>
#include <algorithm>
using namespace std;
const int M = 105;
const int N = 1005;
const int MOD = 1e9+7;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,a[M],f[M][M][N][3];
void add(int &x,int y) {x=(x+y)%MOD;}
signed main()
{
	n=read();m=read();
	if(n==1) {puts("1");return 0;}
	for(int i=1;i<=n;i++)
		a[i]=read();
	sort(a+1,a+1+n);
	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<=m;k++) for(int d=0;d<=2;d++)
		{
			int kk=k+(2*j-d)*(a[i+1]-a[i]),t=f[i][j][k][d];
			if(kk>m || !t) continue;
			//Case1:insert an seg without forming boundary
			add(f[i+1][j+1][kk][d],t*(j+1-d));
			//Case2:merge two seg
			if(j) add(f[i+1][j-1][kk][d],t*(j-1));
			//Case3:add to a seg
			if(j) add(f[i+1][j][kk][d],t*(2*j-d));
			//Case4:insert an seg,forming boundary
			if(d<2) add(f[i+1][j+1][kk][d+1],t*(2-d));
			//Case5:add to a seg,forming boundary
			if(d<2 && j) add(f[i+1][j][kk][d+1],t*(2-d));
		}
	for(int i=0;i<=m;i++) add(ans,f[n][1][i][2]);
	printf("%lld\n",ans);
}
posted @ 2021-09-01 20:27  C202044zxy  阅读(584)  评论(0编辑  收藏  举报