分析题面描述的过程。
如果按高度考虑,则该过程可以描述为:
我们从大到小遍历每种高度。维护一个集合。这个集合中,是初始高度大于当前高度,但是没有被保护起来,所以若干年后高度下降为了当前高度,的这些位置。从大到小遍历所有高度时,每次,我们会向集合中加入两个位置:也就是初始高度等于当前遍历到的高度的位置;然后再把集合里最大(最靠右)的那个位置弹出:被弹出的这个位置,从此以后将会一直被保护起来,所以在最终状态下,该位置的高度就是我们遍历的当前高度。而集合里的其他位置,将会随着我们的遍历,继续下降到更低的高度。
从这个过程,可以发现一些简单的性质。例如:因为共有(n)种高度,每种高度里我们会加入两个位置,弹出一个位置,所以最终被保护起来的位置,和下降到高度为(0)的位置(也就是遍历完所有高度后,集合里剩余的位置),两种位置各有(n)个。从这个过程也可以看出,我们每次会安排一个高度不同的位置被保护起来。所有最终被保护起来的(n)个位置上,在最终状态下的高度,一定是高度为(1dots n)各有(1)个。
这个过程非常优美。但这只是假定已知初始状态后的操作过程。当我们不知道初始状态时,想要对这个过程计数很困难。(至少我没有想到合适的DP状态)。那么,不妨换个角度来理解操作的过程:刚刚是按高度考虑,我们能不能按位置顺序考虑呢?于是这个过程又可以有另一个等价的描述:
我们从(2n)到(1)遍历每一个位置。设位置(i)在初始状态下的高度为(h_i)。我们要考虑其在最终状态下的高度。我们维护一个“未使用的高度集合”,表示集合里的高度,不是(i+1dots 2n)中任何一个位置上石柱的最终高度。那么,当前位置(i)上石柱的最终高度,就是“未使用的高度集合”里,小于等于(h_i)的最大高度。找到这个高度,并将其从集合里删除。特别地,如果“未使用的高度集合”里没有小于等于(h_i)的高度,则当前位置的最终高度为(0)。
对这个过程计数就相对容易。首先,自然想到的一个状态是:(dp[i][mask])表示从(2n)开始向前考虑到了第(i)个位置,当前未使用的高度集合为(mask),这种情况的方案数。转移时考虑位置(i)的初始高度(h_i)。分“当前位置最终是、否留下来了”两种情况讨论(注意,“最终是否留下来了”这一条件是题目输入中告诉我们的,相当于已经确定了)。
-
如果位置(i)最终没有留下来,那么:(mask)中,小于等于(h_i)的位置一定都已经被使用(也就是说(h_i)位于(mask)的一段前缀(0)当中)。设(mask)里前缀(0)的长度为(j),(i+1dots 2n)中没有被留下来的位置数量为( ext{cntBreak})。
由于不知道每种初始高度在我们DP的这个后缀里已经出现了多少次,所以我们在转移时先假设相同高度的两个元素是本质不同的,最后再从答案里除以(2^n)。你可以理解为,本来只需要从(j)个高度里选一个,但是现在我们把每个高度的两根柱子涂上了不同的颜色,所以变成在(2j)根柱子里选一个。
那么,(h_i)可以选择的元素(也就是柱子)数量为(2j)种。但是它不能选择之前选过的。这(2j)个元素中之前选过的有(j+ ext{cntBreak})个:
- 其中( ext{cntBreak})很好理解,这就是(i+1dots 2n)中没有被留下来的位置数量,每个位置上都一定有一个初始高度(leq j)的柱子。
- 而(j)是被留下来的柱子数量:为什么(i+1dots 2n)中被留下来的柱子里初始高度在(1dots j)的恰有(j)个?因为考虑一个最终高度(leq j)的被留下来的柱子:它的初始高度不可能(>j),否则它就不会下降到(leq j)的高度(因为高度(j+1)是未被使用的)。
所以,(h_i)真正可选的元素数量为:(2j-(j+ ext{cntBreak})=j- ext{cntBreak})。这也就是转移的系数。
-
再考虑如果位置(i)最终被留下来了。设位置(i)上石柱的最终高度为(x) ((x>j)),设(mask)中位置(x)后面连续的一段(0)的长度为(k)。那么转移的方案数就是(k+2)。因为如果(h_i=x),则有(2)种选择的方案(因为我们把相同高度的两根石柱看做本质不同的)。如果(h_i>x),则有(k)种高度可选(因为每种高度的其中一根石柱已经用掉了,只剩一根就不用再挑了)。
这样DP的时间复杂度(O(2^nn^2)),太慢了!考虑优化,就必须把(mask)砍掉。
没有了(mask),我们还能不能做这个DP呢?
-
对于第一种转移,也就是“位置(i)最终没有留下来”,它在(mask)中用到的信息只有:(mask)里前缀(0)的长度为(j)。我们完全可以把这个(j)放到DP状态的第二维,也就是设(dp[i][j])表示从(2n)开始向前考虑到了第(i)个位置,已选择的最终高度里从(1)开始的极长连续段长度为(j),此时的方案数。容易发现新状态完全不影响第一种转移。
-
对于第二种转移,也就是“位置(i)最终被留下来了”,当我们把状态简化为(dp[i][j])后,这种转移就会遇到一些小麻烦。因为我们枚举了当前石柱的最终高度(x)后,我们需要知道:“(mask)中位置(x)后面连续的一段(0)的长度”是多少。
-
考虑当(x>j+1)时,转移后对新状态下DP的第二维是没有改变的。这种转移的方案数我们暂时不计算。也就是直接令:(dp[i][j]=dp[i+1][j])。
-
当(x=j+1)时,我们不仅要统计当前转移的方案数,还要把(j+1)后面的一整个连续段,在转移时的方案数都计算进去。枚举这个连续段的长度,记为(k)(为了方便,这里的(k)是包含了(j+1)这个位置的,也就是说我们会从(dp[i+1][j])转移到(dp[i][j+k]))。
首先,根据前面对朴素DP的讨论,对于位置(x)来说,(h_x)有(k+1)种可选的元素。
设(i+1dots 2n)中被留下来的位置数量为( ext{cntProtect})。那么,我们需要从( ext{cntProtect}-j)个位置里选择(k-1)个,令这些位置的最终高度,构成了(x)后面长度为(k-1)的连续段。所以,转移系数要再乘以({ ext{cntProtect}-jchoose k-1})。
同时,还要考虑这(k-1)个元素,它们的初始高度分别是什么:也就是我们在(x>j+1)的这种转移里没有统计的方案数。容易发现,它们的初始高度一定在(j+2dots j+k)之间,也就是在长度为(k-1)的一段。那么,不同的(j)对这一方案数是没有影响的。所以不妨先记这个方案数为(f[k-1])(你可以假设它是我们预处理出来的一个数组)。
那么,我们可以得到一个转移式:
[dp[i][j+k]leftarrow dp[i+1][j]cdot (k+1)cdot{ ext{cntProtect}-jchoose k-1}cdot f[k-1] ]
-
现在的关键问题是如何预处理(f[k])。这其实比较简单。也可以做一个DP。设(g[i][j])表示考虑了(i)种初始高度,占用了(j)个位置(也就是(j)个最终被保留下来的最终高度)时的方案数。我们对此的限制条件是:
- 每种初始高度最多只有(2)个元素。也就是说,(g[i][j])只能从(g[i-1][j]) ,(g[i-1][j-1])和(g[i-1][j-2])转移。
- (i)种初始高度占用的总位置数,不能大于(i)。也就是说,(ileq j)。
所以得到转移式:(g[i][j]=g[i-1][j]+g[i-1][j-1]cdot 2j+g[i-1][j-2]cdot (j-1)cdot j)。
根据定义,我们要求的(f[k]),就等于(g[k][k])。
时间复杂度(O(n^3))。
参考代码(在LOJ查看):
//problem:LOJ3276
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const int MAXN=600*2;
const int MOD=1e9+7;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int& x,int y){x=mod1(x+y);}
inline void sub(int& x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}
int fac[MAXN+5],ifac[MAXN+5];
inline int comb(int n,int k){
if(n<k)return 0;
return (ll)fac[n]*ifac[k]%MOD*ifac[n-k]%MOD;
}
void facinit(int lim=MAXN){
fac[0]=1;
for(int i=1;i<=lim;++i)fac[i]=(ll)fac[i-1]*i%MOD;
ifac[lim]=pow_mod(fac[lim],MOD-2);
for(int i=lim-1;i>=0;--i)ifac[i]=(ll)ifac[i+1]*(i+1)%MOD;
}
int n,f[MAXN+5][MAXN+5],dp[MAXN+5][MAXN+5];
bool is_protected[MAXN+5];
int main() {
facinit();
cin>>n;
for(int i=1;i<=n;++i){
int pos;cin>>pos;
is_protected[pos]=true;
}
f[0][0]=1;
for(int i=1;i<=n;++i){
for(int j=0;j<=i;++j){
f[i][j]=f[i-1][j];
if(j>=1)add(f[i][j],(ll)f[i-1][j-1]*j*2%MOD);
if(j>=2)add(f[i][j],(ll)f[i-1][j-2]*(j-1)*j%MOD);
}
}
int cnt_protect=0,cnt_break=0;
dp[2*n+1][0]=1;
for(int i=2*n;i>=1;--i){
if(is_protected[i]){
for(int j=0;j<=cnt_protect;++j)if(dp[i+1][j]){
add(dp[i][j],dp[i+1][j]);
for(int k=1;j+k<=cnt_protect+1;++k){
//dp[i+1][j] -> dp[i][j+k]
add(dp[i][j+k],(ll)dp[i+1][j]*(k+1)%MOD*comb(cnt_protect-j,k-1)%MOD*f[k-1][k-1]%MOD);
}
}
cnt_protect++;
}
else{
for(int j=cnt_break+1;j<=cnt_protect;++j){
dp[i][j]=(ll)dp[i+1][j]*(j-cnt_break)%MOD;
}
cnt_break++;
}
}
int ans=(ll)dp[1][n]*pow_mod(pow_mod(2,n),MOD-2)%MOD;
cout<<ans<<endl;
return 0;
}