2021牛客暑期多校训练营6 部分题题解
C.Delete Edges
题目链接
简要题解
这是个构造题,官方给出的构造只有一种,而且证明过程很复杂。
先说结论:输出所有满足\(i+j+k=n\)和\(i+j+k=2*n\)的无序三元组\((i,j,k)\)
此处只证明该方案不会重复删边。
如果出现了重复删边的情况,那么必然是某两个三元组内出现了两个相同元素。
由于每个元素的大小不超过\(n\),那么确定了三元组的两个元素之后我们可以唯一确定第三个元素。
所以在这个构造方案下,重复删边只可能是出现了两个相同的三元组,这个情况我们完全可以避免,因此该方案不会重复删边。
代码如下:
#include<bits/stdc++.h>
using namespace std;
struct Tri{ int A,B,C; }Ans[1000000];
int n,Cnt;
int main()
{ scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
{ if(n-i-j>j) Ans[++Cnt]=(Tri){i,j,n-i-j};
if(2*n-i-j>j&&2*n-i-j<=n) Ans[++Cnt]=(Tri){i,j,2*n-i-j};
}
printf("%d\n",Cnt);
for(int i=1;i<=Cnt;i++) printf("%d %d %d\n",Ans[i].A,Ans[i].B,Ans[i].C);
}
D.Gambling Monster
题目链接
简要题解
这是个期望题,我们不难想到期望\(Dp\)。
根据期望\(Dp\)的一般做法,我们可以设\(F[i]\)表示,当前状态为\(i\),到达状态\(n-1\)的期望步数。
初始状态\(F[n-1]=0\)
接下来考虑转移,下一步可能是原地踏步,也可能走到了更高的地方,并且花费了一步的代价:
改写一下变成了
对于左边,我们设\(G[i]=\sum_{v>i}P[v\bigoplus i]\),那么这个系数是确定的,我们可以想办法提前算出来。
我们设\(w=v\bigoplus i\),那么考虑哪些\(i\)会使得\(w\bigoplus i>i\)。
由于涉及到大小比较,因此从高位开始考虑显然更合理,因为高位对于大小的影响更大。
对于\(w\)最高位的\(1\)来说,如果\(i\)在该位为\(0\),那么异或的值肯定变大。如果\(i\)在该位为\(1\),那么异或的值肯定变小。
我们枚举\(w\),得到\(w\)的最高位,对于所有在该位为\(0\)的\(i\),我们把\(G[i]\)加上\(P[w]\)。
暴力做是\(n^2\)的,不过我们可以利用差分将预处理的复杂度优化至\(O(nlogn)\)。
接下来考虑右边,冷静观察就会发现,\((v\bigoplus i)\bigoplus v=i\),右边是一个异或卷积的形式,我们可以利用\(FWT\)来加速。
对于\(i\)来说,我们需要所有大于它的数的贡献,\(CDQ\)分治是处理这个问题的利器。
现在只剩下一个问题了:如何保证\(CDQ\)分治中\(FWT\)的复杂度?
\(FWT\)在卷积过程中信息和下标有关,我们一般不能直接改变下标,但是我们也不能每一层分治都做一遍\(nlogn\)的\(FWT\)。
实际上,对于一个区间内的所有数,二进制下只有最后若干位会发生变化。
同时由于本题\(n+1=2^x\),因此所有区间的长度都只包含质因子\(2\)。
我们完全可以把卷积数组的长度压缩至区间长度,确定卷积下标与原数组下标的对应关系。
做完卷积之后根据下标的对应关系在相应位置加上贡献即可。
时间复杂度\(O(n*log^2n)\)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=(1<<16)+10;
const int Inv2=500000004;
const int Mod=1e9+7;
int T,n,Sp,P[MAXN],F[MAXN],G[MAXN],R[MAXN];
int Log[MAXN],Pre[MAXN];
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 Pow(int Down,int Up)
{ int Ret=1,Now=Down;
while(Up>=1) Up&1?Ret=1ll*Ret*Now%Mod:0,Now=1ll*Now*Now%Mod,Up>>=1;
return Ret;
}
int Add(int A,int B){ return A+=B,A>=Mod?A-Mod:A; }
namespace Trans
{ int Len,A[MAXN],B[MAXN];
void FWT(int *P,int K)
{ for(int i=1;i<Len;i<<=1)
for(int Pos=i<<1,j=0;j<Len;j+=Pos)
for(int k=0;k<i;k++)
{ int X=P[j+k],Y=P[i+j+k];
P[j+k]=Add(X,Y),P[i+j+k]=Add(X,Mod-Y);
if(K==-1) P[j+k]=1ll*P[j+k]*Inv2%Mod,P[i+j+k]=1ll*P[i+j+k]*Inv2%Mod;
}
}
}using namespace Trans;
void Get_G()
{ for(int i=1;i<=n-1;i++)
for(int j=0;j<=n-1;j+=1<<Log[i]) Pre[j]=Add(Pre[j],P[i]),j+=1<<Log[i],Pre[j]=Add(Pre[j],Mod-P[i]);
G[0]=Pre[0];
for(int i=1;i<=n-1;i++) G[i]=Add(G[i-1],Pre[i]);
}
int Divide(int Le,int Ri,int K)
{ if(Le==Ri) return F[Le]=1ll*(R[Le]+1)*Pow(G[Le],Mod-2)%Mod;
int Mid=(Le+Ri)>>1;
Divide(Mid+1,Ri,K-1),Len=Mid-Le+1;
for(int i=Le;i<=Mid;i++) A[i-Le]=P[i-Le+Len],B[i-Le]=F[i+Len];
FWT(A,1),FWT(B,1);
for(int i=0;i<Len;i++) A[i]=1ll*A[i]*B[i]%Mod;
FWT(A,-1);
for(int i=Le;i<=Mid;i++) R[i]=Add(R[i],A[i-Le]);
return Divide(Le,Mid,K-1);
}
void Init()
{ for(int i=0;i<=n;i++) Pre[i]=G[i]=R[i]=F[i]=0;
Sp=0;
}
int main()
{ for(int i=2;i<=(1<<16);i++) Log[i]=Log[i>>1]+1;
for(T=Read();T;T--)
{ n=Read(),Init();
for(int i=0;i<=n-1;i++) P[i]=Read(),Sp+=P[i];
Sp=Pow(Sp,Mod-2);
for(int i=0;i<=n-1;i++) P[i]=1ll*P[i]*Sp%Mod;
Get_G(),Divide(0,n-1,Log[n]),printf("%d\n",F[0]);
}
}
J.Defend Your Country
题目链接
简要题解
在赛场上,我们可以猜一个结论:
如果\(n\)为偶数,那么不需要操作。如果\(n\)为奇数,那么最优方案一定是只删除一个点连接的所有边,只有这个点的贡献会变负。
我们把这个结论的证明放在最后,先说如何解题。
如果我们要删点,自然想删掉权值最小的那个点。
对于非割点来说,删去不改变连通性,可以直接删掉。
对于割点来说,可能与它连通的所有连通块都包含了偶数个点,那么这个割点可以被删掉,否则该割点不能被删掉。
如何判断这个割点是否可以被直接删去呢?自然我们首先需要判断哪些是割点。
利用\(Tarjan\)算法我们可以求出割点,并且在\(Tarjan\)的过程中我们会随机跑出一棵生成树,我们维护这棵生成树的上所有点的子树大小。
对于一个割点来说,它的一部分子树和上面连通,还有一部分子树和上面不连通。
对于和上面不连通的子树来说,删去割点之后这棵子树就将独立成一个连通块,如果这棵子树的大小为奇数,则该割点不能被删掉。
对于和上面连通的子树来说,我们不需要去管,因为删去割点之后,所有连通块大小的和为偶数,如果出现奇数个连通块,必定至少出现两个。
和上面连通的所有子树,包括割点上面的一整块,在割点被删去后都在同一个连通块内。
若这个连通块大小为奇数,那么必定存在一个和上面不连通的子树,其大小为奇数。
接下来是结论的证明:
反证法,假设最优方案是删去多个点,那么一定是删去奇数个割点。
如果删去的点不是割点,只删那一个非割点显然更优。
假设我们有了原图的点双树,再从点双树上把我们要删除的割点拿出来做一棵虚树。
由于我们只删去了这些割点,那么删去割点之后剩下的连通块大小全部都是偶数。
那么如果我们只删去虚树上的一个叶子割点,它在点双树上的所有子树大小都是偶数,则剩下的所有连通块的大小也都是偶数。
这是一个合法方案,并且显然只删去这一个割点更优,因此假设不成立。
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+10;
const int Inf=2147483647;
struct EDGE{ int u,v,Next; }Edge[MAXN*4];
int T,n,m,Es,First[MAXN],Val[MAXN];
ll Ans;
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; }
int Min(int A,int B){ return A<B?A:B; }
void Link(int u,int v){ Edge[++Es]=(EDGE){u,v,First[u]},First[u]=Es; }
namespace Tar
{ int Ds,Dfn[MAXN],Low[MAXN],Cut[MAXN],Size[MAXN],Good[MAXN];
void Tarjan(int Now,int Ba,int Cnt=0)
{ Dfn[Now]=Low[Now]=++Ds,Size[Now]=1;
for(int i=First[Now],v;i!=-1;i=Edge[i].Next)
if(Dfn[v=Edge[i].v]) Low[Now]=Min(Low[Now],Dfn[v]);
else
{ Tarjan(v,Ba),Size[Now]+=Size[v],Low[Now]=Min(Low[Now],Low[v]);
if(Size[v]%2&&Low[v]>=Dfn[Now]) Good[Now]=0;
Cut[Now]|=(Low[v]>=Dfn[Now]&&Now!=Ba),Cnt+=(Now==Ba);
}
Cut[Now]|=(Now==Ba&&Cnt>=2);
}
}using namespace Tar;
void Do()
{ int Nv=Inf;
for(int i=1;i<=n;i++)
if(!Cut[i]) Nv=Min(Nv,Val[i]);
else if(Good[i]) Nv=Min(Nv,Val[i]);
Ans-=2ll*Nv;
}
void Init()
{ for(int i=0;i<=n;i++) First[i]=-1,Cut[i]=Size[i]=Dfn[i]=Low[i]=0,Good[i]=1;
Ans=Es=Ds=0;
}
int main()
{ for(T=Read();T;T--)
{ n=Read(),m=Read(),Init();
for(int i=1;i<=n;i++) Val[i]=Read(),Ans+=Val[i];
for(int i=1,u,v;i<=m;i++) u=Read(),v=Read(),Link(u,v),Link(v,u);
if(n%2==0) printf("%lld\n",Ans);
else Tarjan(1,1),Do(),printf("%lld\n",Ans);
}
}