Codeforces Round #740 (Div. 1) 部分题题解
B.Up the Strip
题目链接
简要题解
我们可以自然地设出状态\(F[i]\),表示有多少种方案能移动到\(i\)。
假设我们现在已经得到了\(F[i]\)的值,考虑从\(i\)处继续往下走。
对于第一种操作,实际上是将\(F[1]\)~\(F[i-1]\)全部加上\(F[i]\),我们可以用一个维护差分的树状数组来实现。
对于第二种操作,对于每一个\(i\),大约可以转移给\(\sqrt{i}\)个数,显然是不能暴力转移的。
我们倒过来想,对于一个\(i\),有哪些数可以通过第二种操作转移到它,那么我们枚举\(j\),满足\(\lfloor\frac{x}{j}\rfloor=i\)的\(x\)一定是一段连续区间。
我们枚举\(j\),算出对应区间的贡献和,这也可以用树状数组实现。
不难发现,枚举\(j\)的复杂度是\(O(n*logn)\)的,因此总复杂度为\(O(n*log^2n)\)
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=4e6+10;
int n,Mod,F1[MAXN],F2[MAXN];
int Read()
{ int a=0,c=1; char b=getchar();
while(b!='-'&&(b<'0'||b>'9')) b=getchar();
if(b=='-') c=-1,b=getchar();
while(b>='0'&&b<='9') a=a*10+b-48,b=getchar();
return a*c;
}
int Min(int A,int B){ return A<B?A:B; }
int Lowbit(int K){ return K&(-K); }
int Add(int A,int B){ return A+=B,A>=Mod?A-Mod:A; }
void Modify(int *F,int Np,int Num){ while(Np<=n) F[Np]=Add(F[Np],Num),Np+=Lowbit(Np); }
int Query(int *F,int Np)
{ int Ret=0;
while(Np>=1) Ret=Add(Ret,F[Np]),Np-=Lowbit(Np);
return Ret;
}
int main()
{ scanf("%d%d",&n,&Mod),F1[n]=1;
for(int i=n,Nv=0;i>=1;i--)
{ for(int j=2,Le,Ri;i*j<=n;j++)
Le=i*j,Ri=Min(n,Le+j-1),Nv=Add(Nv,Add(Query(F2,Ri),Mod-Query(F2,Le-1)));
Nv=Add(Nv,Query(F1,i)),Modify(F1,1,Nv),Modify(F1,i,Mod-Nv),Modify(F2,i,Nv),Nv=0;
}
printf("%d\n",F2[1]);
}
C.Bottom-Tier Reversals
题目链接
简要题解
我们需要把整个序列排序,由于每次只能翻转前缀,因此一定是把元素从后往前依次归位。
由于只能翻转奇数长度的前缀,所以奇数位上的数不能移到偶数位上,反之亦然。
那么若奇数位上存在偶数,或者偶数位上存在奇数,就一定无法还原序列。
接下来讨论从后往前还原序列,假设当前需要还原第\(i\)位和第\(i-1\)位,其中\(i\)是奇数。
我们设当前第\(i\)个位置上的数为\(b_i\),那么有以下几种情况:
\(Case 1:i,i-1,...,b_i,i+1,i+2,...,n\)
我们只需要将前\(i\)个位置翻转即可还原\(i\)和\(i-1\),总共\(1\)步还原。
\(Case 2:b_1,b_2,...,i-1,i,...,b_i,i+1,i+2,...,n\)
我们只需要将以\(i\)为结尾的前缀翻转即可变成\(Case1\)的情况,总共\(2\)步还原。
\(Case 3:b_1,b_2,...,i,i-1,...,b_i,i+1,i+2,...,n\)
我们只需要将任意一个包含\(i\)和\(i-1\)的前缀翻转即可变成\(Case2\)的情况,总共\(3\)步还原。
\(Case 4:i,b_2,...,i-1,...,b_i,i+1,i+2,...,n\)
我们只需要将\(i-1\)前面的位置翻转即可变成\(Case3\)的情况,总共\(4\)步还原。
\(Case 5:b_1,b_2,...,i-1,...,i,...,b_i,i+1,i+2,...,n\)
我们只需要将以\(i\)为结尾的前缀翻转即可变成\(Case4\)的情况,总共\(5\)步还原。
\(Case 6:b_1,b_2,...,i,...,i-1,...,b_i,i+1,i+2,...,n\)
我们只需要将以\(i\)为结尾的前缀翻转即可变成\(Case4\)的情况,总共\(5\)步还原。
可以发现,我们至多花五步就可以还原两个位置,因此在\(\frac{5}{2}n\)步内一定可以还原整个序列。
可以直接暴力翻转,时间复杂度\(O(n^2)\)
代码如下:
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int MAXN=2050;
int T,n,Flag,A[MAXN];
vector<int>Ans;
void Solve(int Lp,int Rp,int Aim)
{ if(Rp==Aim&&Lp==Aim-1) return ;
if(Rp==1&&Lp==2) return reverse(A+1,A+Aim+1),Ans.pb(Aim);
if(Rp==Lp+1) return reverse(A+1,A+Rp+1),Ans.pb(Rp),Solve(2,1,Aim);
if(Lp==Rp+1)
{ int Nt=Lp+1;
for(int i=Nt;i<=Aim;i+=2)
if(A[i+1]==A[1]-1) Nt=i;
return reverse(A+1,A+Nt+1),Ans.pb(Nt),Solve(Nt-Lp+1,Nt-Rp+1,Aim);
}
if(Rp==1) return reverse(A+1,A+Lp),Ans.pb(Lp-1),Solve(Lp,Lp-1,Aim);
if(Rp>Lp) reverse(A+1,A+Rp+1),Ans.pb(Rp),Solve(Rp-Lp+1,1,Aim);
if(Rp<Lp) reverse(A+1,A+Rp+1),Ans.pb(Rp),Solve(Lp,1,Aim);
}
int main()
{ for(scanf("%d",&T);T;T--)
{ scanf("%d",&n),Flag=0,Ans.clear();
for(int i=1;i<=n;i++) scanf("%d",&A[i]),Flag|=(A[i]^i)&1;
if(Flag){ puts("-1"); continue ; }
for(int i=n,Lp,Rp;i>=2&&!Flag;i-=2)
{ for(int j=1;j<=i;j++) A[j]==i-1?Lp=j:0,A[j]==i?Rp=j:0;
Solve(Lp,Rp,i);
}
if(Flag||Ans.size()>5*n/2){ puts("-1"); continue ; }
printf("%d\n",Ans.size());
for(int v:Ans) printf("%d ",v);
if(Ans.size()) puts("");
}
}
D.Top-Notch Insertions
题目链接
简要题解
我们设排序完成后的数组为\(\{b_i\}\),由题意得\(b_i\leq b_{i+1}\),然后考虑我们现在有什么限制条件。
如果对于序列中的某个位置\(p\),不存在\(x_i=p\)的\((x_i,y_i)\),那么\(a_p\)就是前\(p\)个元素中第\(p\)大的,此时允许元素相等的情况。
如果对于序列中的某个位置\(p\),存在\(x_i=p\)的\((x_i,y_i)\),那么\(a_p\)就是前\(p\)个元素中第\(y_i\)大的,并且严格小于第\(y_i+1\)大的元素。
我们发现,对序列中每一个位置都分析一次之后,我们就能把\(\{a_i\}\)和\(\{b_i\}\)一一对应起来,所以我们只要求有多少种\(\{b_i\}\)
对于每一个\(b_i\),有两种限制。
一是题目原本的限制,需要满足\(b_i\leq b_{i+1}\)
二是来自\((x,y)\)的限制,每一个\((x,y)\)会形成一个\(b_y<b_{y+1}\)的限制。我们不关心\(j\)具体是多少,但是我们要知道有几个这样的限制。
不同的\((x_i,y_i)\)对可能会形成相同的\(b_y<b_{y+1}\)的限制,因此我们需要想办法去重。
不妨按\(x\)从小到大处理每一对\((x,y)\),假设当前处理\((x_i,y_i)\),它会贡献一个\(b_{y_i}<b_{y_i+1}\)的限制。
我们发现,加入第\(x_i\)个元素之后,先前已有的限制会发生变化。
对于所有\(k\geq y_i-1\)的限制\(b_k<b_{k+1}\)来说,它们会变成\(b_{k+1}<b_{k+2}\),这个可以自己手玩推导一下。
那么变化之后,如果已经存在\(b_{y_i}<b_{y_i+1}\)的限制,那么我们的限制就重复了一次。
具体实现可以用平衡树,维护哪些位置有第二种限制,需要支持插入元素,区间加法,查询某个元素是否存在。
去重之后,假设我们知道\(m\)对\((x,y)\)产生了\(K\)个限制,然后考虑如何确定\(\{b_i\}\)
先说结论,满足条件的\(\{b_i\}\)有\(C_{2*n-K-1}^{n}\)个。
为什么?我们考虑构造数列\(\{c_i\}\),初始令\(c_i=b_i\)
然后对于每一个限制\(b_j\leq b_{j+1}\),我们把\(c_{j+1}\)~\(c_n\)全部加一。
这样构造出来的\(\{c_i\}\)和\(\{b_i\}\)是一一对应的,而且\(c_i\)各不相同。
不难发现,\(1\leq c_i\leq 2*n-K-1\),所以我们只需要从值域中挑\(n\)个不同的值就能确定一个\(\{b_i\}\)
时间复杂度\(O(m*logn)\)
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+10;
const int Mod=998244353;
int T,n,m,Ans,K,Fac[MAXN*2],Inv[MAXN*2];
int Read()
{ int a=0,c=1; char b=getchar();
while(b!='-'&&(b<'0'||b>'9')) b=getchar();
if(b=='-') c=-1,b=getchar();
while(b>='0'&&b<='9') a=a*10+b-48,b=getchar();
return a*c;
}
int Pow(int Down,int Up)
{ int Ret=1,Now=Down;
for(;Up>=1;Up>>=1) Up&1?Ret=1ll*Ret*Now%Mod:0,Now=1ll*Now*Now%Mod;
return Ret;
}
int C(int A,int B){ return B>A||B<0?0:1ll*Fac[A]*Inv[B]%Mod*Inv[A-B]%Mod; }
namespace Treap
{ int Root,H1,H2,H3,Ts,Fix[MAXN],Num[MAXN],Son[MAXN][2],Size[MAXN],Lan[MAXN];
int New(int S){ return Ts++,Fix[Ts]=rand(),Num[Ts]=S,Size[Ts]=1,Ts; }
void Push_up(int S){ Size[S]=Size[Son[S][0]]+Size[Son[S][1]]+1; }
void Push_down(int S)
{ if(!Lan[S]) return ;
if(Son[S][0]) Num[Son[S][0]]+=Lan[S],Lan[Son[S][0]]+=Lan[S];
if(Son[S][1]) Num[Son[S][1]]+=Lan[S],Lan[Son[S][1]]+=Lan[S];
Lan[S]=0;
}
int Merge(int A,int B)
{ if(!A||!B) return A+B;
Push_down(A),Push_down(B);
if(Fix[A]>Fix[B]) return Son[A][1]=Merge(Son[A][1],B),Push_up(A),A;
return Son[B][0]=Merge(A,Son[B][0]),Push_up(B),B;
}
void Split(int S,int Lim,int &R1,int &R2)
{ if(!S) return R1=R2=0,(void)0;
Push_down(S);
if(Num[S]<=Lim) R1=S,Split(Son[S][1],Lim,Son[S][1],R2),Push_up(S);
else R2=S,Split(Son[S][0],Lim,R1,Son[S][0]),Push_up(S);
}
void Insert(int Val){ Split(Root,Val-1,H1,H2),Root=Merge(Merge(H1,New(Val)),H2); }
int Query(int Val,int Ret=0){ return Split(Root,Val,H1,H2),Ret=Size[H1],Root=Merge(H1,H2),Ret; }
void Modify(int Val)
{ Split(Root,Val-1,H1,H2);
if(H2) Num[H2]++,Lan[H2]++;
Root=Merge(H1,H2);
}
void Init()
{ for(int i=0;i<=Ts;i++) Fix[i]=Num[i]=Son[i][0]=Son[i][1]=Size[i]=Lan[i]=0;
Root=Ts=0;
}
}using namespace Treap;
int main()
{ Fac[0]=Inv[0]=1,srand(time(NULL));
for(int i=1;i<=400000;i++) Fac[i]=1ll*Fac[i-1]*i%Mod;
Inv[400000]=Pow(Fac[400000],Mod-2);
for(int i=399999;i>=1;i--) Inv[i]=1ll*Inv[i+1]*(i+1)%Mod;
for(T=Read();T;T--)
{ n=Read(),m=Read(),Ans=0,K=m,Init();
for(int i=1,Xi,Yi;i<=m;i++)
{ Xi=Read(),Yi=Read(),Modify(Yi-1);
if(Query(Yi)!=Query(Yi-1)) K--;
else Insert(Yi);
}
printf("%d\n",C(2*n-1-K,n));
}
}