【CodeForces】CodeForcesRound594 Div1 解题报告

点此进入比赛

\(A\):Ivan the Fool and the Probability Theory(点此看题面

大致题意: 给一个\(n\times m\)的矩阵\(01\)染色,使得不存在某个同色连通块大小超过\(2\)

这道题看似很神仙,实际上仔细想一想、推一推性质,还是比较简单的。

先考虑第一行,这是一个\(1\times m\)的矩阵,可以设\(f_{i,0/1}\)为第\(i\)个格子所填与上个格子不同/相同的方案数,则:\(f_{i,0}=f_{i-1,0}+f_{i-1,1},f_{i,1}=f_{i-1,0}\)

由于第一个位置可填\(0\)可填\(1\),所以初始化\(f_{1,0}=2,f_{1,1}=0\)

接下来,我们考虑从第一行向下扩展,分两种情况讨论:

  • 如果第一行不存在连续两个格子所填相同,则每一行填的数只能与上一行完全相反或是完全相同,且不能有连续超过\(2\)行完全相同。此时第一行只有\(0101...01\)\(1010...10\)两种情况,而若我们把\(0101...01\)看作\(0\)\(1010...10\)看作\(1\),那么就相当于对\(n\times 1\)的矩阵\(01\)染色,所以方案数为\(f_{n,0}+f_{n,1}\)
  • 如果第一行存在连续的两个格子所填相同,则每一行填的数只能与上一行完全相反。此时第一行有\(f_{m,0}+f_{m,1}-2\)种填法(除去第一行不存在连续两个格子所填相同的两种填法),每种第一行填法对应唯一一种全局填法,所以方案数为\(f_{m,0}+f_{m,1}-2\)

综上所述,总方案数为\(f_{n,0}+f_{n,1}+f_{m,0}+f_{m,1}-2\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define X 1000000007
using namespace std;
int n,m,f[N+5][2];
I int Qpow(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
int main()
{
    RI i;scanf("%d%d",&n,&m);
    for(f[1][0]=2,i=2;i<=max(n,m);++i) f[i][0]=(f[i-1][0]+f[i-1][1])%X,f[i][1]=f[i-1][0];
    return printf("%d",(1LL*f[n][0]+f[n][1]+f[m][0]+f[m][1]-2+X)%X),0;
}

\(B\):The World Is Just a Programming Task (Hard Version)(点此看题面

大致题意: 定义一个括号序列\(S\)的美丽值为有多少个\(p\)使能得\(S_{p,n}S_{1,p-1}\)为合法括号序列。现在你可以交换一个括号序列中两个括号,求所能得到的最大的美丽值及交换的位置。

细节巨多的题目,写了一个下午......

首先,如果\(S\)的左右括号数量不同,直接输出\(0\)\(1\ 1\)走人。

然后,我们考虑,对于一个括号序列,它的美丽值的意义。然后不难推导出,如果我们把(看作\(1\))看作\(-1\),求出前缀和\(s\)数组后,美丽值就是\(s\)中的最小值个数

假设我们交换括号\(x,y(x<y)\)\(s\)中原本的最小值为\(Mn\)

我的做法是,先对于\(x\)是左括号还是右括号进行讨论,然后再分别继续处理。

  • 对于\(x\)是左括号的情况。此时,就相当于\(s_{x\sim y-1}\)减去\(2\)
    • 如果\(s_{x\sim y-1}\)中的最小值是\(Mn\),则操作后\(s\)中的最小值是\(Mn-2\)。显然答案不会更优。
    • 如果\(s_{x\sim y-1}\)中的最小值是\(Mn+1\),则操作后\(s\)中的最小值是\(Mn-1\)。答案就是\(s_{x\sim y-1}\)\(Mn+1\)的个数。
    • 如果\(s_{x\sim y-1}\)中的最小值是\(Mn+2\),则操作后\(s\)中的最小值依然是\(Mn-2\)。答案就是\(s_{x\sim y-1}\)\(Mn+2\)的个数加上整个\(s\)\(Mn\)的个数。
  • 对于\(x\)是右括号的情况。此时,就相当于\(s_{x\sim y-1}\)加上\(2\),也就是\(s_{1\sim x-1},s_{y\sim n}\)减去\(2\),随后的讨论与上面类似。

通过上面的讨论,不难发现,在最小值不变的情况下,变化区间肯定是范围越大越好,因此我们用两个双指针来维护出最小值为\(Mn+1,Mn+2\)的两个答案区间即可。

具体实现可能有些细节要注意,可详见代码。

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con cosnt
#define CI Con int&
#define I inline
#define W while
#define N 300000
using namespace std;
int n,a[N+5],p[N+5];char s[N+5];
int main()
{
    RI i,Mn=n,res=0,ans=0,tx=1,ty=1;for(scanf("%d%s",&n,s+1),i=1;i<=n;++i)//读入+转化
        a[i]=s[i]=='('?1:-1,p[i]=p[i-1]+a[i],Mn>p[i]?(Mn=p[i],res=1):Mn==p[i]&&++res;
    if(p[n]) return puts("0\n 1 1"),0;ans=res;RI t=0,g=0;//如果左右括号数量不同,直接输出
    RI nxt1=n,nxt2=n;for(i=n-1;i;--i)//对于左边是左括号
        p[i]==Mn?(t=0,nxt1=i):(t+=p[i]==Mn+1),//维护最小值是Mn+1的答案区间
        (p[i]==Mn||p[i]==Mn+1)?(g=0,nxt2=i):(g+=p[i]==Mn+2),//维护最小值是Mn+2的答案区间
        ~a[i]&&(ans<t&&(ans=t,tx=i,ty=nxt1),ans<res+g&&(ans=res+g,tx=i,ty=nxt2));//更新答案
    RI v=0,w=0,k=0;for(t=g=0,i=1;i<=n;++i) p[i]==Mn+1&&++t,p[i]==Mn+2&&++g;//初始化
    RI lim=t;for(nxt1=nxt2=i=1;i<=n;++i)//对于左边是右括号
    {
        W(v^res) p[nxt1]==Mn&&++v,p[nxt1]==Mn+1&&--t,nxt1=nxt1%n+1;//维护最小值是Mn+1的答案区间
        W(w^lim||k^res) p[nxt2]==Mn+1&&++w,p[nxt2]==Mn&&++k,p[nxt2]==Mn+2&&--g,nxt2=nxt2%n+1;//维护最小值是Mn+2的答案区间
        !~a[i]&&ans<t&&(ans=t,tx=i,ty=nxt1),p[i]==Mn&&--v,p[i]==Mn+1&&++t,//更新最小值是Mn+1的答案,然后更新区间
        !~a[i]&&ans<res+g&&(ans=res+g,tx=i,ty=nxt2),p[i]==Mn+1&&--w,p[i]==Mn&&--k,p[i]==Mn+2&&++g;//更新最小值是Mn+2的答案,然后更新区间
    }
    return printf("%d\n%d %d",ans,tx,ty),0;//输出答案
}

\(C\):Queue in the Train(点此看题面

大致题意:\(n\)个人要灌水,每个人灌水用时皆为一个定值。每个人会在第\(p_i\)刻开始想要灌水,如果大于等于\(p_i\)的某一时刻他前方没有人在排队,他就会去排队。求每个人灌完水的时间。

模拟题,一开始理解错几个细节就挂飞了......

其实这道题说简单也很简单,个人认为我的做法可能有点复杂......

首先,我们把人以开始灌水时间为第一关键字、编号为第二关键字,排序,并用一个指针\(i\)来表示排序后的前\(i\)个人当前能够灌水。

然后,我们开一个小根堆(优先队列),把当前能够灌水的人都扔到堆里。

扔的同时,我们要判断这个人是否能去排队了,这只需要判断他之前是否有人去排队了,因此我写了一个树状数组。

如果能排队,我们就把它扔到一个队列里,果然是排队。

还有一些细节自己注意一下吧,下面放出代码以供参考。

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define LL long long
using namespace std;
int n,m,a[N+5];LL ans[N+5];queue<int> q;
struct data
{
    int p,v;I data(CI x=0,CI y=0):p(x),v(y){}
    I bool operator < (Con data& o) Con {return v^o.v?v<o.v:p<o.p;}
}s[N+5];priority_queue<int,vector<int>,greater<int> > p;
class FastIO
{
    private:
        #define FS 100000
        #define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
        #define pc(c) (C==E&&(clear(),0),*C++=c)
        #define tn (x<<3)+(x<<1)
        #define D isdigit(c=tc())
        int T;char c,*A,*B,*C,*E,FI[FS],FO[FS],S[FS];
    public:
        I FastIO() {A=B=FI,C=FO,E=FO+FS;}
        Tp I void read(Ty& x) {x=0;W(!D);W(x=tn+(c&15),D);}
        Tp I void write(Ty x) {W(S[++T]=x%10+48,x/=10);W(T) pc(S[T--]);}
        Tp I void write(Con Ty& x,Con char& y) {write(x),pc(y);}
        I void clear() {fwrite(FO,1,C-FO,stdout),C=FO;}
}F;
class TreeArray//树状数组
{
    private:
        int v[N+5];
    public:
        I void Add(RI x,CI y) {W(x<=n) v[x]+=y,x+=x&-x;}//后缀修改
        I int Qry(RI x,RI t=0) {W(x) t+=v[x],x-=x&-x;return t;}//单点查询
}T;
int main()
{
    RI i,f;LL t=0;for(F.read(n),F.read(m),i=1;i<=n;++i) F.read(a[i]),s[i]=data(i,a[i]);
    sort(s+1,s+n+1),i=1;W(i<=n||!p.empty()||!q.empty())
    {
        !(f=q.empty())&&(ans[q.front()]=(t+=m)),i<=n&&p.empty()&&q.empty()&&(t=s[i].v);//计算当前时间
        W(!p.empty()&&!T.Qry(p.top())) q.push(p.top()),T.Add(p.top()+1,1),p.pop();//处理原有能够灌水的人
        W(i<=n&&s[i].v<=t) p.push(s[i++].p),!T.Qry(p.top())&&(q.push(p.top()),T.Add(p.top()+1,1),p.pop(),0);//处理新增能够灌水的人,同时判断是否能去排队
        !f&&(T.Add(q.front()+1,-1),q.pop(),0);//当前正在灌水的人已经灌完
    }
    for(i=1;i<=n;++i) F.write(ans[i]," \n"[i==n]);return F.clear(),0;//输出答案
}

\(D\):Catowice City(点此看题面

大致题意:\(n\)个人,每人有一只猫。每个人都认识若干猫(一定认识自己的猫),现在要选出人和猫共\(n\)个,使得至少有一个人和一只猫,且每个人都不认识任何一只猫。

首先,我们可以发现,因为一个人一定认识自己的猫,所以一个数不可能被选两次。而题目要求选出\(n\)个数,就必然是把\(1\sim n\)分成两部分。

则,我们考虑如果选择了编号为\(i\)的人,那么他认识的所有的猫的主人都必须被选择,而他认识的猫的主人认识的猫的主人同样也都必须被选择,以此类推。

如果我们把编号为\(i\)的人认识编号为\(j\)的猫看作一条有向边,那么选择一个人,就相当于选择了从这个点出发能够到达的所有点。

因此,我们用\(Tarjan\)将图缩点,显然,如果原图只有一个强连通分量,就会输出\(No\),否则必然输出\(Yes\)

考虑如果我们选择一个强连通分量中的点作为人,如果这个强连通分量出度为\(0\),是肯定合法的。而根据\(Tarjan\)的原理可知,\(Tarjan\)过程中得到的第一个强连通分量,出度是必然为\(0\)的。

因此我们只要把编号为\(1\)的强连通分量中的点作为人,剩余点作为猫,即可满足题目要求了。

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 1000000
#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
#define Gmin(x,y) (x>(y)&&(x=(y)))
using namespace std;
int n,m,ee,d,cnt,T,lnk[N+5],dfn[N+5],low[N+5],col[N+5],vis[N+5],S[N+5],Sz[N+5];
struct edge {int to,nxt;}e[N+5];
class FastIO
{
    private:
        #define FS 100000
        #define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
        #define pc(c) (C==E&&(clear(),0),*C++=c)
        #define tn (x<<3)+(x<<1)
        #define D isdigit(c=tc())
        int T;char c,*A,*B,*C,*E,FI[FS],FO[FS],S[FS];
    public:
        I FastIO() {A=B=FI,C=FO,E=FO+FS;}
        Tp I void read(Ty& x) {x=0;W(!D);W(x=tn+(c&15),D);}
        Tp I void write(Ty x) {W(S[++T]=x%10+48,x/=10);W(T) pc(S[T--]);}
        Tp I void write(Con Ty& x,Con char& y) {write(x),pc(y);}
        I void writes(Con string& x) {for(RI i=0,y=x.length();i^y;++i) pc(x[i]);}
        I void clear() {fwrite(FO,1,C-FO,stdout),C=FO;}
}F;
I void Tarjan(CI x,CI lst)//Tarjan缩点
{
    dfn[x]=low[x]=++d,vis[S[++T]=x]=1;for(RI i=lnk[x];i;i=e[i].nxt) dfn[e[i].to]?
        vis[e[i].to]&&Gmin(low[x],dfn[e[i].to]):(Tarjan(e[i].to,x),Gmin(low[x],low[e[i].to]));
    if(dfn[x]^low[x]) return;Sz[col[x]=++cnt]=1,vis[x]=0;
    W(S[T]^x) ++Sz[col[S[T]]=cnt],vis[S[T--]]=0;--T;
}
int main()
{
    RI Tt,i,j,x,y;F.read(Tt);W(Tt--)
    {
        for(F.read(n),F.read(m),ee=d=cnt=0,i=1;i<=n;++i) lnk[i]=dfn[i]=vis[i]=0;//清空
        for(i=1;i<=m;++i) F.read(x),F.read(y),x^y&&add(x,y);//连边
        for(i=1;i<=n;++i) !dfn[i]&&(Tarjan(i,0),0);//Tarjan缩点
        if(cnt==1) {F.writes("No\n");continue;}F.writes("Yes\n");//判断是否有解
        F.write(Sz[1],' '),F.write(n-Sz[1],'\n');//分别输出人与猫的个数
        for(i=1;i<=n;++i) col[i]==1&&(F.write(i,' '),0);F.writes("\n");//输出强连通分量内的点,作为人
        for(i=1;i<=n;++i) col[i]^1&&(F.write(i,' '),0);F.writes("\n");//输出剩余的点,作为猫
    }return F.clear(),0;
}

\(E\):Turtle(点此看题面

大致题意: 让你把\(2n\)个元素放到一个\(2\times n\)的矩阵中,使得从左上角走到右下角(只能往下或往右)所经元素总和的最大值最小。

因为只能往下或往右走,所以我们可以找到一个关键点\(p\),使得在\(p\)点之前在第一行走,\(p\)点之后在第二行走,\(p\)点从第一行到第二行。

设所经元素总和为\(f(p)=\sum_{i=1}^pa_{1,i}+\sum_{i=p}^na_{2,i}\)

根据贪心,显而易见,第一排的数应当递增摆放,第二排的数应当递减摆放。

考虑将\(p\)\(1\)移动到\(n\),则每次移动,元素总和发生的变化其实都是加上\(a_{1,p+1}-a_{2,p}\)。由于第一排递增,第二排递减,则\(a_{1,p+1}-a_{2,p}\)也是递增的。

假设当\(p=x\)时满足使\(f(p)\)最大的同时\(x\)最大,则我们可以得到结论:\(x=1\)\(x=n\)

\(x≠1\)\(x≠n\)时,\(\because f(x)=f(x-1)+a_{1,x}-a_{2,x-1},f(x)\ge f(x-1),\therefore a_{1,x}-a_{2,x-1}\ge0.\)

\(\because a_{1,x+1}-a_{2,x}\ge a_{1,x}-a_{2,x-1},\therefore a_{1,x+1}-a_{2,x}\ge 0.\)

\(\therefore f(x+1)=f(x)+a_{1,x+1}-a_{2,x}\ge f(x).\)

这与\(p=x\)时满足使\(f(p)\)最大的同时\(x\)最大矛盾,\(\therefore x=1\)\(x=n.\)

也就是说,最大值应该是\(max(f(1),f(n))\),即\(max(a_{1,1}+\sum_{i=1}^na_{2,i},\sum_{i=1}^na_{1,i}+a_{2,n})\)

如果我们同时从两个式子中拿出\(a_{1,1}\)\(a_{2,n}\),就得到:\(a_{1,1}+a_{2,n}+max(\sum_{i=1}^{n-1}a_{2,i},\sum_{i=2}^na_{1,i})\)

由于\(a_{1,1}\)\(a_{2,n}\)无论如何都会被取到,显然它们两个应该分别填上最小值和次小值。

那么,题目也就变成了,将剩余的\(2n-2\)个数分成两个大小为\(n-1\)的集合,使得两个集合内数总和的较大值最小。

所以,我们可以先通过背包来求出这个答案。

\(f_{i,j,k}\)表示是否能在前\(i\)个数中选择\(j\)个数使得它们和为\(k\),第一维根据背包问题的常用技巧,可以在数组中去掉,变成\(f_{j,k}\)。然后由于只需知道是否可行,因此我们可以用\(bitset\)优化。

最后我们枚举\(k\),然后找到一个\(f_{n-1,k}=1\)\(max(k,s-k)\)最小的\(k\)\(s\)为这\(2n-2\)个数的和)。

然后,我们考虑题目要求输出一个合法方案。

则我们用\(Meet\ in\ middle\),搜索过程中记录\(t,v,s\)分别表示选择数的个数、选择数的和以及选择数状压后的状态。

这样一来,这道题就算做完了。

说实话,其实最后的实现是很简单的,难点主要是前面的部分。

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 25
#define V 50000
#define LL long long
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)<(y)?(x):(y))
#define Gmin(x,y) (x>(y)&&(x=(y)))
using namespace std;
int n,a[2*N+5];
class DpSolver//背包
{
    private:
        bitset<2*N*V+5> f[2*N+5];
    public:
        I int GetAns()
        {
            RI i,j,s=0,ans=s;for(i=3;i<=2*n;++i) s+=a[i];//记录数的和
            for(f[0].set(0),i=3;i<=2*n;++i) for(j=min(n-1,i-2);j;--j) f[j]|=f[j-1]<<a[i];//背包
            for(i=0;i<=s;++i) f[n-1].test(i)&&max(ans,s-ans)>max(i,s-i)&&(ans=i);//统计答案
            return min(ans,s-ans);//返回
        }
}DP;
class DfsSolver//Meet in middle
{
    private:
        #define pb push_back
        LL w[N+5][N*V+5];
        I void dfs1(CI p,CI x,CI y,CI t,CI v,Con LL& s)//前一半dfs
        {
            if(x>y) return (void)(!~w[t][v]&&(w[t][v]=s));//记录答案
            dfs1(p,x+1,y,t,v,s),v+a[x]<=p&&(dfs1(p,x+1,y,t+1,v+a[x],s|(1LL<<x)),0);return;
        }
        I void dfs2(CI p,CI x,CI y,CI t,CI v,Con LL& s)//后一半dfs
        {
            if(x>y)
            {
                if(!~w[n-1-t][p-v]) return;RI i;LL g=w[n-1-t][p-v]|s;
                printf("%d ",a[1]);for(i=3;i<=2*n;++i) g>>i&1&&printf("%d ",a[i]);putchar('\n');//第一行,升序输出
                for(i=2*n;i>=3;--i) !(g>>i&1)&&printf("%d ",a[i]);printf("%d\n",a[2]);exit(0);//第二行,降序输出
            }
            dfs2(p,x+1,y,t,v,s),v+a[x]<=p&&(dfs2(p,x+1,y,t+1,v+a[x],s|(1LL<<x)),0);
        }
    public:
        I void Solve(CI x) {memset(w,-1,sizeof(w)),dfs1(x,3,n+1,0,0,0),dfs2(x,n+2,2*n,0,0,0);}
}DFS;
int main()
{
    RI i,j;for(scanf("%d",&n),i=1;i<=2*n;++i) scanf("%d",a+i);
    return sort(a+1,a+2*n+1),DFS.Solve(DP.GetAns()),0;
}

\(F\):Swiper, no swiping!(点此看题面

大致题意: 让你在一张图中删去若干点(不能不删,也不能删完),使得剩余的每个点度数模\(3\)的余数不变。

大码量多细节分类讨论题,调了两天......(话说这场比赛实在太多细节了,心态爆炸啊)

首先,这里我们按照一个点度数模\(3\)的余数将其分为\(0\)类点、\(1\)类点、\(2\)类点。(注意,下面的每一个情况都建立于之前情况不成立的基础上)

  • 如果存在\(0\)类点:对于\(n=1\)的情况输出\(No\),否则保留这个点。
  • 如果存在一个由\(2\)类点组成的环:对于整张图是一个环的情况输出\(No\),否则保留这个环。(注意,这个环必须是简单环,不然度数会出锅)
  • 如果存在两个\(1\)类点:对于整张图是一条链的情况输出\(No\),否则保留一条以两个\(1\)类点为端点、中间全为\(2\)类点的链。(同样要注意这些\(2\)类点之间不能有边,可以用\(BFS\)
  • 此时,由于没有\(0\)类点、没有超过\(1\)\(1\)类点、没有\(2\)类点组成的环,所以可以保证这张图必然由一个\(1\)类点和一片\(2\)类点森林组成。可以证明,必然存在至少两棵树皆与这个\(1\)类点有至少两条边相连。则在两棵树上各找出一条以和\(1\)号点有边相连的点为端点的链,这个以\(1\)号点为唯一交点的两个相交的环,显然是符合要求的。但如果图中只有这样一对相交的环,也应该输出\(No\)。(再次强调,要注意这些\(2\)类点之间不能有边相连,我已经被这坑惨了)

具体实现可以看代码。

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 500000
#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
using namespace std;
int n,m,ee,x[N+5],y[N+5],s[N+5],q[N+5],lst[N+5],lnk[N+5],deg[N+5],vis[N+5],fa[N+5],tag[N+5];
struct edge {int to,nxt;}e[2*N+5];
struct data {int v;I bool operator < (Con data& o) Con {return fa[v]<fa[o.v];}}p[N+5];
class FastIO
{
    private:
        #define FS 100000
        #define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
        #define pc(c) (C==E&&(clear(),0),*C++=c)
        #define tn (x<<3)+(x<<1)
        #define D isdigit(c=tc())
        int T;char c,*A,*B,*C,*E,FI[FS],FO[FS],S[FS];
    public:
        I FastIO() {A=B=FI,C=FO,E=FO+FS;}
        Tp I void read(Ty& x) {x=0;W(!D);W(x=tn+(c&15),D);}
        Tp I void write(Ty x) {W(S[++T]=x%10+48,x/=10);W(T) pc(S[T--]);}
        Tp I void write(Con Ty& x,Con char& y) {write(x),pc(y);}
        I void writes(Con string& x) {for(RI i=0,y=x.length();i^y;++i) pc(x[i]);}
        I void clear() {fwrite(FO,1,C-FO,stdout),C=FO;}
}F;
I int getfa(CI x) {return fa[x]^x?fa[x]=getfa(fa[x]):x;}
I void FindLine(CI x)//有两个度数为1的点,找出一条链
{
    RI i,k,H=1,T=0;vis[q[++T]=x]=1;W(H<=T)
    {
        if(deg[k=q[H++]]%3==1&&k^x) break;
        for(i=lnk[k];i;i=e[i].nxt) !vis[e[i].to]&&(lst[q[++T]=e[i].to]=k,vis[e[i].to]=1);
    }W(s[k]=1,k^x) k=lst[k];
}
I void FindOnTree(CI x,CI st)//在以x为根的树上找出一条合法路径 
{
    RI i,k,H=1,T=0;vis[q[++T]=x]=1;W(H<=T)
    {
        if(tag[k=q[H++]]&&k^x) break;
        for(i=lnk[k];i;i=e[i].nxt) !vis[e[i].to]&&(lst[q[++T]=e[i].to]=k,vis[e[i].to]=1);
    }W(s[k]=1,k^x) k=lst[k];
}
I bool FillCircle(CI x,CI st,CI f=1)//找出以st为环上一点的简单环,f表示点数
{
    if(f>2) for(RI i=lnk[x];i;i=e[i].nxt) if(e[i].to==st) return s[x]=1;//先判断是否有环 
    vis[x]=2;for(RI i=lnk[x];i;i=e[i].nxt) if(vis[e[i].to]==1)
        if(FillCircle(e[i].to,st,f+1)) return s[x]=1;return vis[x]=1,false;
}
I bool FindCircle(CI x,CI lst)//找度数模3余2的点之间的环
{
    vis[x]=1;for(RI i=lnk[x];i;i=e[i].nxt) if(e[i].to^lst&&vis[e[i].to]) return FillCircle(x,x);//将环补充完整 
    for(RI i=lnk[x];i;i=e[i].nxt) if(e[i].to^lst&&FindCircle(e[i].to,x)) return true;return false;
}
int main()
{
    freopen("a.in","r",stdin),freopen("a.out","w",stdout);
    RI Tt,i,j,t1,t2;F.read(Tt);W(Tt--)
    {
        for(F.read(n),F.read(m),ee=0,i=1;i<=n;++i) s[i]=lnk[i]=deg[i]=vis[i]=tag[i]=0,fa[i]=i;//清空
        for(i=1;i<=m;++i) F.read(x[i]),F.read(y[i]),++deg[x[i]],++deg[y[i]];//读入边,计算点数 
        if(n==1) {F.writes("No\n");continue;}//只有一个点,无解 
        for(i=1;i<=n;++i) if(!(deg[i]%3)) {s[i]=1;goto End;}//存在度数模3余0的点,保留该点 
        for(t2=0,i=1;i<=n;++i) deg[i]==2&&++t2;if(t2==n) {F.writes("No\n");continue;}//整张图是一个环,无解 
        for(i=1;i<=m;++i) deg[x[i]]%3==2&&deg[y[i]]%3==2&&
            (fa[getfa(x[i])]=getfa(y[i]),add(x[i],y[i]),add(y[i],x[i]));//给度数模3余2的点连边
        for(i=1;i<=n;++i) if(deg[i]%3==2&&!vis[i]&&FindCircle(i,0)) goto End;//找度数模3余2的点之间的环 
        for(i=1;i<=n;++i) vis[i]=0; 
        for(t1=t2=0,i=1;i<=n;++i) deg[i]==1&&++t1,deg[i]==2&&++t2;
        if(t1==2&&t2==n-2) {F.writes("No\n");continue;}//整张图是一条链,无解
        for(t1=0,i=1;i<=n;++i) deg[i]%3==1&&++t1;if(t1>1)//有两个度数模3余1的点,找出一条链
        {
            for(i=1;i<=m;++i) ((deg[x[i]]%3)^2||(deg[y[i]]%3)^2)&&(add(x[i],y[i]),add(y[i],x[i]));//补上之前没有连的边 
            for(i=1;i<=n;++i) if(deg[i]%3==1) {FindLine(i);goto End;}//搜索出一条路径 
        } 
        for(t1=t2=0,i=1;i<=n;++i) deg[i]%3==1&&(t1=i),deg[i]==2&&++t2;
        if(deg[t1]==4&&t2==n-1) {F.writes("No\n");continue;}//整张图是两个相连的环,无解
        for(s[t1]=1,t2=0,i=1;i<=m;++i) x[i]==t1&&(tag[p[++t2].v=y[i]]=1),y[i]==t1&&(tag[p[++t2].v=x[i]]=1);//记下与度数模3余1的点相连的点 
        for(i=1;i<=n;++i) deg[i]%3==2&&(fa[i]=getfa(i)); 
        for(t1=0,sort(p+1,p+t2+1),i=1;i<t2;++i) if(fa[p[i].v]==fa[p[i+1].v])
            if(FindOnTree(p[i].v,p[i].v),!t1) {t1=1;W(i<t2&&fa[p[i].v]==fa[p[i+1].v]) ++i;}else goto End;//从两棵树中找一条合法路径 
        End:for(t1=0,i=1;i<=n;++i) !s[i]&&++t1;F.writes("Yes\n"),F.write(t1,'\n');//统计删去的点数 
        for(i=1;i<=n;++i) !s[i]&&(F.write(i,' '),0);F.writes("\n");//输出删去的点 
    }return F.clear(),0;
}
posted @ 2019-11-14 14:41  TheLostWeak  阅读(...)  评论(... 编辑 收藏