#dp,排列#LOJ 2743「JOI Open 2016」摩天大楼

题目

将互不相同的 \(n\) 个数重排,使得相邻两数差的总和不超过 \(L\) 的有多少种方式。

\(n\leq 100,L\leq 1000\)


分析

对于排列的问题,有一种很妙的方法就是从小到大插入,

若升序数列 \(B\)\(B_{i+1}-B_i\) 对答案产生贡献

当且仅当相邻两数 \(x\leq B_i,y\geq B_{i+1}\)

那么在 \(B_1\)\(B_i\) 排列后可以添加的位置就能产生贡献,

也就是与段数有关,而且要考虑左右边界。

\(dp[i][j][k][opt]\) 表示升序后前 \(i\) 个数,依次被分成 \(j\) 段,

目前确定 \(opt\) 个边界(边界不能新开一段),总和为 \(k\) 的方案数。

\(i-1\) 过渡到 \(i\) 的贡献即是 \(t=(j*2-opt)*(B_{i}-B_{i-1})\)

  1. 新开一段(不充当边界): \(dp[i][j+1][k][opt]+=dp[i-1][j][k-t][opt]*(j+1-opt)\)

  2. 合并一段:\(dp[i][j-1][k][opt]+=dp[i-1][j][k-t][opt]*(j-1)\)

  3. 将这个数放在段首或段尾(不包含边界): \(dp[i][j][k][opt]+=dp[i-1][j][k-t][opt]*(j*2-opt)\)

  4. 将这个数新开一段并作为边界: \(dp[i][j+1][k][opt+1]+=dp[i-1][j][k-t][opt]*(2-opt)\)

  5. 将这个数作为边界但不新开一段: \(dp[i][j][k][opt+1]+=dp[i-1][j][k-t][opt]*(2-opt)\)

最后答案为 \(\sum_{i=0}^L dp[n][1][i][2]\),注意当 \(n=1\) 时要特判。


代码

#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
const int N=111,mod=1000000007;
int dp[N][N][N*10][3],n,m,a[N],ans;
int iut(){
	int ans=0; char c=getchar();
	while (!isdigit(c)) c=getchar();
	while (isdigit(c)) ans=ans*10+c-48,c=getchar();
	return ans;
}
void Mo(int &x,int y){x=x+y>=mod?x+y-mod:x+y;}
int main(){
	n=iut(),m=iut();
	if (n==1) return !printf("1");
	for (int i=1;i<=n;++i) a[i]=iut();
	sort(a+1,a+1+n),a[0]=a[1];
	dp[0][0][0][0]=1;
	for (int i=1;i<=n;++i)
	for (int j=0;j<i;++j)
	for (int opt=0;opt<3;++opt){
		if (j*2<opt) break;
		int t=(j*2-opt)*(a[i]-a[i-1]);
		for (int k=t;k<=m;++k)
		if (dp[i-1][j][k-t][opt]){
			int now=dp[i-1][j][k-t][opt];
			Mo(dp[i][j+1][k][opt],(j+1ll-opt)*now%mod);
			Mo(dp[i][j][k][opt],(j*2ll-opt)*now%mod);
			if (j) Mo(dp[i][j-1][k][opt],(j-1ll)*now%mod);
			if (opt==2) continue;
			Mo(dp[i][j+1][k][opt+1],(2ll-opt)*now%mod);
			Mo(dp[i][j][k][opt+1],(2ll-opt)*now%mod);
		}
	}
	for (int i=0;i<=m;++i) Mo(ans,dp[n][1][i][2]);
	return !printf("%d",ans);
}
posted @ 2021-12-31 12:05  lemondinosaur  阅读(33)  评论(0编辑  收藏  举报