#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})\)
-
新开一段(不充当边界): \(dp[i][j+1][k][opt]+=dp[i-1][j][k-t][opt]*(j+1-opt)\)
-
合并一段:\(dp[i][j-1][k][opt]+=dp[i-1][j][k-t][opt]*(j-1)\)
-
将这个数放在段首或段尾(不包含边界): \(dp[i][j][k][opt]+=dp[i-1][j][k-t][opt]*(j*2-opt)\)
-
将这个数新开一段并作为边界: \(dp[i][j+1][k][opt+1]+=dp[i-1][j][k-t][opt]*(2-opt)\)
-
将这个数作为边界但不新开一段: \(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);
}