1. 程式人生 > 其它 >#dp,排列#LOJ 2743「JOI Open 2016」摩天大樓

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

dp,排列

題目

將互不相同的 \(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);
}