#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); }