2021牛客暑期多校訓練營5 部分題題解


C.Cheating and Stealing

題目鏈接

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

題目鏈接

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

題目鏈接

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

題目鏈接

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]);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM