Codeforces Round 873 (Div. 2)
CF 873
题号:CF1828A/B,CF1827A,B1B2CD
A
构造一个长度为 \(n\) 的序列 \(a\) 满足以下条件。
- 对于每个元素 \(a_i\) 满足 \(a_i \le 1000\)。
- 对于每个元素 \(a_i\) 满足 \(a_i \equiv 0 \pmod i\)。即每个元素 \(a_i\) 能被 \(i\) 整除。
- 满足 \(\sum_{i = 1}^{n} a_i \equiv 0 \pmod n\)。即所有元素之和能被 \(n\) 整除。
\(n \leq 200\)。
题解:考虑三个条件。注意到 \(n\le200<1000\),所以,我们完全可以只用 \(a_1\) 就满足整除 \(n\) 的条件。
那么,我们构造一个长度为 \(n\) 的序列,\(a_i=i\),然后求和,对 \(n\) 取模,也即 \(k=\frac{n(n+1)}{2}\bmod n\)
将 \(a_1\longleftarrow 1+k\) 即可。
B
给你一个长度为 \(n\) 的未排序的排列。找到最大的整数 \(k\) 满足可以通过只交换下标差为 \(k\) 的元素使排列被从小到大排序
注意到 \(k\) 本质上是一种循环,下标模 \(k\) 为不同值的在不同循环。我们需要让每个数 \(a_i\) 和它本应在的位置 \(a_i\) 的下标差模 \(k\) 相同,也即:\(\forall i\in [1,n],k||a_i-i|\)
显然,\(k_{\max}=\gcd(|1-a_1|,|2-a_2|\dots |n-a_n|)\)
C
求有多少种重新排列 \(a\) 的方式,使得对于任意 \(1\le i\le n\),都满足 \(a_i>b_i\),结果对 \(10^9+7\) 取模。
\(1\le n\le 2\times 10^5,1\le a_i,b_i\le 10^9\),保证 \(a_i\) 互不相同。
注意到,这个重新排列不会移动 \(b\),那么本质上,对于 \(b\) 的任意排列而言,方案数必定相同。因为原对应的方案可以通过与 \(b\) 这个排列相同的转移变成另一个合法方案。
所以我们不妨将 \(b\) 排序,可以利用双指针/二分求出 \(c_i=\sum_{i=1}^n[a_i>b_i]\)。然后真正 \(b_i\) 这里可以随便选的数实际上为 \(c_i-(n-i)\) 。
故答案为: \(\prod_{i=1}^n(c_i-n+i)\)
D2
对一个数组 \(\{p_i\}\) 的一段区间 \([l,r]\) 排序的代价为 \(r-l\) ,对整个数组 \(p_i\) 排序的代价为选定若干区间并排序,使得整个数组有序的代价之和。
求 \(\{a_i\}\) 的所有子段排序的最小代价之和。
首先,注意到对于段 \([l,r]\),操作一次的代价是 \(r-l\),操作两次的代价是 \(r-l-1\)。所以实际上,对于子段 \([l,r]\) 而言,排序的代价为 \(r-;+1-c\),其中 \(c\) 是最大操作次数。
然后,我们考虑 \(c\) 怎么求。转化一下,首先一口气排所有子段的代价显然是 \(\sum_{i=2}^n(i-1)(n-i+1)\),我们考虑求额外的可以减少操作次数。
考虑对于区间 \([l,r]\),有且仅有 \(\max_{l\le i\le k}a_i<\min_{k<i\le r}a_i\),此时可以分为 \([l,i],[i+1,r]\) 两端进行排序,答案减去1。
显然,如果我们求出每个断点 \([i,i+1]\) 可以作为多少段落的的断点,那么就可以求出答案。
考虑到 \(\max\) 运算越往后越小,\(\min\) 同理。所以我们可以枚举 \(i\),同时利用双指针 \(l,r\) 即可统计答案。求最值这一步需要ST表。
这样做复杂度 \(O(n^2+n\log n)\)。足以通过Easy版本。
init();int ans=0;
for(int len=2;len<=n;len++)ans+=(len-1)*(n-len+1);
for(int i=1;i<n;i++){
int l=1;
for(int r=i+1;r<=n;++r){
while(l<=i&&get_mx(l,i)>get_mn(i+1,r))++l;
if(l>i)break;
ans-=(i-l+1);
}
}
cout<<ans<<"\n";
注意到上述做法的瓶颈在于,对于切点来说,它符合条件的 \(l,r\) 是都在变化的,这注定它难以维护。而且容易发现,事实上合法的切点非常少。。。
换一个角度,我们来考虑最值。我们钦定右半区间最小值为 \(a_i\),考虑确定整个区间。
首先,对于左边第一个小于它的数 \(a_{h_i}\),这个数必定作为切点。其次,对于右边第一个小于它的数 \(a_{z_i}\),说明右端点最多在 \(z_i-1\)。
这样的话,我们确定了右端点的范围和切点的位置,我们再来考虑求出左端点。
左端点是什么,是在 \(h_i\) 左侧第一个大于它的数的位置加一。
\(h_i,z_i\) 均可以使用单调栈求出,而这个在 \(h_i\) 左侧第一个大于它的数,可以 ST+二分找到。
int n,t,k,a[505050],lg[505050],mx[505050][25];
void init(){
for(int i=2;i<=n;i++)lg[i]=lg[i/2]+1;
for(int i=1;i<=n;i++)mn[i][0]=mx[i][0]=a[i];
for(int j=1;j<=lg[n]+1;j++){
for(int i=1;i+(1<<j)-1<=n;i++){
mx[i][j]=max(mx[i][j-1],mx[i+(1<<(j-1))][j-1]);
}
}
}
int get_mx(int l,int r){
int k=lg[r-l+1];
return max(mx[l][k],mx[r-(1<<k)+1][k]);
}
struct node{
int x,id;
};
stack<node>s;
node mk(int x,int id){
return (node){x,id};
}
int z[505050],h[505050];
signed main(){
read(t);
while(t--){
read(n);
for(int i=1;i<=n;i++)read(a[i]);
init();int ans=0;
for(int len=2;len<=n;len++)ans+=(len-1)*(n-len+1);
s.push(mk(0,n+1));
for(int i=n;i;--i){
while(s.top().x<-a[i])s.pop();
z[i]=s.top().id;
s.push(mk(-a[i],i));
}
while(!s.empty())s.pop();
s.push(mk(0,0));
for(int i=1;i<=n;++i){
while(s.top().x<-a[i])s.pop();
h[i]=s.top().id;
s.push(mk(-a[i],i));
}
while(!s.empty())s.pop();
for(int i=1;i<=n;i++){
int l,mid=h[i],r=z[i]-1;
if(!mid)continue;
if(get_mx(1,mid)<a[i])l=1;
else {
int L=1,R=mid;
while(L<R){
int d=L+R>>1;
if(get_mx(d,mid)>a[i])L=d+1;
else R=d;
}
l=L;
}
ans-=(mid-l+1)*(r-i+1);
}
cout<<ans<<"\n";
}
}
启发:序列问题的统计考虑角度:固定断点,固定端点,固定最值,固定……
E
称一个字符串是好的,当且仅当它是一个长度为偶数的回文串或由若干长度为偶数的回文串拼接而成。
给定一个长度为 \(n\) 的字符串 \(s\),求有多少 \(s\) 的子串是好的。
\(1\le n\le5\times10^5\),\(s\) 仅包含小写字母。
这里学习了Manacher,在此做一个小小的学习报告。
Manacher的步骤:类似于Z函数
- 将原字符串改造,插如奇怪字符:
s[0]='&';s[1]='#';num=1;for(int i=1;i<=n;i++)s[++num]=a[i],s[++num]='#';
- 维护对称半径 \(p_i\),以及建立当前最远的影响范围 ,命 \(mid\) 为取到 \(i+p_i\) 最大值的 \(i\),\(r=(i+p_i)_{\max}\)
- 考虑由于对称性,有 \(p_i\ge \min(r-i,p_{2mid-i})\),否则 \(p_i=1\)。然后往后暴力扩展
- 扩展后,\(r=\max(r,i+p_i)\)
for(int i=1,mid=0,r=0;i<=num;++i){
if(i<=r)p[i]=min(r-i,p[(mid<<1)-i]);
else p[i]=1;
while(i-p[i]>=1&&i+p[i]<=num&&s[i-p[i]]==s[i+p[i]])++p[i];
if(i+p[i]>r)mid=i,r=i+p[i];
}
然后我们考虑解决这个题。
显然,对于一个偶回文串而言,如果它对称中心右边还存在一个子回文串,则左边同样存在,且若二者被删,还能留下一个回文子串,所以这考虑启发我们可以DP求解。
设 \(f_{i}\) 表示以 \(i\) 为结尾偶数回文串个数,则 \(f_i=f_{h_i-1}+1\),其中 \(h_i\) 是以 \(i\) 为结尾的的最小回文字符串的对称轴。
问题化为我们怎么通过 \(p_i\) 求得 \(h_i\)。很简单,区间覆盖。从后往前做区间覆盖即可。
cin>>n;cin>>a+1;
manacher();int ans=0;
for(int i=1;i<=n+1;i++)f[i]=0,g[i]=i,len[i]=0x3f3f3f3f;
for(int i=num;i>=1;i-=2)pai(i/2+1,i/2+p[i]/2,i/2);
for(int i=1;i<=n;i++)len[i]=2*len[i]-i;
for(int i=1;i<=n;i++)if(len[i]<i)f[i]=f[len[i]]+1;
for(int i=1;i<=n;i++)ans+=f[i];
cout<<ans<<"\n";
F
地灵殿门口有一个小挂件,小挂件可视作一棵树,初始只有 \(1\) 个编号为 \(1\) 的节点。
接下来恋恋会做 \(n-1\) 次操作,第 \(i\) 次操作添加一个编号为 \(i+1\) 的节点,并在节点 \(i+1\) 与 \(p_i\) 之间连边。
现在她想知道,在每一次操作之后,至少还需要进行多少次“新建一个点并与原树相连”的操作,才能使这棵树具有两个重心。
由于恋恋忙着给挂件做装饰,所以你要帮她解决这个问题。
回顾一下树的重心的基本性质:
- 树的重心 \(u\) 的每一颗子树大小都不超过 \(\frac{n}{2}\),且 \(siz_u\ge \frac{n}{2}\)
- 一棵树最多有两个重心,且若存在两个,必定为父子关系,此时作为儿子的重心的子树的大小正好为 \(\frac{n}{2}\)
- 往树内添加一个叶子节点,树的重心最多移动一条边
我们现在考虑求解这个问题。先维护一个重心 \(rt\)。最初 \(rt=1\)
然后,考虑怎么使得一个重心变成两个。很简单,往最大的子树/\(rt\) 外塞叶子节点,直到两边平衡。设塞 \(k\) 个,则有 \(n+k\) 个点,其中重心处占一半,也即 \(siz’_{rt}=\frac{n+k}{2}\)
当往最大的子树里塞叶子,说明重心向下产生,设当前最大子树为 \(v\),则 \(n-siz_{v}=siz'_v=siz_v+k\)。所以操作次数\(k_1=n-2siz_v\)
同理,当往子树外塞叶子的时候,说明重心向上移动,则 \(siz_{rt}=n'-siz_{rt}=n+k-siz_{rt}\implies k_2=2siz_{rt}-n\)。
对于 \(k_2\) 而言,\(siz'_v=n-siz_{rt}\)。
那么我们回到该问题,这里我们可以动态维护重心和子树大小,以及最大子树(或者子树外),具体做法:
将完整的树建立出来,但不标记大小等,动态插入维护信息(相当于它存在,但假装不存在
将树拍成DFS序列,每个子树对应一个区间,这可以通过单点修改插入一个点并维护子树大小
那么我们只需要看插入新的点 \(u\) 后,重心是否会产生移动即可。分为以下情况:
- 点 \(u\) 插入了当前 \(rt\) 的子树中
- 点 \(u\) 插入了其他地方
插入时进行单点修改维护 \(siz\)
对于插入子树这个情况,重心只能移动到 \(u\) 的 \(dep_u-dep_{rt}-1\) 级祖先,这可以通过预处理倍增实现。
记录这个点为 \(v\)。我们判断是否 \(siz_v> \frac{u}{2}\)。若大于,则重心下移,此时答案为 \(u\bmod 2\)。注意此时更新最大子树大小为 \(\lfloor \frac{u}{2}\rfloor\)因为此时 \(|siz_v-(u-siz_{rt})|\le 1\)。
如果不下移,用这个新的 \(siz_v\) 去更新当前维护的最大子树大小
同理,插入子树外的情况,只需判断是否 \(u-siz_{rt}>\frac{u}{2}\),如果是,则重心上移,此时答案一样是 \(u\bmod 2\),同时更新最大子树大小。
否则,用 \(u-siz_{rt}\) 更新最大子树大小即可。
void dfs(int u,int fa){
l[u]=++num;f[u][0]=fa;dep[u]=dep[fa]+1;
for(int i=1;i<=18;++i)f[u][i]=f[f[u][i-1]][i-1];
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(v==fa)continue;
dfs(v,u);
}
r[u]=num;
}
int get(int x,int k){
for(int i=18;i>=0;--i)if((k>>i)&1)x=f[x][i];//注意这个写法,非常经典
return x;
}
inline int lowbit(int x){
return x&-x;
}
void Add(int x,int k){
while(x<=n){
c[x]+=k;x+=lowbit(x);
}
}
int ask(int x){
int ans=0;
while(x){
ans+=c[x];x-=lowbit(x);
}
return ans;
}
int Ask(int l,int r){
return ask(r)-ask(l-1);
}
signed main(){
ios::sync_with_stdio(false);
cin>>t;
while(t--){
clear();cin>>n;
for(int i=2;i<=n;i++){
int p;cin>>p;add(i,p);
}
dfs(1,0);
Add(1,1);int rt=1,ans=0;
for(int u=2;u<=n;++u){
Add(l[u],1);
if(l[rt]<=l[u]&&r[u]<=r[rt]){
int v=get(u,dep[u]-dep[rt]-1);
int s=Ask(l[v],r[v]);
if(s>u/2)rt=v,ans=u/2;
else ans=max(ans,s);
}
else {
int s=u-Ask(l[rt],r[rt]);
if(s>u/2)rt=f[rt][0],ans=u/2;
else ans=max(ans,s);
}
cout<<u-(ans<<1)<<" ";
}
cout<<"\n";
}
}
启发:DFS序结合数据结构维护子树信息。树的重心的性质。
它存在,但我们假装不存在。
Trick 总结
- 序列问题的统计考虑角度:固定断点,固定端点,固定最值,固定……
- Manacher 算法,并查集做区间覆盖
- DFS序结合数据结构维护子树信息。树的重心的性质。
- 存在,但假装不存在
- 合理运用离线