CSP-S 模拟76
最简单的T1没去做,T2,T3倒是A了
将序列分成B块,每块长度最大为A,每一个块都是连续的上升序列,且前一个块所有权值大于后边所有块的所有权值
也就是类似与(8,9,10)(5,6,7)(2,3,4))(1)的样子,显然最长上升子序列不超过A,所有块中取出一个值组成最长下降子序列B
#include<iostream> #include<cstdio> using namespace std; int T,n,A,B,a[110000]; int main(){ scanf("%d",&T); while(T--){ scanf("%d%d%d",&n,&A,&B); if(n-A<B-1||A*B<n){ puts("No"); continue; } for(register int i=1,j=n-A+1;i<=A;i++,j++) a[i]=j; if(B>1){ int las=n-A+1; int bg=A; for(register int i=B-1;i>=1;i--){ int w=min(las-i,A); bg+=w; for(register int j=1,k=bg;j<=w;j++,k--){ a[k]=--las; } } } puts("Yes"); for(register int i=1;i<=n;i++) printf("%d ",a[i]); puts(""); } }
显然$k\in[(sum+1)/2,sum] $,可以做一个背包(大神说的,我不知道什么叫背包QWQ),维护k的取值区间,加上一个a[i],就是将所有区间基础上加上[(a[i]+1)/2,a[i]](保留原来区间,加入新的区间),会发现会有一堆连起来的区间
显然是个板子——Old Driver tree (老司机树)——又称柯朵莉树
#include<iostream> #include<cstdio> #include<set> #include<algorithm> #include<vector> using namespace std; struct node{ long long l,r; bool operator < (const node x)const{ return (l<x.l)||(l==x.l&&r<x.r); } }; set<node>s; vector<node>v; int n,a[110000]; void add(int x){ auto it=s.begin(); for(;it!=s.end();it++){ node w=*it; v.push_back((node){w.l+(x+1)/2,w.r+x}); } while(v.size()){ s.insert(v.back()); v.pop_back(); } } void split(){ auto it=s.begin(); for(;it!=s.end();){ auto L=it; it++; if(it==s.end()) break; if((*L).r>=(*it).l){ node w=(node){(*L).l,(*it).r}; s.erase(it); s.erase(L); s.insert(w); it=s.lower_bound(w); } } } int main(){ //freopen("2.in","r",stdin); //freopen("2.out","w",stdout); scanf("%d",&n); for(register int i=1;i<=n;i++) scanf("%d",&a[i]); s.insert((node){0,0}); for(register int i=1;i<=n;i++){ add(a[i]); split(); } long long ans=0; auto it=s.begin(); for(;it!=s.end();it++){ if((*it).l==0&&(*it).r==0) continue; ans+=((*it).r-(*it).l+1); } printf("%lld\n",ans); }
首先要知道: 前序遍历——根,左,右
中序遍历——左,根,右
后序遍历——左,右,根
对于$M=0$即无限制的情况,我们只需要考虑前序遍历,对于一个前序遍历$[l,r]$,第一个位置$l$一定是根,从第二个位置开始到某一个位置是左子树,从该位置后一个位置到最后是右子树,可以枚举$i\in[l,r]$,$[l+1,i]$为左子树,$[i+1,r]$为右子树,那么方案数是$\sum\limits_{i=l}^{r}f[l+1][i]*f[i+1][r]$,包含无左子树和无右子树的情况
那么就可以不断划分子问题,知道l==r或l>r,返回1即可
但是这样会T成狗,可以发现对于任意长度为len的序列,在没有限制的情况下方案数是固定的,所以用数组f[len]记录长度为len的区间的方案数,记忆化搜索一下会快到飞起
考虑加上限制,限制有两种: 在中序遍历中 $1.$$u$先于$v$出现 $2.$$u$晚于$v$出现
因为先序遍历是从$1$到$n$,$u$,$v$之间有谁大谁小的不同情况
我们让它一边倒,只考虑$v<u$的情况,因为先序遍历是从小的为根再考虑到大的,所以$v>u$的情况会在$u$的位置考虑到
对于$v<u$的情况,在先序遍历所能构造的二叉树中,$u$要么在$v$的某一个祖宗的右子树中,要么在v的子树中——左子树或右子树
- 对于中序遍历,$u$先于$v$出现,考虑上面的情况,$u$必定在$v$的左子树中,那么在划分$[v,r]$的序列时,$[v+1,u]$的区间只能是左子树,即枚举$v$的左右子树的分界线至少要在$u$及其以后才行
- 对于中序遍历,$u$晚于$v$出现,同样考虑上面的情况,$u$只能在$v$的祖宗的右子树,或v的右子树中。
- 对于第一种在祖宗的右子树中,在考虑祖宗的子树的时候,$v$,$u$不会被划分到一个序列里,因此在$v$为根的时候不需要考虑;
- 对于第二种在$v$的右子树中,和$u$先与$v$出现类似,$u$必须是右子树,所以$[u,r]$只能是右子树,即枚举$v$的左右子树的分界线最大也要小于$u$
综上所述,我们可以预处理出$L[x]$,$R[x]$,分别表示以$x$做根,枚举左右子树边界i的范围,然后dfs搜索即可,而对于记忆化,因为现在对于长度相等的序列在有限制的条件下方案不一样,但是对于一个区间$[l,r]$的方案数是肯定不变的,所以记忆化$f[l][r]$即可
$L[x]$,为大于x的里边最后一个在中序遍历中先于$x$出现的点
$R[x]$,为大于$x$的里边第一个在中序遍历中晚于$x$出现的点的前一个位置
注意如果$R[x]$大于当前以$x$打头的序列$[l,r]$的$r$,那么说明$R[x]$那些晚于$x$出现的点在$x$的祖宗的右子树中,所以枚举分界线的右边界为$min(R[x],r)$
#include<iostream> #include<cstdio> #include<cstring> using namespace std; int T,n,m,a[410][410],L[410],R[410]; const long long mod=1e9+7; long long f[410],dp[410][410]; long long dfs(int l,int r){ if(l>r) return f[0]=1; if(l==r) return f[1]=1; if(f[r-l+1]) return f[r-l+1]; long long ans=0; for(register int i=l;i<=r;i++){ f[r-l+1]=(f[r-l+1]+dfs(l+1,i)*dfs(i+1,r)%mod)%mod; } f[r-l+1]%=mod; return f[r-l+1]; } long long DFS(int l,int r){ if(l>r) return 1; if(L[l]>r) return 0; if(l==r) return 1; if(dp[l][r]!=-1) return dp[l][r]; dp[l][r]=0; for(register int i=L[l];i<=min(R[l],r);i++){ dp[l][r]=(dp[l][r]+DFS(l+1,i)*DFS(i+1,r)%mod)%mod; } return dp[l][r]; } int main(){ scanf("%d",&T); while(T--){ scanf("%d%d",&n,&m); if(m==0) printf("%lld\n",dfs(1,n)); else{ memset(a,0,sizeof(a)); memset(dp,-1,sizeof(dp)); for(register int i=1,x,y;i<=m;i++) scanf("%d%d",&x,&y),a[x][y]=1; for(register int i=1;i<=n;i++){ L[i]=i,R[i]=i; for(register int j=i+1;j<=n;j++){ if(a[j][i]) L[i]=j; } for(register int j=i+1;j<=n;j++){ if(a[i][j]) break; R[i]=j; } } printf("%lld\n",max(DFS(1,n),0ll)); } } }