2021牛客暑期多校训练营6 部分题题解

C.Delete Edges

题目链接

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

题目链接

Gambling Monster

简要题解

这是个期望题,我们不难想到期望\(Dp\)
根据期望\(Dp\)的一般做法,我们可以设\(F[i]\)表示,当前状态为\(i\),到达状态\(n-1\)的期望步数。
初始状态\(F[n-1]=0\)
接下来考虑转移,下一步可能是原地踏步,也可能走到了更高的地方,并且花费了一步的代价:

\[F[i]=\sum_{v\leq i}P[v\bigoplus i]*F[i]+\sum_{v>i}P[v\bigoplus i] *F[v]+1 \]

改写一下变成了

\[\sum_{v>i}P[v\bigoplus i]F[i]=\sum_{v>i}P[v\bigoplus i] *F[v]+1 \]

对于左边,我们设\(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

题目链接

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);
    }
}
posted @ 2021-08-04 15:32  Alkaid~  阅读(197)  评论(0编辑  收藏  举报