2021牛客暑期多校训练营2 部分题题解
E
简要题意
给定一棵n个点的树,每个点有点权\(a_i\),每条边有边权\(w_i\)。
现有\(q\)个询问,每个询问给定\(x_i\),\(d_i\),\(p_i\),表示初始权值为\(d_i\),从\(x_i\)出发,不能经过\(p_i\),能够到达的点的数量。
每经过一个点,权值加上该点的点权,每经过一条边,权值减去该边的边权,要求任意时刻的权值非负。
数据范围
\(n\leq 10^5,q\leq 10^5 , 1\leq a_i,w_i\leq 10^9 ,1\leq x_i \leq n ,0\leq d_i\leq 10^{14},p_i\neq x_i\)
简要题解
此题涉及到的是点对之间的关系,故点分治是一种优秀的解题方法。
首先将询问离线,挂链到\(x_i\)上,然后点分治,点分治的基本操作不再赘述。
假设当前区域重心为\(rt\),我们需要考虑的是区域内经过重心的点对贡献,所以需要预处理两个数组,记为\(Down[i]\)和\(Up[i]\)。
\(Down[i]\)表示,从\(rt\)出发,到达点\(i\),需要的最小初始权值;\(Up[i]\)表示,从\(i\)出发,到达\(rt\),需要的最小初始权值。
于是我们就能够很容易地知道,哪些询问可以走到\(rt\)且还剩下多少权值。
将\(Down[i]\)离散化后插入树状数组,我们就能够快速知道,当某个询问走到\(rt\)后,剩下的权值能够走到多少个点。
这里会有一些不合法的情况,因为我们强制询问先走到\(rt\)再向下走,和询问在同一子树内的点就不能计算答案,还有经过了\(p_i\)的不能算入答案。
如果\(p_i\)不在当前点分治区域内,就直接忽略。如果在,经过\(p_i\)的情况有三种,一是\(p_i\)在\(x_i\)到\(rt\)的路径上,那么本层不可能不会对该询问产生贡献。二是\(p_i\)和\(x_i\)在同一子树内,这种情况不需要单独考虑,因为同一子树本来就不能计算贡献。三是\(p_i\)和\(x_i\)在\(rt\)的不同子树内,需要减去\(p_i\)子树的贡献。
至于如何得到子树贡献,可以利用树状数组作差,Dfs到达某点后,先计算树状数组内满足条件的点数,再把当前点贡献加入树状数组,Dfs子树,再计算树状数组内满足条件的点数,两个点数作差就是子树内的贡献。
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+10;
struct EDGE{ int u,v,w,Next; }Edge[MAXN<<1];
struct QUE{ ll Gas; int Ban; }Q[MAXN];
struct OPT{ ll Lim; int Id,K; };
int n,Qs,Es,First[MAXN];
ll Ans[MAXN],Gas[MAXN];
vector<int>List[MAXN];
vector<OPT>Opt[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;
}
ll Min(ll A,ll B){ return A<B?A:B; }
ll Max(ll A,ll B){ return A>B?A:B; }
void Link(int u,int v,int w){ Edge[++Es]=(EDGE){u,v,w,First[u]},First[u]=Es; }
namespace BIT
{ int Top,Tr[MAXN];
void Clear(int S){ for(Top=S;S;S--) Tr[S]=0; }
int Lowbit(int K){ return K&(-K); }
int Query(int S)
{ int Ret=0;
while(S>=1) Ret+=Tr[S],S-=Lowbit(S);
return Ret;
}
void Add(int S,int K){ while(S<=Top) Tr[S]+=K,S+=Lowbit(S); }
}using BIT::Add;using BIT::Query;using BIT::Clear;
namespace Dot
{ int Rt,Heavy,Ds,Ls;
int Fa[MAXN],Anc[MAXN],Size[MAXN],Dfn[MAXN],Idfn[MAXN],Vis[MAXN],Col[MAXN];
ll Lsh[MAXN],Dis[MAXN],Up[MAXN],Down[MAXN];
void Get_Root(int Now,int Ba,int All)
{ int Maxv=0; Size[Now]=1;
for(int i=First[Now],v;i!=-1;i=Edge[i].Next)
{ if(Vis[v=Edge[i].v]||v==Ba) continue ;
Get_Root(v,Now,All),Size[Now]+=Size[v],Maxv=Max(Maxv,Size[v]);
}
Maxv=Max(Maxv,All-Size[Now]),Maxv<Heavy?(Heavy=Maxv,Rt=Now):0;
}
void Dfs1(int Now,int Ba)
{ Idfn[Dfn[Now]=++Ds]=Now,Size[Now]=1,Fa[Now]=Ba;
Anc[Now]=(Ba==Rt?Now:Anc[Ba]),Col[Now]=Rt,Lsh[++Ls]=-Down[Now];
for(int i=First[Now],v,w;i!=-1;i=Edge[i].Next)
{ v=Edge[i].v,w=Edge[i].w;
if(Vis[v]||v==Ba) continue ;
Dis[v]=Dis[Now]+Gas[Now]-w,Down[v]=Min(Dis[v],Down[Now]);
Up[v]=Min(0,Min(Up[Now]-w+Gas[v],Dis[Now]-Gas[Rt]*(Rt!=Now)+Gas[v]+Gas[Now])),Dfs1(v,Now),Size[Now]+=Size[v];
}
}
void Dfs2(int Now)
{ for(OPT Np:Opt[Now])
Ans[Np.Id]-=Query(Np.Lim)*Np.K;
Add(Down[Now],1);
for(int i=First[Now],v;i!=-1;i=Edge[i].Next)
if(!Vis[v=Edge[i].v]&&v!=Fa[Now]) Dfs2(v);
for(OPT Np:Opt[Now])
Ans[Np.Id]+=Query(Np.Lim)*Np.K;
}
void Divide(int St,int Hr)
{ Ds=Ls=0,Heavy=Hr,Get_Root(St,St,Heavy),Vis[Rt]=1;
Down[Rt]=Up[Rt]=0,Dis[Rt]=0,Dfs1(Rt,Rt);
sort(Lsh+1,Lsh+Ls+1),Ls=unique(Lsh+1,Lsh+Ls+1)-Lsh-1;
for(int i=1,Now;i<=Ds;i++)
Now=Idfn[i],Up[Now]*=-1,Down[Now]*=-1,Down[Now]=lower_bound(Lsh+1,Lsh+Ls+1,Down[Now])-Lsh,Opt[Now].clear();
for(int i=1,Now;i<=Ds;i++)
for(int Nq:List[Now=Idfn[i]])
{ int Ban=Q[Nq].Ban; ll Lim=Dis[Now]+Q[Nq].Gas+Gas[Now]-Gas[Rt];
if(Up[Now]>Q[Nq].Gas) continue ;
if(Dfn[Ban]<=Dfn[Now]&&Dfn[Now]<Dfn[Ban]+Size[Ban]&&Col[Ban]==Rt) continue ;
Lim=upper_bound(Lsh+1,Lsh+Ls+1,Lim)-Lsh-1,Opt[Rt].push_back((OPT){Lim,Nq,1});
if(Now!=Rt) Opt[Anc[Now]].push_back((OPT){Lim,Nq,-1});
else Ans[Nq]--;
if(Anc[Ban]!=Anc[Now]&&Col[Ban]==Rt) Opt[Ban].push_back((OPT){Lim,Nq,-1});
}
Clear(Ls),Dfs2(Rt);
for(int i=First[Rt],v;i!=-1;i=Edge[i].Next)
if(!Vis[v=Edge[i].v]) Divide(v,Size[v]);
}
}using Dot::Divide;
int main()
{ n=Read(),Qs=Read(),memset(First,-1,sizeof(First));
for(int i=1,u,v,w;i<n;i++) u=Read(),v=Read(),w=Read(),Link(u,v,w),Link(v,u,w);
for(int i=1;i<=n;i++) Gas[i]=Read();
for(int i=1,X;i<=Qs;i++)
X=Read(),Q[i].Gas=Read(),Q[i].Ban=Read(),List[X].push_back(i);
Divide(1,n);
for(int i=1;i<=Qs;i++) printf("%lld\n",Ans[i]+1);
}
G
简要题意
给定\(n\)个区间\((l_i,r_i)\),将这\(n\)个区间分成\(k\)组。要求每组中的区间必须有交,对于每一种划分方案,求每组区间交的长度的和,求所有方案中这个和最大是多少。
数据范围
\(1\leq n,k\leq5000\),\(0\leq l_i,r_i\leq 10^5\)
简要题解
对于处理若干区间的题,通常需要进行预处理,去掉完全包含或被包含的区间。
如果存在区间\(A\),能够完全包含区间\(B\),那么\(A\)的分组情况有两种:要么自己单独成为一组,贡献为该区间的长度,要么和它的任一子区间成为一组,贡献为0。
否则,假设区间A和区间C一组,且A与C没有包含关系,那么将A与B分到一组会更优。
接下来我们将区间分成两类\({S_i}\)和\({T_i}\),\({S_i}\)是能够完全包含某一区间的区间,那么\({T_i}\)中的区间就没有包含关系了。
我们对\({S_i}\)按长度降序排序,\({T_i}\)按右端点坐标升序排序,可以证明,最优的划分方案,一定是将\({T_i}\)分成若干组,且组内下标连续,再把\(S_i\)中最长的若干个区间单独成组。
根据这个思路不难想到动态规划,设\(F[i][j]\)表示,将\({T}\)中前\(i\)个区间分成\(j\)组,能够达到的最大贡献,若不合法则设为\(-inf\)。
状态转移方程:$$F[i][j]=max(F[k-1][j-1]+r_k-l_i+1)$$
需要保证\(k\leq i\)且\(r_k\geq l_i\)。
这里表示将\(T_k\)~\(T_i\)分成一组的情况,枚举\(i,j,k\),时间复杂度为\(O(n^3)\)。
改写一下转移方程,进行分离变量:$$F[i][j]+l_i-1=max(F[k-1][j-1]+r_k)$$
方程右边是一个只和\(k\)有关的值,可以进行单调队列优化\(Dp\)。
即对于每一个\(j\),维护一个单调队列\(Q[j]\),队列中每个元素储存\(F[k-1][j-1]\)和\(r_k\),保证\(r_k\)单增,\(F[k-1][j-1]+r_k\)单减。
每次转移前弹队首,将\(Q_{j-1}\)中\(r_k<l_i\)的扔掉,再直接利用第一个元素转移,并将转移后的信息放入\(Q_j\)队尾。
这样我们得到了\(F[i][j]\)的值,然后枚举一下\({S_i}\)中有几个单独成组,和\(F[i][j]\)合并计算答案即可。
时间复杂度可以优化至\(O(n^2)\)
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5010;
const ll Inf=1e18;
struct SEC{ int Le,Ri; }Sec[MAXN];
struct ELE{ int Ri; ll Num; };
bool Big[MAXN];
int n,K,Bs;
ll Ans,Blen[MAXN],F[MAXN][MAXN];
deque<ELE>Team[MAXN];
bool cmp2(ll A,ll B){ return A>B; }
bool cmp(SEC A,SEC B){ return A.Ri==B.Ri?A.Le>B.Le:A.Ri<B.Ri; }
ll Max(ll A,ll B){ return A>B?A:B; }
void Prepare()
{ int Maxl=-1;
sort(Sec+1,Sec+n+1,cmp);
for(int i=1;i<=n;i++)
{ Big[i]=Sec[i].Le<=Maxl,Maxl=Max(Maxl,Sec[i].Le);
if(Big[i]) Blen[++Bs]=Sec[i].Ri-Sec[i].Le+1;
}
for(int i=1;i<=n;i++)
while(Big[i]&&i<=n) swap(Sec[i],Sec[n]),swap(Big[i],Big[n]),n--;
sort(Sec+1,Sec+n+1,cmp),sort(Blen+1,Blen+Bs+1,cmp2);
for(int i=1;i<=Bs;i++) Blen[i]+=Blen[i-1];
}
void Solve()
{ for(int i=0;i<=n;i++)
for(int j=0;j<=K;j++) F[i][j]=-Inf;
F[0][0]=0;
for(int i=0;i<=K;i++) Team[i].push_back((ELE){Sec[1].Ri,F[0][i]+Sec[1].Ri});
for(int i=1;i<=n;i++)
for(int j=K;j>=1;j--)
{ while(!Team[j-1].empty()&&Team[j-1][0].Ri<Sec[i].Le) Team[j-1].pop_front();
if(Team[j-1].empty()) continue ;
F[i][j]=Team[j-1].front().Num-Sec[i].Le+1;
ll Num=F[i][j]+Sec[i+1].Ri;
while(!Team[j].empty()&&Team[j].back().Num<=Num) Team[j].pop_back();
Team[j].push_back((ELE){Sec[i+1].Ri,Num});
}
for(int i=0;i<=K;i++) Ans=Max(Ans,F[n][i]+Blen[K-i]);
}
int main()
{ scanf("%d%d",&n,&K);
for(int i=1;i<=n;i++) scanf("%d%d",&Sec[i].Le,&Sec[i].Ri),Sec[i].Ri--;
Prepare(),Solve(),printf("%lld\n",Ans);
}
J
简要题意
给定集合\(S\),集合内的元素为整数\(x_i\),\(T\)为集合\(S\)的子集,\(|T|=k\),记\(f(T)\)为\(T\)内所有元素的\(gcd\),求$$\prod_Tf(T)$$
答案对P取模
数据范围
多组数据,数据组数\(t\leq60\),\(10^6\leq P\leq10^{14}\),\(1\leq x_i\leq 8*10^4\),\(|S|\leq 4*10^4\),\(1\leq k\leq min(|S|,30)\)
简要题解
答案是所有gcd的乘积,所以可以枚举质因子及其次幂,单独计算贡献。
假设枚举到质因子\(q\)以及次幂\(x\),不难求出恰好拥有\(x\)个\(q\)的数的个数\(A\),以及拥有超过\(x\)个\(q\)的数的个数\(B\),那么这里产生的贡献是 $$ q^{x*(C(A+B,k)-C(B,k))} $$
指数太大,需要对\(\phi(P)\)取模,即P的欧拉函数
综上,此题有几个关键步骤:
1.欧拉筛法求出\(10^7\)之内的质数
2.利用质数表求出\(\phi(P)\)
3.递推求出组合数
4.枚举\(q\)和\(x\)计算贡献
5.注意卡常
P太大,可以用__int128来计算,只需要计算过程转换,为了卡常,储存还是用longlong
求组合数的时候用减法代替取模
其他地方能优化就优化
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=4e4+10;
const int MAXM=1e7+10;
bool Notp[MAXM];
int T,Ls,Ps,K,Pps;
int Seq[MAXN],Pri[MAXN*20],Minp[MAXM],Top[MAXN];
int Own[MAXN*2][22];
ll P,Ep;
ll C[MAXN][35];
ll Ans=1;
inline 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;
}
inline ll Add(ll A,ll B){ return A+=B,A>=Ep?A-Ep:A; }
inline ll Mul(ll A,ll B){ return (__int128)A*B%P; }
void Euler()
{ ll Np=P;
for(int i=1;1ll*Pri[i]*Pri[i]<=P&&i<=Pps;i++)
{ if(Np%Pri[i]) continue ;
Ep*=(Pri[i]-1),Np/=Pri[i];
while(Np%Pri[i]==0) Ep*=Pri[i],Np/=Pri[i];
}
if(Np>1) Ep*=Np-1;
}
void Combine()
{ for(int i=0;i<=40000;i++)
{ C[i][0]=1;
for(int j=1;j<=K&&j<=i;j++) C[i][j]=Add(C[i-1][j],C[i-1][j-1]);
}
}
inline ll Pow(ll Down,ll Up)
{ ll Ret=1,Now=Down;
while(Up) Up&1?Ret=Mul(Ret,Now):0,Now=Mul(Now,Now),Up>>=1;
return Ret;
}
void Divide()
{ for(int i=1;i<=Ls;i++)
{ int Num=Seq[i],Now=Seq[i],Np,Nps;
while(Now>1)
{ Np=Minp[Now],Nps=0;
while(Now%Np==0) Now/=Np,Nps++;
Own[Np][Nps]++;
}
}
for(int i=1;i<=Ps;i++)
for(int j=Top[i];j>=0;j--) Own[Pri[i]][j]+=Own[Pri[i]][j+1];
}
void Solve()
{ for(int i=1;i<=Ps;i++)
for(int j=1;j<=Top[i];j++)
Ans=Mul(Ans,Pow(Pri[i],Add(C[Own[Pri[i]][j]][K],Ep-C[Own[Pri[i]][j+1]][K])*j%Ep));
}
void Init()
{ Ans=1,Ep=1;
for(int i=0;i<=Ps;i++)
for(int j=0;j<=Top[i];j++) Own[Pri[i]][j]=0;
}
void Get_Pri()
{ for(int i=2;i<=1e7;i++)
{ if(!Notp[i]) Pri[++Ps]=i,Minp[i]=i;
for(int j=1;j<=Ps;j++)
{ if(Pri[j]*i>1e7) break ;
Notp[Pri[j]*i]=1,Minp[Pri[j]*i]=Pri[j];
if(i%Pri[j]==0) break ;
}
}
Pps=Ps,Ps=1;
while(Pri[Ps+1]<80000) Ps++;
for(int i=1;i<=Ps;i++)
{ int K=80000;
while(K>1) Top[i]++,K/=Pri[i];
}
}
int main()
{ Get_Pri();
for(T=Read();T;T--)
{ Ls=Read(),K=Read(),scanf("%lld",&P);
for(int i=1;i<=Ls;i++) Seq[i]=Read();
Init(),Euler(),Combine(),Divide(),Solve(),printf("%lld\n",(ll)Ans);
}
}