题解 LOJ3276 「JOISC 2020 Day2」遗迹

题目链接

分析题面描述的过程。

如果按高度考虑,则该过程可以描述为:

我们从大到小遍历每种高度。维护一个集合。这个集合中,是初始高度大于当前高度,但是没有被保护起来,所以若干年后高度下降为了当前高度,的这些位置。从大到小遍历所有高度时,每次,我们会向集合中加入两个位置:也就是初始高度等于当前遍历到的高度的位置;然后再把集合里最大(最靠右)的那个位置弹出:被弹出的这个位置,从此以后将会一直被保护起来,所以在最终状态下,该位置的高度就是我们遍历的当前高度。而集合里的其他位置,将会随着我们的遍历,继续下降到更低的高度。

从这个过程,可以发现一些简单的性质。例如:因为共有\(n\)种高度,每种高度里我们会加入两个位置,弹出一个位置,所以最终被保护起来的位置,和下降到高度为\(0\)的位置(也就是遍历完所有高度后,集合里剩余的位置),两种位置各有\(n\)个。从这个过程也可以看出,我们每次会安排一个高度不同的位置被保护起来。所有最终被保护起来的\(n\)个位置上,在最终状态下的高度,一定是高度为\(1\dots n\)各有\(1\)个。

这个过程非常优美。但这只是假定已知初始状态后的操作过程。当我们不知道初始状态时,想要对这个过程计数很困难。(至少我没有想到合适的DP状态)。那么,不妨换个角度来理解操作的过程:刚刚是按高度考虑,我们能不能按位置顺序考虑呢?于是这个过程又可以有另一个等价的描述:

我们从\(2n\)\(1\)遍历每一个位置。设位置\(i\)在初始状态下的高度为\(h_i\)。我们要考虑其在最终状态下的高度。我们维护一个“未使用的高度集合”,表示集合里的高度,不是\(i+1\dots 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+1\dots 2n\)没有被留下来的位置数量为\(\text{cntBreak}\)

    由于不知道每种初始高度在我们DP的这个后缀里已经出现了多少次,所以我们在转移时先假设相同高度的两个元素是本质不同的,最后再从答案里除以\(2^n\)。你可以理解为,本来只需要从\(j\)个高度里选一个,但是现在我们把每个高度的两根柱子涂上了不同的颜色,所以变成在\(2j\)根柱子里选一个。

    那么,\(h_i\)可以选择的元素(也就是柱子)数量为\(2j\)种。但是它不能选择之前选过的。这\(2j\)个元素中之前选过的有\(j+\text{cntBreak}\)个:

    • 其中\(\text{cntBreak}\)很好理解,这就是\(i+1\dots 2n\)中没有被留下来的位置数量,每个位置上都一定有一个初始高度\(\leq j\)的柱子。
    • \(j\)是被留下来的柱子数量:为什么\(i+1\dots 2n\)中被留下来的柱子里初始高度\(1\dots j\)的恰有\(j\)个?因为考虑一个最终高度\(\leq j\)的被留下来的柱子:它的初始高度不可能\(>j\),否则它就不会下降到\(\leq j\)的高度(因为高度\(j+1\)是未被使用的)。

    所以,\(h_i\)真正可选的元素数量为:\(2j-(j+\text{cntBreak})=j-\text{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+1\dots 2n\)中被留下来的位置数量为\(\text{cntProtect}\)。那么,我们需要从\(\text{cntProtect}-j\)个位置里选择\(k-1\)个,令这些位置的最终高度,构成了\(x\)后面长度为\(k-1\)的连续段。所以,转移系数要再乘以\({\text{cntProtect}-j\choose k-1}\)

      同时,还要考虑这\(k-1\)个元素,它们的初始高度分别是什么:也就是我们在\(x>j+1\)的这种转移里没有统计的方案数。容易发现,它们的初始高度一定在\(j+2\dots j+k\)之间,也就是在长度为\(k-1\)的一段。那么,不同的\(j\)对这一方案数是没有影响的。所以不妨先记这个方案数为\(f[k-1]\)(你可以假设它是我们预处理出来的一个数组)。

      那么,我们可以得到一个转移式:

      \[dp[i][j+k]\leftarrow dp[i+1][j]\cdot (k+1)\cdot{\text{cntProtect}-j\choose 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\)。也就是说,\(i\leq 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;
}
posted @ 2020-05-12 16:40  duyiblue  阅读(783)  评论(3编辑  收藏  举报