P5469 [NOI2019] 机器人
神而明之,存乎其人。——题记
题意
-
现有一个长度为
n 的数列a[n],a[i]\in[A[i],B[i]] 。 -
定义
pre[i]=\begin{cases} j_{max}\ that\ a[j]>a[i]\ (j<i)\\1\ (\nexists\ j) \end{cases} ,nxt[i]=\begin{cases} j_{min}\ that\ a[j]\geqslant\ a[i]\ (j>i)\\n\ (\nexists\ j) \end{cases} 。 -
定义
left[i]=i-pre[i]-1,right[i]=nxt[i]-i-1 。 -
求有多少种合法的数列使得
MAX_{i=1}^n\ abs(left[i]-right[i])\ \leqslant 2 。 -
我知道这个题面转化似乎让它更不可读了,不过多多担待吧...毕竟形式化难免会更加不可理解(逃)。
数据范围
解析
0.题意分析
-
乍一看也没什么,但若以普遍理性而论,我们可以假设存在一个全局最高点。
-
如果最高点有多个,我们取最靠右的一个(因为向左右限制不同)。
-
能够看出,这个最高点(称为
mid ,下同)可以走到1/n ,且其左右两边的区间互相走不到。 -
稍微手推一下可以发现,分奇偶讨论之后合法的
mid 不超过3 个,而且在区间的中位数左右。 -
我会分治!区间
[l,r] 合法=有合法mid +左区间合法+右区间合法。 -
联系一下数据范围。
我会爆搜!
1.dp 设计
-
令
\mathit{dp}_ {l,r,mx} 表示区间[l,r] 的最大值不超过mx 的总方案数,定义M=\max_{i=1}^nB_i ,则所求答案即为\mathit{dp}_ {1,n,M} 。 -
推一下转移式。
\mathit{dp}_ {l,r,mx}=\dots 额好像很难推,我们用一个辅助状态来推一下试试。 -
定义
\mathit{tp}_ {l,r,mx} 为区间[l,r] 的最大值恰好为mx 的方案数。 -
那么对于一个
\mathit{tp}_ {l,r,mx} ,感性理解一下,它的方案数应当等于每种拆分(每种mid ,注意此时的a_{mid}=mx )下左右两遍区间的合法方案数乘积。 -
形式化如下:
\mathit{tp}_ {l,r,mx}=\sum\limits_{mid\in[l,r]\ that\ \operatorname{abs}((mid-l)-(r-mid))\leqslant 2} (\sum\limits_{i=1}^{mx} \mathit{tp}_ {l,mid-1,i}) \times (\sum\limits_{i=1}^{mx-1} \mathit{tp}_ {mid+1,r,i})\quad (1) -
稍微观察实际意义,可以发现上式等价于
\mathit{tp}_ {l,r,mx}=\sum\limits_{mid} \mathit{dp}_ {l,mid-1,mx} \times \mathit{dp}_ {mid+1,r,mx-1}\quad (2) -
尝试把式左化成
\mathit{dp}_ {l,r,mx} :\mathit{dp}_ {l,r,mx}=\sum\limits_{i=1}^{mx} \sum\limits_{mid}(\mathit{dp}_ {l,mid-1,i} \times \mathit{dp}_ {mid+1,r,i-1)}\quad (3) -
把两个
\sum 换位:\mathit{dp}_ {l,r,mx}=\sum\limits_{mid}\sum\limits_{i=1}^{\min(mx,B_{mid})}(\mathit{dp}_ {l,mid-1,i}\times \mathit{dp}_ {mid+1,r,i-1)}\quad (4) -
好像我们走远了。
(4) 式看起来并不好转移,(3) 式也一样(固然可以前缀和优化,但我倾向于下面这种方式)。 -
从定义出发:
\mathit{dp}_ {l,r,mx}= \sum\limits_{i=1}^{mx} \mathit{tp}_ {l,r,i} 。 -
我们用方便转移的
(2) 式来转移罢。按区间转移,转移完毕后进行前缀和。 -
现在回过头来考虑我们的初始化问题。
-
对于
l=r 的区间,显然\mathit{tp}_ {l,r,A_i\sim B_i}=1 ,转化成dp 不难。 -
实际写的时候会发现,对于非常短的区间(如
len=2,3 ),它的mid 可能是它某一侧的端点,导致它拆出了l>r 的区间(称为虚区间,下同)。 -
这种情况下,其实另一侧的区间的方案数就是这种拆分对整体的总方案数的贡献。所以虚区间的
\mathit{tp}_ {l,r,0\sim M}=1 。注意,是从0 开始,因为可能有区间最大值为1 然后mid 在右端点的情况,这可能是合法的。 -
复杂度:状态数
n^2 (按区间来转移),单次转移O(M) ,共计O(n^2M) 。 -
对于
50 分部分的数据\approx9\times 10^8 ,不太能接受。考虑一下上面说的mid 选择很有限的限制,容易看出区间其实很稀疏。记搜实现或预处理出所有区间,记区间数为m ,打表可得m\leqslant 2518 ,O(mM) 完全能过。
2.dp 优化
-
好像转移式本身已经到极限了,我们也使用了正确的 dp 实现方式。下一步优化在哪里?
-
观察到这里有 10 分的数据中
A_i,B_i 完全一样,同时1\times 10^9 的范围算是让我们死了继续暴力 dpmx 这一维的心。 -
考虑把
\mathit{dp}_ {1,n,M} 的转移式展开,我们知道展开后形式完全一样(因为各个点B_i 都一样,枚举的范围没有区别)。做过\text{[APIO2016]划艇} 的同学应该看到这个数据就想到正解了。 -
大胆猜测,这是个
n 次左右的多项式!我们可以暴力 dp 出mx=1\sim n+(\approx10) 项,然后拉格朗日插值法! -
思路一下就打开了啊朋友们。
-
考虑存下所有的“dp 转移变化点”。容易看出这些点应该是
A_i 和B_i+1 ,排序并去重(该数组记为key )之后我们就得到了一系列左闭右开区间[key_i,key_{i+1}) ,区间内部的转移逻辑是完全一样的! -
考虑模仿
\text{[NOI2020]美食家} (因为我其实没打过划艇),连续段加速,断点暴力 dp! -
算一下复杂度。大概
2n 段,dp 部分每段大约O(mn) ,拉插每段要插n 次,单次n^2 ,总计O(n^2\times (m+n^2)) ,完全没救。 -
我会优化!参考
\text{CF622F The Sum of the k-th Powers} ,拉插在对应的x_i 取值连续时分母为阶乘,分子可做前后缀积。 -
而我们 dp 的那段刚好是连续的!乱搞把分母的逆元预处理出来,我们就可以
O(n) 插值了。 -
-
对于本做法,本题非常卡常! 据说(小小声)正解的前缀和部分要用下降幂,可惜这是 NOI+ 考点…额好吧就是我不会…
-
卡常技巧:
-
保证拉插不带对分母求逆元的
\log 。 -
-
拆
%mod,尽量少取模。 -
对区间长度是否需要插分类讨论。
-
示范代码
#include<bits/stdc++.h>
#define il inline
#define re register
#define b_s basic_string
#define For(i,a,b) for(re int i=(a);i<=(b);++i)
#define foR(i,a,b) for(re int i=(a);i>=(b);--i)
#define uint unsigned int
#define ll long long
#define ull unsigned long long
using namespace std;
il void rd(int &x){
x=0;bool f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=0;ch=getchar();}
while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
x=f?x:-x;
return;
}
const int maxn=307,maxm=2608;//maxm是最大区间数
const ll mod=1e9+7;
int n;
int A[307],B[307];
il ll qpow(ll x,ll t){
int ret=1;
while(t){
if(t&1) ret=ret*x%mod;
x=x*x%mod;
t>>=1;
}
return ret;
}
il int inv(int x){return qpow(x,mod-2);}
int id[maxn][maxn],toti;//0就是区间不存在
il bool ok(int l,int mid,int r){return abs((mid-l)-(r-mid))<=2;}
il void sinte(int l,int r){//本函数是用于区间初始化的递归函数。
if(id[l][r]) return;
id[l][r]=++toti;
if(l>=r) return;
For(mid,l,r)
if(ok(l,mid,r))
sinte(l,mid-1),sinte(mid+1,r);
}
int key[607],totk;
ll invfact[307];
int usek;
il void init(){//本函数负责读入,并参与到区间初始化和拉插初始化中。
rd(n); usek=n+1;
For(i,1,n){
rd(A[i],B[i]);
key[++totk]=A[i],key[++totk]=B[i]+1;//A[i]是到这里就变,B[i]是下一个才变。我们希望key里面存的每一个都是变的点,这样一来我们就可以取出左闭右开区间。
}
sort(key+1,key+1+totk);
totk=unique(key+1,key+1+totk)-key-1;//去重后结果。
sinte(1,n);
invfact[0]=1;
For(i,1,usek) invfact[i]=invfact[i-1]*inv(i)%mod;
}
int L,R,S,N;//当前考虑的闭区间的左右端点,该区间长度,计划中要dp出的长度
ll dp[maxn][maxm]; bitset<maxm> vis;//该区间是否在本轮dp中访问过
//dp[mx][now]表示第now个区间的最大值不超过mx的方案数。定义tp[mx][now]表示恰好为的方案数。
//对于非初始区间(r>l):
//因为我们在dfs内实际上是逐个区间转移,我们可以先这样转移:dp(实为tp)[mx][now]=∑mid [ (∑i=0~mx tp[i][left])*(∑i=0~mx-1 tp[i][right]) ]=∑mid (dp[mx][left]*dp[mx-1][right])
//然后我们对dp前缀和,因为dp[mx][now]=∑i=1~mx tp[i][now]。不用关心i=0,因为对于非初始区间那里一定是0(A[i]>=1)(更实质地,只有虚区间会用到那个)
il void dfs(int l,int r){
re int now=id[l][r]; if(vis[now]) return; vis[now]=1;
if(l>r){
For(i,0,N)
dp[i][now]=1;
return;
}
if(l==r){
if(A[l]<=L && R<=B[l])
For(i,1,N)
dp[i][now]=1;
}
if(l<r){
For(mid,l,r)
if(ok(l,mid,r)){
dfs(l,mid-1); dfs(mid+1,r);
if(A[mid]<=L && R<=B[mid])
For(i,1,N)
dp[i][now]=(dp[i][now] + dp[i][id[l][mid-1]] * dp[i-1][id[mid+1][r]]) % mod;
}
}
For(i,1,N){
dp[i][now]=dp[i-1][now]+dp[i][now];
if(dp[i][now]>=mod) dp[i][now]-=mod;
}
return;
}
ll son[maxn],sonqian[maxn],sonhou[maxn];
ll mu[maxn],invmu[maxn];
il void work(){
for(re int i=1;i<totk;++i){
L=key[i],R=key[i+1]-1,S=R-L+1;
if(S<=usek){
N=S;
dfs(1,n);
For(i,1,toti) dp[0][i]=dp[N][i];//每次都把上一段的结果放在0处,此时的1就代表L,相当于平移了整个dp数组
}
else{
N=usek;
dfs(1,n);
//首先我们处理分子 son 及其前后缀积。 拉插的代入值 x 应当为区间末端 R 。
sonqian[0]=sonhou[usek+1]=1;
For(j,1,usek){//因为这里1代表L,所以rj(j为1~usek的任意值)=j+L-1
son[j]=S-j;//实际的式子是son[j]=R-rj=R-L+1-j=S-j,由else知S>usek>=j,所以不用判负,而且 R<=1e9 ,所以也不用模
sonqian[j]=sonqian[j-1]*son[j]%mod;
}
foR(j,usek,1) sonhou[j]=sonhou[j+1]*son[j]%mod;
//然后我们来处理分母
For(i,1,usek){
if(((usek-i)&1)==0) invmu[i]=invfact[i-1]*invfact[usek-i]%mod;
else invmu[i]=mod-invfact[i-1]*invfact[usek-i]%mod;
}
for(re int i=1;i<=usek;++i){
ll fenzi=sonqian[i-1]*sonhou[i+1]%mod;
ll xishu=fenzi*invmu[i]%mod;
For(now,1,toti)//暂时放在这里
dp[N+1][now]=(dp[N+1][now]+xishu*dp[i][now])%mod;
}
For(now,1,toti) dp[0][now]=dp[N+1][now],dp[N+1][now]=0;
}
vis.reset();
For(i,1,N)
For(j,1,toti)
dp[i][j]=0;//清空dp。
}
}
int main(){
init();
work();
wt(dp[0][1]);
return 0;
}
总结与反思
1.知识点
-
学到了拉插优化 dp。
-
学到了拉插的强行
O(n) 做法。
2.解题中的失误
3.其他
- 码力超级加倍(希望)。至少是被折磨惨了。
后记
-
神而明之,存乎其人。一定要保持清醒,不管是在代码的字里行间,还是...
-
因为山就在那里。