Codeforces Round 873 (Div. 2)
CF 873
题号:CF1828A/B,CF1827A,B1B2CD
A
构造一个长度为
的序列 满足以下条件。
- 对于每个元素
满足 。 - 对于每个元素
满足 。即每个元素 能被 整除。 - 满足
。即所有元素之和能被 整除。
。
题解:考虑三个条件。注意到
那么,我们构造一个长度为
将
B
给你一个长度为
的未排序的排列。找到最大的整数 满足可以通过只交换下标差为 的元素使排列被从小到大排序
注意到
显然,
C
求有多少种重新排列
的方式,使得对于任意 ,都满足 ,结果对 取模。
,保证 互不相同。
注意到,这个重新排列不会移动
所以我们不妨将
故答案为:
D2
对一个数组
的一段区间 排序的代价为 ,对整个数组 排序的代价为选定若干区间并排序,使得整个数组有序的代价之和。 求
的所有子段排序的最小代价之和。
首先,注意到对于段
然后,我们考虑
考虑对于区间
显然,如果我们求出每个断点
考虑到
这样做复杂度
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";
注意到上述做法的瓶颈在于,对于切点来说,它符合条件的
换一个角度,我们来考虑最值。我们钦定右半区间最小值为
首先,对于左边第一个小于它的数
这样的话,我们确定了右端点的范围和切点的位置,我们再来考虑求出左端点。
左端点是什么,是在
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
称一个字符串是好的,当且仅当它是一个长度为偶数的回文串或由若干长度为偶数的回文串拼接而成。
给定一个长度为
的字符串 ,求有多少 的子串是好的。
, 仅包含小写字母。
这里学习了Manacher,在此做一个小小的学习报告。
Manacher的步骤:类似于Z函数
- 将原字符串改造,插如奇怪字符:
s[0]='&';s[1]='#';num=1;for(int i=1;i<=n;i++)s[++num]=a[i],s[++num]='#';
- 维护对称半径
,以及建立当前最远的影响范围 ,命 为取到 最大值的 , - 考虑由于对称性,有
,否则 。然后往后暴力扩展 - 扩展后,
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求解。
设
问题化为我们怎么通过
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
地灵殿门口有一个小挂件,小挂件可视作一棵树,初始只有
个编号为 的节点。 接下来恋恋会做
次操作,第 次操作添加一个编号为 的节点,并在节点 与 之间连边。 现在她想知道,在每一次操作之后,至少还需要进行多少次“新建一个点并与原树相连”的操作,才能使这棵树具有两个重心。
由于恋恋忙着给挂件做装饰,所以你要帮她解决这个问题。
回顾一下树的重心的基本性质:
- 树的重心
的每一颗子树大小都不超过 ,且 - 一棵树最多有两个重心,且若存在两个,必定为父子关系,此时作为儿子的重心的子树的大小正好为
- 往树内添加一个叶子节点,树的重心最多移动一条边
我们现在考虑求解这个问题。先维护一个重心
然后,考虑怎么使得一个重心变成两个。很简单,往最大的子树/
当往最大的子树里塞叶子,说明重心向下产生,设当前最大子树为
同理,当往子树外塞叶子的时候,说明重心向上移动,则
对于
那么我们回到该问题,这里我们可以动态维护重心和子树大小,以及最大子树(或者子树外),具体做法:
将完整的树建立出来,但不标记大小等,动态插入维护信息(相当于它存在,但假装不存在
将树拍成DFS序列,每个子树对应一个区间,这可以通过单点修改插入一个点并维护子树大小
那么我们只需要看插入新的点
- 点
插入了当前 的子树中 - 点
插入了其他地方
插入时进行单点修改维护
对于插入子树这个情况,重心只能移动到
记录这个点为
如果不下移,用这个新的
同理,插入子树外的情况,只需判断是否
否则,用
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序结合数据结构维护子树信息。树的重心的性质。
- 存在,但假装不存在
- 合理运用离线
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!