zoukankan      html  css  js  c++  java
  • 题解 LOJ3276 「JOISC 2020 Day2」遗迹

    题目链接

    分析题面描述的过程。

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

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

    从这个过程,可以发现一些简单的性质。例如:因为共有(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;
    }
    
  • 相关阅读:
    Mercury产品介绍
    操纵txt文本文件
    MOSS开发辅助小工具
    Notes 8/8.5 超慢解决之道的最佳实践
    实战OO设计——OO设计原则
    SQL Server XML 拆分示例
    认识IL
    javascript 面向对象特性与编程实现
    MTV
    C#轻松仿造Vista风格窗体_cici 自娱自乐
  • 原文地址:https://www.cnblogs.com/dysyn1314/p/12877113.html
Copyright © 2011-2022 走看看