2021牛客暑期多校训练营5 部分题题解
C.Cheating and Stealing
题目链接
简要题解
先来解释一下题目想要我们做什么:枚举一个\(i\),表示比赛的\(winning\) \(point\),然后计算\(f_i(S)\)。
正常情况,一局乒乓球比赛的\(winning\) \(point\)是\(11\)分,如果双方都打到了\(10\)分,那么就必须领先两球才能获胜(本题比赛可以一直打)。
\(f_i(S)\)表示的是,按照\(S\)的顺序来得分,能赢几场球。
似乎没有什么很好的办法,就只能根据题意来一局一局模拟了,不过对于每一局来说,我们通过预处理,是可以快速计算出结果的。
具体来说,我们预处理\(Prew[i]\)和\(Prel[i]\),表示序列前\(i\)位有几个\(W\)和几个\(L\)。
再预处理\(Posw[i]\)和\(Posl[i]\),表示第\(i\)个\(W\)和第\(i\)个\(L\)在序列中的哪个位置。
考虑到双方平分时会延长比赛,还需要预处理一个\(Tie[i]\),表示如果双方打完\(S\)中第\(i\)个球,在赛点处平分时,将在哪个位置结束比赛。
\(Tie[i]\)的转移:\(Tie[i]=(S[i+1]==S[i+2]?i+2:Tie[i+2])\)
如果接下来两局是同一个人得分,那么直接结束比赛,否则将在第\(i+2\)个位置继续平分。
预处理完成后,解题就只需要分情况讨论即可。
可能比赛到\(winning\) \(point\)就结束了,那么找到赢了或输了\(i\)球的位置即可。
可能比赛没打完,那么需要做一些记号,特判一下。
可能最后双方平分,比赛延长,那么利用\(Tie\)数组来快速结束比赛。
具体细节得看个人的代码习惯和实现方式了。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+10;
const int Mod=998244353;
char Str[MAXN];
int n,Ws,Ls,Ans,Prew[MAXN],Prel[MAXN];
int Posw[MAXN],Posl[MAXN],Tie[MAXN],F[MAXN];
void Prepare()
{ for(int i=1;i<=n;i++)
{ Prew[i]=Prew[i-1]+(Str[i]=='W'),Prel[i]=Prel[i-1]+(Str[i]=='L');
Posw[i]=Posl[i]=n+1,Str[i]=='W'?Posw[++Ws]=i:Posl[++Ls]=i;
}
for(int i=n;i>=1;i--) Tie[i]=(Str[i+1]==Str[i+2]?i+2:Tie[i+2]);
}
int Abs(int S){ return S>0?S:-S; }
int Max(int A,int B){ return A>B?A:B; }
int Solve(int St,int K)
{ if(n-St+1<K) return n+1;
int Endw=Posw[Prew[St-1]+K],Endl=Posl[Prel[St-1]+K],Nt=Max(Endw,Endl);
if(!Endw&&!Endl) return n+1;
if(Endw==n&&Prel[n]-Prel[St-1]==K-1) return n+1;
if(Endl==n&&Prew[n]-Prew[St-1]==K-1) return n+1;
if(Endw<Endl&&Prel[Endw]-Prel[St-1]<=K-2) return F[K]++,Endw+1;
if(Endl<Endw&&Prew[Endl]-Prew[St-1]<=K-2) return Endl+1;
if(Endw<Endl&&Str[Endw+1]=='W') return F[K]++,Endw+2;
if(Endl<Endw&&Str[Endl+1]=='L') return Endl+2;
if(!Tie[Nt]||Tie[Nt]>n) return n+1;
return F[K]+=Str[Tie[Nt]]=='W',Tie[Nt]+1;
}
int main()
{ scanf("%d%s",&n,Str+1),Prepare();
for(int i=1,Np=1;i<=n;i++,Np=1)
while(Np&&Np<=n) Np=Solve(Np,i);
for(int i=1,Td=1;i<=n;i++) Ans=(Ans+1ll*F[i]*Td)%Mod,Td=1ll*Td*(n+1)%Mod;
printf("%d\n",Ans);
}
E.Eert Esiwtib
题目链接
简要题解
我们知道,位运算中混入加减法会很麻烦,因为这样的话各位之间就会相互影响。
不过观察数据范围,我们发现\(d\)很小,不超过\(100\),因此不难想到离线操作,枚举这个\(d\),然后每次解决对\(d\)的询问。
对于每个询问,我们需要的是询问点到它子树上所有点的路径信息,两点之间是一条路径,而且这条路径对应了一个值。
路径值是通过位运算的算式得到的,这个应该可以转移。
那么不难想到树形\(Dp\),我们设\(F[i][0/1/2]\),表示\(i\)号节点到子树内所有点路径对应的值,的或/与/异或和。
\(0\)表示或,\(1\)表示与,\(2\)表示异或。
转移的时候我们要考虑两个东西,一个是位运算对于路径值的影响,另一个是位运算对于所有路径值的或/与/异或和的影响。
具体转移方程可以自己推导,也可以看代码中的\(Trans\)函数。
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+10;
struct EDGE{ int u,v,Next; }Edge[MAXN*2];
struct ASK{ int D,P; }Q[MAXN];
int n,Es,Qs,Fa[MAXN],Opt[MAXN],Size[MAXN],First[MAXN];
ll All,A[MAXN],Val[MAXN],Ans[MAXN][3],Dp[MAXN][3];
vector<int>Qd[110],Qp[MAXN];
ll Read()
{ ll 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;
}
void Link(int u,int v){ Edge[++Es]=(EDGE){u,v,First[u]},First[u]=Es; }
void Dfs(int Now,int Ba)
{ Size[Now]=1;
for(int i=First[Now],v;i!=-1;i=Edge[i].Next)
if((v=Edge[i].v)!=Ba) Dfs(v,Now),Size[Now]+=Size[v];
}
void Trans(int Lp,int Np,int K)
{ if(K==0)
{ Dp[Lp][0]|=Dp[Np][0]|Val[Lp],Dp[Lp][1]&=Dp[Np][1]|Val[Lp];
Dp[Lp][2]^=(Size[Np]&1?Val[Lp]:0)|(~Val[Lp]&Dp[Np][2]);
}
if(K==1)
Dp[Lp][0]|=Dp[Np][0]&Val[Lp],Dp[Lp][1]&=Dp[Np][1]&Val[Lp],Dp[Lp][2]^=Dp[Np][2]&Val[Lp];
if(K==2)
{ Dp[Lp][0]|=(~Dp[Np][1]&Val[Lp])|(~Val[Lp]&Dp[Np][0]);
Dp[Lp][1]&=(~Dp[Np][0]&Val[Lp])|(~Val[Lp]&Dp[Np][1]);
Dp[Lp][2]^=(Size[Np]&1?Val[Lp]:0)^Dp[Np][2];
}
}
void Dfs2(int Now,int Ba)
{ Dp[Now][0]=Dp[Now][2]=0,Dp[Now][1]=All;
for(int i=First[Now],v;i!=-1;i=Edge[i].Next)
{ if((v=Edge[i].v)==Ba) continue ;
Dfs2(v,Now),Trans(Now,v,Opt[v]);
}
for(int i:Qp[Now]) Ans[i][0]=Dp[Now][0],Ans[i][1]=Dp[Now][1],Ans[i][2]=Dp[Now][2];
Dp[Now][0]|=Val[Now],Dp[Now][1]&=Val[Now],Dp[Now][2]^=Val[Now];
}
int main()
{ n=Read(),Qs=Read(),All=~0ll;
memset(First,-1,sizeof(First));
for(int i=1;i<=n;i++) A[i]=Read();
for(int i=2;i<=n;i++) Fa[i]=Read(),Opt[i]=Read(),Link(i,Fa[i]),Link(Fa[i],i);
for(int i=1;i<=Qs;i++) Q[i].D=Read(),Q[i].P=Read(),Qd[Q[i].D].push_back(i);
Dfs(1,1);
for(int i=0;i<=100;i++)
{ for(int j:Qd[i]) Qp[Q[j].P].push_back(j);
for(int j=1;j<=n;j++) Val[j]=A[j]+1ll*j*i;
Dfs2(1,1);
for(int j:Qd[i]) Qp[Q[j].P].clear();
}
for(int i=1;i<=Qs;i++) printf("%lld %lld %lld\n",Ans[i][0],Ans[i][1],Ans[i][2]);
}
G.Greater Integer, Better LCM
题目链接
简要题解
我们已经知道\(a+x\)和\(b+y\)的\(lcm\),需要使\(x+y\)最小。
由于加法操作会直接改变质因子的种类,因此没有什么好的办法来维护我们想要的东西。
观察数据范围,\(\sum q_i \leq18\),那么暴力枚举每一个质因子及其次幂的时间复杂度不超过\(2^{18}\)。
我们可以知道\(lcm\)的每一个约数,和\(a,b\)作比较就能知道\(x,y\)的值了,但是我们要满足\(lcm\)的限制。
对于\(lcm\)的某一个质因子来说,\(a+x\)和\(b+y\)两个数中,至少有一个要取得最高次幂。
质因子不超过\(18\)种,因此不难想到状态压缩,每一种质因子对应一位\(0/1\),为\(1\)表示取到最高次幂。
那么每一个约数对应一个状态,只要两个状态或起来之后,每一位都是1,那么这两个状态对应的数就满足了\(lcm\)的限制。
那么我们维护两个状态数组分别储存\(a+x\)和\(b+y\)的信息,暴力求出\(lcm\)的所有约数以及对应状态之后更新状态数组。
每个状态只保留合法的最小的\(x\)和\(y\),因为约数对\(lcm\)的影响只和对应状态有关。
考虑合并两个状态数组的信息计算答案,可以枚举一遍的状态,再枚举另一边所有的合法状态。
这相当于枚举一个集合,再枚举这个集合的子集,复杂度是\(O(3^n)\)的。
实际上,我们可以利用子集\(Dp\)的技巧,每次利用当前状态更新子集状态,即将信息下放。
为了保证复杂度,只需要减去该状态上一个位置上的\(1\)即可,即只更新所有最大的真子集。
因为我们下一次将会枚举到这个子集,并且更新更小的子集,这样子每个状态就储存了所有包含该子集的集合信息。
这个过程的时间复杂度是\(O(n*2^n)\)的,可以接受。
计算答案的话,就只需要枚举两个互补的状态即可。
总时间复杂度为\(O(n*2^n)\)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=(1<<18)+10;
const __int128 Inf=1e36;
int n,Top,P[20],Q[20];
__int128 A,B,Ans,Va[MAXN],Vb[MAXN];
__int128 Read()
{ __int128 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 Lowbit(int K){ return K&(-K); }
__int128 Min(__int128 A,__int128 B){ return A<B?A:B; }
void Print(__int128 K)
{ __int128 Ten=1;
while(Ten*10<K) Ten*=10;
for(int S;Ten;) S=K/Ten,printf("%d",S),K%=Ten,Ten/=10;
}
void Update(__int128 Nv,int St)
{ if(Nv>=A) Va[St]=Min(Va[St],Nv-A);
if(Nv>=B) Vb[St]=Min(Vb[St],Nv-B);
}
void Dfs(__int128 Nv,int Now,int St)
{ if(Now==n) return Update(Nv,St);
for(int i=0;i<Q[Now];i++) Dfs(Nv,Now+1,St),Nv*=P[Now];
Dfs(Nv,Now+1,St|(1<<Now));
}
void Solve()
{ for(int i=Top;i>=0;i--)
for(int j=i,Bit;j;j^=Bit)
Bit=Lowbit(j),Va[i^Bit]=Min(Va[i^Bit],Va[i]),Vb[i^Bit]=Min(Vb[i^Bit],Vb[i]);
for(int i=0;i<=Top;i++) Ans=Min(Ans,Va[i]+Vb[Top^i]);
}
int main()
{ scanf("%d",&n),Top=(1<<n)-1,Ans=Inf;
for(int i=0;i<=Top;i++) Va[i]=Vb[i]=Inf;
for(int i=0;i<n;i++) scanf("%d%d",&P[i],&Q[i]);
A=Read(),B=Read(),Dfs(1,0,0),Solve(),Print(Ans);
}
I.Interval Queries
题目链接
简要题解
我们有一个序列,还有若干关于区间的询问,这很符合莫队的特点。
观察到\(\sum k \leq 10^7\),那么处理\(k\)时暴力拓展左右端点也是可以接受的。
往莫队的方向来想,对于每一个区间,我们要求的其实是这个区间内最长的连续段的长度,似乎可以用桶和链表来维护。
向桶内加入元素比较好办,合并连续段,更新链表,计算新连续段的长度,但是删除元素会很麻烦。
因为如果我们删除当前元素后拆掉了最长的连续段,我们无法知道新的最长连续段是什么。
而回滚莫队就是用来解决无法删除的问题的。
具体地说,我们用一般方法对询问分块,每次处理左端点在同一块内的询问,且询问的右端点递增。
每次做一个询问,只需要将右端点右移,左端点重新从块的最右端向左暴力扫。
我们维护一个桶,表示值为\(x\)的元素有多少个,再维护两个链表\(Pre[i]\)和\(Next[i]\),表示桶内左边、右边第一个值为\(0\)的下标是多少。
这个链表是支持撤销操作的,不能支持撤销操作的是维护答案,但是我们现在不需要维护撤销后的答案了。
所以对于每个询问,我们先拓展右端点,记录当前答案,然后重新拓展左端点,再同时拓展左右端点计算\(k\)的答案。
算完之后撤销链表和桶,撤销方法可以采用栈序撤销,答案重新赋成拓展前的值即可。
时间复杂度约为\(O(n\sqrt n+\sum k)\)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+10;
const int Mod=998244353;
struct ASK{ int Le,Ri,K,Id; }Sec[MAXN];
int n,Qs,Len,Ans1,Ans2,A[MAXN],Ans[MAXN];
int Bel[MAXN],Pow[MAXN],Next[MAXN],Pre[MAXN],Tong[MAXN];
stack<int>Undo;
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 Max(int A,int B){ return A>B?A:B; }
bool cmp1(ASK A,ASK B){ return Bel[A.Le]==Bel[B.Le]?A.Ri<B.Ri:Bel[A.Le]<Bel[B.Le]; }
bool cmp2(ASK A,ASK B){ return A.Id<B.Id; }
void Add(int Np,int K)
{ if((++Tong[Np])==1) Next[Pre[Np]]=Next[Np],Pre[Next[Np]]=Pre[Np],Ans1=Max(Ans1,Next[Np]-Pre[Np]-1);
if(K) Undo.push(Np);
}
void Clear()
{ Ans1=Ans2;
for(int Top;!Undo.empty();Undo.pop())
if(!(--Tong[Top=Undo.top()])) Pre[Next[Top]]=Top,Next[Pre[Top]]=Top;
}
void Build()
{ for(int i=1;i<=n;i++) Next[i]=i+1,Pre[i]=i-1,Tong[i]=0;
Ans1=Ans2=0,Next[n+1]=n+1;
}
void Special(int Np)
{ for(int i=Sec[Np].Le;i<=Sec[Np].Ri;i++) Add(A[i],1);
Ans[Sec[Np].Id]=Ans1;
for(int i=1;i<Sec[Np].K;i++)
Add(A[Sec[Np].Le-i],1),Add(A[Sec[Np].Ri+i],1),Ans[Sec[Np].Id]=(Ans[Sec[Np].Id]+1ll*Ans1*Pow[i])%Mod;
}
int main()
{ n=Read(),Qs=Read(),Pow[0]=1,Len=sqrt(n);
for(int i=1;i<=n;i++) A[i]=Read();
for(int i=1;i<=Qs;i++) Sec[i].Le=Read(),Sec[i].Ri=Read(),Sec[i].K=Read(),Sec[i].Id=i;
for(int i=1;i<=n;i++) Pow[i]=1ll*Pow[i-1]*(n+1)%Mod,Bel[i]=(i-1)/Len+1;
sort(Sec+1,Sec+Qs+1,cmp1);
for(int i=1,j=1,Le,Ri,Lim;i<=Bel[n];i++)
{ Lim=i*Len+1,Build(),Le=Lim,Ri=Lim-1;
while(j<=Qs&&Sec[j].Ri<Lim) Special(j),Clear(),j++;
while(j<=Qs&&Bel[Sec[j].Le]==i)
{ while(Ri<Sec[j].Ri) Add(A[++Ri],0),Ans2=Ans1;
while(Le>Sec[j].Le) Add(A[--Le],1);
Ans[Sec[j].Id]=Ans1;
for(int k=1;k<Sec[j].K;k++)
Add(A[Sec[j].Le-k],1),Add(A[Sec[j].Ri+k],1),Ans[Sec[j].Id]=(Ans[Sec[j].Id]+1ll*Ans1*Pow[k])%Mod;
Le=Lim,j++,Clear();
}
}
for(int i=1;i<=Qs;i++) printf("%d\n",Ans[i]);
}