一句话题解

QwQ主要是因为这篇文章写的有点长……有时候要找某一个题可能不是很好找,所以写了这个东西。

具体的题意、题解和代码可以再往下翻。_(:з」∠)_

AGC 001

C:枚举中点/中边。

D:构造。

E:根据组合数意义转为$DP$。

F:拓扑排序,线段树优化连边。

AGC 002

C:水题,看是否有a[i]+a[i+1]>=k。

D:并查集上倍增,二分答案。

E:博弈(坑)

F:模型转化然后$DP$。

AGC 003

C:一个数到自己应到位置距离为奇数的个数/2。

D:数学,质因数分解,素数筛。

E:转化为递增序列,拆分(建议看代码理解……)

F:难的一比的矩阵快速幂。

AGC 004

C:构造。

D:贪心,满了就断。

E:$DP$,设$f[i][j][k][l]$表示取完了$(i,j)(k,l)$这个矩形内的机器人。

F:分类讨论,模型转化。

AGC 005

C:构造。

D:建立二分图,转化后$DP$。

E:贪心?博弈?

F:$NTT$。

AGC 006

C:差分,倍增。

D:二分塔顶的数,把底层$01$转化。

E:结论题……

F:三元环,对染色过程分类讨论。

AGC 007

C:期望。不会。

D:线段树优化$DP$。

E:二分,合并二元组。

F:用折线覆盖变换的过程,需要操作的步数就是折线的个数。

AGC 008

C:讨论奇偶性。

D:按出现位置$sort$一下从前往后填就好了。

E:没看懂题解。(坑)

F:$DP$求最短路和次短路。

AGC 009

C:设f[i]表示第i个元素放到Y集合的方案数。

D:神仙性质题?

E:神仙$DP$。

AGC 010

C:贪心。

D:博弈,根据偶数个数的奇偶来讨论一下。


AGC 001

C - Shorten Diameter

题意:

给定一颗节点个数为n的树,问最少删除多少个节点能使得直径小于等于k

题解:

若k为偶数,枚举作为直径中点的点。若k为奇数,枚举作为直径中点的边。

 #include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#define N (2000+100)
using namespace std; struct Edge{int to,next;}edge[N<<];
int n,k,u[N],v[N],dis[N],ans=;
int head[N],num_edge;
queue<int>q; void add(int u,int v)
{
edge[++num_edge].to=v;
edge[num_edge].next=head[u];
head[u]=num_edge;
} int main()
{
scanf("%d%d",&n,&k);
for (int i=; i<=n-; ++i)
{
scanf("%d%d",&u[i],&v[i]);
add(u[i],v[i]); add(v[i],u[i]);
} if (k%==)
{
for (int i=; i<=n; ++i)
{
int cnt=;
memset(dis,-,sizeof(dis));
dis[i]=; q.push(i);
while (!q.empty())
{
int x=q.front(); q.pop();
for (int j=head[x]; j; j=edge[j].next)
if (dis[edge[j].to]==-)
{
dis[edge[j].to]=dis[x]+;
if (dis[edge[j].to]<=k/) cnt++;
q.push(edge[j].to);
}
}
ans=min(ans,n-cnt);
}
}
else
{
for (int i=; i<=n-; ++i)
{
int cnt=;
memset(dis,-,sizeof(dis));
dis[u[i]]=dis[v[i]]=; q.push(u[i]); q.push(v[i]);
while (!q.empty())
{
int x=q.front(); q.pop();
for (int j=head[x]; j; j=edge[j].next)
if (dis[edge[j].to]==-)
{
dis[edge[j].to]=dis[x]+;
if (dis[edge[j].to]<=k/) cnt++;
q.push(edge[j].to);
}
}
ans=min(ans,n-cnt);
}
}
printf("%d",ans);
}

D - Arrays and Palindrome

题意:

给出一个数字之和为N的顺序可变的数组,构造一个数字和为N的数组,满足对于任意满足前两个条件的字符串,一定满足第三个条件:

1.从头开始的a1个字符,a1个后紧接着的a2个、a3个……分别为回文串。
2.从头开始的b1个字符,b1个后紧接着的b2个、b3个……分别为回文串。
3.该字符串仅由同一字符构成。

题解:

一个鬼畜有趣的构造题……还TM被卡输出空格和回车了……→QAQ

 #include<iostream>
#include<cstdio>
#define N (100000+1000)
using namespace std; int a[N],b[N],cnt,n,m; int main()
{
scanf("%d%d",&n,&m);
for (int i=; i<=m; ++i)
{
scanf("%d",&a[i]);
if (a[i]%) b[++cnt]=i;
}
if (cnt>) {puts("Impossible"); return ;}
if (m==)
{
if (a[]==) printf("1\n1\n1\n");
else printf("%d\n2\n1 %d\n",a[],a[]-);
return ;
}
if (b[]) swap(a[],a[b[]]);
if (b[]) swap(a[m],a[b[]]); for (int i=; i<=m; ++i)
printf("%d%c",a[i]," \n"[i==m]);
a[]++; a[m]--;
printf("%d\n",a[m]==?m-:m);
for (int i=; i<=m; ++i)
if (a[i]!=) printf("%d%c",a[i]," \n"[i==m]);
}

E - BBQ Hard

题意:

询问$\sum_{1 \leq i,j \leq n,i \neq j} {a_i+a_j \choose a_i+a_j+b_i+b_j}$

题解:

一个比较巧妙的题,对于任意i,j,可以发现${a_i+a_j \choose a_i+a_j+b_i+b_j}$可以被描述成平面上从$(-a_i,-b_i)$到$(a_j,b_j)$的方案数

所以只需要把平面上所有点$(-a_i,-b_i)$标记成1,然后暴力dp就可以了:$f_{i,j}=f_{i-1,j}+f_{i,j-1}$,注意要把从$(-a_i,-b_i)$到$(a_i,b_i)$的方案数减去

 #include<iostream>
#include<cstdio>
#define N (200000+100)
#define MOD (1000000007)
using namespace std;
long long n,a[N],b[N],f[][],ans;
long long Inv[N],Fac[N],FacInv[N]; void Init()
{
Fac[]=; Inv[]=; FacInv[]=;
for (int i=; i<=; ++i)
{
if (i!=) Inv[i]=(MOD-MOD/i)*Inv[MOD%i]%MOD;
Fac[i]=Fac[i-]*i%MOD; FacInv[i]=FacInv[i-]*Inv[i]%MOD;
}
} long long C(long long n,long long m)
{
if (m>n) return ;
return Fac[n]*FacInv[m]%MOD*FacInv[n-m]%MOD;
} int main()
{
Init();
scanf("%lld",&n);
for (int i=; i<=n; ++i)
{
scanf("%lld%lld",&a[i],&b[i]);
f[-a[i]+][-b[i]+]++;
}
for (int i=; i<=; ++i)
for (int j=; j<=; ++j)
{
if (i) (f[i][j]+=f[i-][j])%=MOD;
if (j) (f[i][j]+=f[i][j-])%=MOD;
}
for (int i=; i<=n; ++i)
(ans+=f[a[i]+][b[i]+])%=MOD;
for (int i=; i<=n; ++i)
ans=((ans-C(*a[i]+*b[i],*b[i]))%MOD+MOD)%MOD;
printf("%lld",(ans*Inv[])%MOD);
}

F - Wide Swap

题意:

给你一个序列,你可以任意交换两个距离大于等于k,且差的绝对值为1的数的位置,求可以实现的字典序最小的序列。

题解:

一个神题……首先我们设$pos_{a_i}=i$,显然要令a字典序最小,只要让pos字典序最小就好了。因为“权值小的尽量靠前”和“前面的权值尽量小”是一个意思。

那么对于pos数组,相邻且权值差>=k的可以交换顺序。也就是说,i与后面所有的j比,若$abs(pos_i-pos_j)<k$,那么$pos_i$和$pos_j$的相对顺序就是确定不变的,连一条$pos_i$到$pos_j$的边即可。这显然是一个DAG,连完边跑拓扑排序。

但这样的话显然边数是$n^2$级别的,发现有一些边是没有用的,所以考虑优化掉一些的边。

例如三个点x,y,z。x->y, y->z, x->z,可以发现x->z这条边是没有用的。

对于$pos_i$,我们是向$(pos_i-k+1,pos_i-1)$,$(pos_i+1,pos_i+k-1)$这两个区间连边的。我们可以从n~1枚举,查询$(1,pos_i-1)$和$(pos_i+1,n)$两个区间内的两个最小元素x和y,$pos_i$向$pos_x$和$pos_y$连边即可(可行的原因可以考虑一下拓扑序小的在前的性质)。查询最小值可以用线段树。

 #include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#define N (500000+1000)
#define INF (10000000)
using namespace std; struct Edge{int to,next;}edge[N<<];
int n,k,a[N],pos[N],Segt[N<<];
int head[N],num_edge,Ind[N];
priority_queue<int,vector<int>,greater<int> >q; void add(int u,int v)
{
edge[++num_edge].to=v;
edge[num_edge].next=head[u];
head[u]=num_edge;
Ind[v]++;
} void Build(int now,int l,int r)
{
if (l==r){Segt[now]=INF; return;}
int mid=(l+r)>>;
Build(now<<,l,mid); Build(now<<|,mid+,r);
Segt[now]=min(Segt[now<<],Segt[now<<|]);
} int Query(int now,int l,int r,int l1,int r1)
{
if (r<l1 || l>r1) return INF;
if (l1<=l && r<=r1) return Segt[now];
int mid=(l+r)>>;
return min(Query(now<<,l,mid,l1,r1),Query(now<<|,mid+,r,l1,r1));
} void Update(int now,int l,int r,int x,int k)
{
if (l==r) {Segt[now]=k; return;}
int mid=(l+r)>>;
if (x<=mid) Update(now<<,l,mid,x,k);
else Update(now<<|,mid+,r,x,k);
Segt[now]=min(Segt[now<<],Segt[now<<|]);
} void Toposort()
{
int cnt=;
for (int i=; i<=n; ++i) if (!Ind[i]) q.push(i);
while (!q.empty())
{
int x=q.top(); q.pop(); a[x]=++cnt;
for (int i=head[x]; i; i=edge[i].next)
{
Ind[edge[i].to]--;
if (!Ind[edge[i].to]) q.push(edge[i].to);
}
}
for(int i=; i<=n; ++i) printf("%d\n",a[i]);
} int main()
{
scanf("%d%d",&n,&k);
for (int i=; i<=n; ++i)
scanf("%d",&a[i]),pos[a[i]]=i;;
Build(,,n);
for (int i=n; i>=; --i)
{
int x=Query(,,n,pos[i]+,min(pos[i]+k-,n));
if (x!=INF) add(pos[i],pos[x]);
int y=Query(,,n,max(pos[i]-k+,),pos[i]-);
if (y!=INF) add(pos[i],pos[y]);
Update(,,n,pos[i],i);
}
Toposort();
}

AGC 002

C - Knot Puzzle

题意:

有n段绳子,将他们打结形成一段有n-1个绳结的绳子。每次可以选择一段长度大于k的绳子解开它的任意一个绳结。问将所有绳结解开的顺序。

题解:

水题,看是否有a[i]+a[i+1]>=k,有的话以这两个为中心两头拆。

 #include<iostream>
#include<cstdio>
#define N (100000+1000)
using namespace std; int n,k,a[N]; int main()
{
scanf("%d%d",&n,&k);
for (int i=; i<=n; ++i)
scanf("%d",&a[i]);
for (int i=; i<=n-; ++i)
if (a[i]+a[i+]>=k)
{
puts("Possible");
for (int j=; j<=i-; ++j) printf("%d\n",j);
for (int j=n-; j>=i+; --j) printf("%d\n",j);
printf("%d\n",i);
return ;
}
puts("Impossible");
}

D - Stamp Rally

题意:

给出一个无向连通图。有q次询问,每次询问给出两个不同的点x和y,还有一个参数z。求一个最小的k使得只考虑编号不超过k的边时,x的连通块和y的连通块中的点的并集不小于z。

题解:

并查集+倍增好题……首先我们可以二分一个答案mid,那么问题就转化成了求一个点走的边的编号不超过mid的最大连通块。

加第i条边的时候,新创建一个权值为i的点new,然后把x,y两个点并查集的根连到new。可以发现这样做原图中的点都是叶子节点。那么加入第i条边的时候new点子树的叶子数量就是连通块大小。还发现点权值是随着点的深度减小而增大的,所以就可以用倍增了。

具体做法就是让x,y两个点跳到权值<=mid的最高的地方。如果两个点跳到一起就说明他们两个在可以用编号<=mid的边的时候处于同一个连通块,连通块大小就是这个点的size,否则就是两个点分别跳到的点的size之和。

 #include<iostream>
#include<cstring>
#include<cstdio>
#define N (200000+1000)
using namespace std; int n,m,x,y,z,q,tot,Father[N],f[N][],Size[N],Val[N]; int Find(int x)
{
if (x==Father[x]) return x;
return Father[x]=Find(Father[x]);
} int Get(int x,int v)
{
for (int i=; i>=; --i)
if (f[x][i] && Val[f[x][i]]<=v) x=f[x][i];
return x;
} int check(int x,int y,int v)
{
int xx=Get(x,v), yy=Get(y,v);
if (xx==yy) return Size[xx];
return Size[xx]+Size[yy];
} int main()
{
scanf("%d%d",&n,&m); tot=n;
for (int i=; i<=n*; ++i)
Father[i]=i,Size[i]=(i<=n);
for (int i=; i<=m; ++i)
{
scanf("%d%d",&x,&y);
if (Find(x)==Find(y)) continue;
int fx=Find(x), fy=Find(y);
tot++; Val[tot]=i;
Size[tot]=Size[fx]+Size[fy];
Father[fx]=Father[fy]=f[fx][]=f[fy][]=tot;
}
for (int j=; j<=; ++j)
for (int i=; i<=*n; ++i)
f[i][j]=f[f[i][j-]][j-];
scanf("%d",&q);
for (int i=; i<=q; ++i)
{
scanf("%d%d%d",&x,&y,&z);
int l=, r=m, ans=-;
while (l<=r)
{
int mid=(l+r)>>;
if (check(x,y,mid)>=z) ans=mid,r=mid-;
else l=mid+;
}
printf("%d\n",ans);
}
}

E - Candy Piles

是个博弈论好像……不会博弈论先坑着

F - Leftmost Ball

题意:

有n种颜色的球,每种m个。现在把这n*m个球排成一排,然后把每种颜色的最左边的球染成第n+1种颜色。求最终的颜色序列有多少种,对1000000007取模。

题解:

感觉是一道思维难度很高的题目……不过看了题解就发现这个思路并不难懂……

QAQ这个模型转化可以说是非常巧妙了 我怎么什么都不会啊……我怎么这么菜啊……

 #include<iostream>
#include<cstdio>
#define N (5000000+1000)
#define MOD (1000000007)
using namespace std; long long n,k,fac[N],inv[N],facinv[N],f[][]; void Init()
{
fac[]=inv[]=facinv[]=;
for (int i=; i<=; ++i)
{
if (i!=) inv[i]=(MOD-MOD/i)*inv[MOD%i]%MOD;
fac[i]=fac[i-]*i%MOD;
facinv[i]=facinv[i-]*inv[i]%MOD;
}
} long long C(long long n,long long m)
{
if (m>n) return ;
return fac[n]*facinv[m]%MOD*facinv[n-m]%MOD;
} int main()
{
Init();
scanf("%lld%lld",&n,&k);
if (k==){printf(""); return ;}
f[][]=;
for (int i=; i<=n; ++i)
f[][i]=f[][i-]*C(i*(k-)-,k-)%MOD;
for (int i=; i<=n; ++i)
for (int j=i; j<=n; ++j)
f[i][j]=(f[i-][j]+f[i][j-]*C(i+j*(k-)-,k-)%MOD)%MOD;
printf("%lld",f[n][n]*fac[n]%MOD);
}

AGC 003

C - BBuBBBlesort!

题意:

有两种操作,第一种是将相邻的两个元素反转,第二种是将相邻的三个元素反转,尽可能多的使用第二种操作将序列排序,输出最小的第一种操作的使用次数。

题解:

可以发现二操作的本质就是随意交换下标奇偶性相同的位置。那么只需要统计一个数到自己应该到的位置的距离为奇数的个数最后除二就好了。

 #include<iostream>
#include<cstdlib>
#include<cstdio>
#include<map>
#include<algorithm>
#define N (100000+1000)
using namespace std; int n,a[N],b[N],ans;
map<int,int>pos; int main()
{
scanf("%d",&n);
for (int i=; i<=n; ++i)
scanf("%d",&a[i]),b[i]=a[i];
sort(b+,b+n+);
for (int i=; i<=n; ++i) pos[b[i]]=i;
for (int i=; i<=n; ++i)
if (abs(i-pos[a[i]])%)
ans++;
printf("%d",ans>>);
}

D - Anticube

题意:

给你一个序列,让你尽可能选择多的数,让被选择的数两两间的乘积不为立方数。

题解:

感觉最近降智有点严重啊……

首先很容易想到的是,对于一个数,我们可以将其质因数分解然后将所有质因子的质数模3。

假设在指数模3下,一个数是$p_1^2*p_2^1*p_3^2$,设它的补数为$p_1^1*p_2^2*p_3^1$,显然一个数只有与其唯一的补数相乘才可以得到立方数。所以重点是如何将一个数质因子指数取模后的数及其补数求出来,最后答案只需要统计一个数和其补数的出现次数的max就好了。

显然若一个数的某个立方质因子的指数>=3,那么这个立方质因子的范围一定小于$10^{\frac{10}{3}}$,直接化简即可。至于一个数的补数,可以用$10^{\frac{10}{3}}$以内的素数去分解这个数,分解完后剩下的质因子个数肯定小于等于2,判断一下是否是完全平方数就好了。

 #include<iostream>
#include<cstdio>
#include<cmath>
#include<map>
#define N (100000+1000)
using namespace std; long long vis[N],prime[N],cnt,n,x,a[N],b[N];
map<long long,long long>Map; void Init(int n)
{
for (int i=; i<=n; ++i)
{
if (!vis[i]) prime[++cnt]=i;
for (int j=; j<=cnt && prime[j]*i<=n; ++j)
{
vis[prime[j]*i]=true;
if (i%prime[j]==) break;
}
}
} long long sqr(long long x){return x*x;} int main()
{
Init();
scanf("%lld",&n);
for (int i=; i<=n; ++i)
{
scanf("%lld",&x);
for (int j=; j<=cnt; ++j)
while (x%(prime[j]*prime[j]*prime[j])==)
x/=prime[j]*prime[j]*prime[j];
Map[x]++; a[i]=x;
long long y=;
for (int j=; j<=cnt; ++j)
if (x%prime[j]==)
{
y*=x%(prime[j]*prime[j])==?prime[j]:prime[j]*prime[j];
while (x%prime[j]==) x/=prime[j];
}
if (sqr((long long)sqrt(x))==x) y*=(long long)sqrt(x);
else y*=x*x;
b[i]=y;
}
long long ans=;
if (Map[]) ans++; Map[]=;
for (int i=; i<=n; ++i)
{
ans+=max(Map[a[i]],Map[b[i]]);
Map[a[i]]=Map[b[i]]=;
}
printf("%lld",ans);
}

E - Sequential operations on Sequence

题意:

有一个数字串S,初始长度为n,是1 2 3 4 …… n。
有m次操作,每次操作给你一个正整数a[i],你先把S无穷重复,然后把前a[i]截取出来成为新的S。
求m次操作后,每个数字在S中出现的次数。

题解:

心态崩了

发现如果$a_i>=a_{i+1}$的话那么$a_i$显然是无效的。所以可以把m次操作处理一下成为一个递增序列。之前看的题解大小于号写反了让我懵逼了半天……

设m个操作处理后剩下了cnt个,设f[i]表示第i次操作后形成的序列在最终序列中出现了几次,显然f[cnt]=1

设初始长度为3,有两个操作7,15(如下图)。为了方便计算,将3也算入一个操作。

AtCoder Grand Contest-LMLPHP

可以发现,长度变成15后的结果=长度为7时候的结果*2+余下的一点边角料。而长为x边角料显然与总序列前x是相等的。(如下图)

AtCoder Grand Contest-LMLPHP

当边角料大于最小的操作的时候,显然是可以不停拆分的。就这样不停拆分直到边角料小于最小的操作,这样的话就可以直接将其加入对最终答案的贡献了。 实在不理解可以举几个例子对着代码模拟,然后感性理解一下反正我就是这么干的

 #include<iostream>
#include<cstdio>
#include<algorithm>
#define N (100000+1000)
using namespace std; long long n,q,x,a[N],cnt,ans[N],f[N]; int main()
{
scanf("%lld%lld",&n,&q);
a[++cnt]=n;
for (int i=; i<=q; ++i)
{
scanf("%lld",&x);
while (cnt && x<=a[cnt]) --cnt;
a[++cnt]=x;
}
f[cnt]=;
for (int i=cnt; i>=; --i)
{
long long k=a[i];
while (k>a[])
{
long long t=lower_bound(a+,a+cnt+,k)-a-;
f[t]+=f[i]*(k/a[t]);
k%=a[t];
}
ans[k]+=f[i];
}
for (int i=n; i>=; --i) ans[i]+=ans[i+];
for (int i=; i<=n; ++i) printf("%lld\n",ans[i]);
}

F - Fraction of Fractal

题意:

屠龙宝刀点击就送

题解:

我 不会 抄别人的题解

不过思路还是很妙的

 #include<iostream>
#include<cstring>
#include<cstdio>
#define N (1000+100)
#define MOD (1000000007)
using namespace std; long long n=,H,W,K,a[N][N],S,A,B;
char st[N]; struct Matrix
{
long long m[][];
void clear(){memset(m,,sizeof(m));}
}V,G;
Matrix operator * (Matrix a,Matrix b)
{
Matrix ans; ans.clear();
for (int i=; i<=n; ++i)
for (int j=; j<=n; ++j)
for (int k=; k<=n; ++k)
(ans.m[i][j]+=a.m[i][k]*b.m[k][j])%=MOD;
return ans;
} Matrix Mat_Qpow(Matrix a,long long p)
{
Matrix ans; ans.clear();
for (int i=; i<=n; ++i) ans.m[i][i]=;
while (p)
{
if (p&) ans=ans*a;
a=a*a; p>>=;
}
return ans;
} long long Qpow(long long a,long long p)
{
long long ans=,base=a;
while (p)
{
if (p&) ans=ans*base%MOD;
base=base*base%MOD; p>>=;
}
return ans;
} int Get()
{
int ansLR=,ansUD=;//左右,上下接口的个数
int anslr=,ansud=;//左右,上下相邻为黑色的位置个数
for (int i=; i<=H-; ++i)
for (int j=; j<=W; ++j)
ansud+=(a[i][j]==a[i+][j] && a[i][j]==);
for (int i=; i<=H; ++i)
for (int j=; j<=W-; ++j)
anslr+=(a[i][j]==a[i][j+] && a[i][j]==); for (int i=; i<=H; ++i)
ansLR+=(a[i][]==a[i][W] && a[i][]==);
for (int i=; i<=W; ++i)
ansUD+=(a[][i]==a[H][i] && a[][i]==); if (ansLR && ansUD) return ;
if (!ansLR && !ansUD) return ;
if (ansLR) A=anslr,B=ansLR;
else A=ansud,B=ansUD;
return ;
} void Solve()
{
G.m[][]=A; G.m[][]=B;
V.m[][]=S; V.m[][]=; V.m[][]=A; V.m[][]=B;
V=Mat_Qpow(V,K-); G=G*V;
long long ans=Qpow(S,K-)-G.m[][];
printf("%lld",ans<=?ans+MOD:ans);
} int main()
{
scanf("%lld%lld%lld",&H,&W,&K);
for (int i=; i<=H; ++i)
{
scanf("%s",st);
for (int j=; j<=W; ++j)
a[i][j]=st[j-]=='#'?:,S+=a[i][j];
}
if (K<=) {printf(""); return ;} int g=Get();
if (g==) {printf("%lld",Qpow(S,K-)); return ;}
if (g==) {printf(""); return ;} Solve();
}

AGC 004

C - AND Grid

题意:

给你一个网格图,有一些地方涂了色。构造两个涂色后的四连通的网格图,让他们涂色的方格的并集等于给定网格图。给定的网格图第一行,第一列,第n行,第m列一定没有涂色。

题解:

设给定网格图为s,构造的为a和b。初始值要设成a=s且b=s。然后a涂第一行,b涂最后一行。最后a涂奇数列,b涂偶数列。正确性显然。

 #include<iostream>
#include<cstring>
#include<cstdio>
#define N (601)
using namespace std;
int n,m;
char s[N][N],a[N][N],b[N][N];
int main()
{
scanf("%d%d",&n,&m);
for (int i=; i<=n; ++i)
scanf("%s",s[i]+);
memcpy(a,s,sizeof(s)); memcpy(b,s,sizeof(s));
for (int i=; i<=m; ++i)
a[][i]=b[n][i]='#';
for (int j=; j<m; ++j)
for (int i=; i<n; ++i)
if (j%) a[i][j]='#';
else b[i][j]='#';
for (int i=; i<=n; ++i)
{
for (int j=; j<=m; ++j)
printf("%c",a[i][j]);
printf("\n");
}
printf("\n");
for (int i=; i<=n; ++i)
{
for (int j=; j<=m; ++j)
printf("%c",b[i][j]);
printf("\n");
}
}

D - Teleporter

题意:

一个n个点的基环树(环套树),更改尽量少的出边,使得任意点在走k步后都正好到达1节点。

题解:

一开始sb了从上往下贪结果只能过三分之二……首先很容易发现1的出边必须连向自己,否则一定无法满足条件,这样就形成了一棵树。接下来只需要从下往上贪心,若一个点最长能往下延伸k-1条边,就将这个点断掉连向1节点。

 #include<iostream>
#include<cstdio>
#define N (100000+1000)
using namespace std; struct Edge{int to,next;}edge[N<<];
int n,k,a[N],head[N],num_edge,ans; void add(int u,int v)
{
edge[++num_edge].to=v;
edge[num_edge].next=head[u];
head[u]=num_edge;
} int Dfs(int x)
{
int maxn=;
for (int i=head[x]; i; i=edge[i].next)
maxn=max(maxn,Dfs(edge[i].to)+);
if (maxn==k- && x!= && a[x]!=){maxn=-; ans++;}
return maxn;
} int main()
{
scanf("%d%d%d",&n,&k,&a[]);
for (int i=; i<=n; ++i)
scanf("%d",&a[i]),add(a[i],i);
Dfs();
printf("%d",ans+(a[]!=));
}

E - Salvage Robots

题意:

一个n*m的网格,上面有一些机器人,还有一个出口。每次可以将所有机器人向一个方向一起移动一格,机器人出界就挂了。问最多多少个机器人能逃离。

题解:

我再也不做边界处理这么恶心的题了……

可以换一种想法,把移动机器人转换成移动出口。设f[i][j][k][l]表示取完了(i,j)(k,l)这个矩形内的机器人,然后分别向四周扩展即可。开short不会MLE。

 #include<iostream>
#include<cstdio>
#define N (100+5)
using namespace std; short n,m,line[N][N],list[N][N],f[N][N][N][N],sx,sy,ans;
char a[N][N]; int main()
{
cin>>n>>m;
for (short i=; i<=n; ++i)
scanf("%s",a[i]+);
for (short i=; i<=n; ++i)
for (short j=; j<=m; ++j)
{
line[i][j]=line[i][j-]+(a[i][j]=='o');
list[i][j]=list[i-][j]+(a[i][j]=='o');
if (a[i][j]=='E') sx=i,sy=j;
}
for (short i=sx; i>=; --i)
for (short j=sy; j>=; --j)
for (short k=sx; k<=n; ++k)
for (short l=sy; l<=m; ++l)
{
if (i> && k-sx<i-)
f[i-][j][k][l]=max((int)f[i-][j][k][l],f[i][j][k][l]+line[i-][min((int)l,m-sy+j)]-line[i-][max(j-,l-sy)]);
if (k<n && sx+k<n+i)
f[i][j][k+][l]=max((int)f[i][j][k+][l],f[i][j][k][l]+line[k+][min((int)l,m-sy+j)]-line[k+][max(j-,l-sy)]);
if (j> && l-sy<j-)
f[i][j-][k][l]=max((int)f[i][j-][k][l],f[i][j][k][l]+list[min((int)k,n-sx+i)][j-]-list[max(i-,k-sx)][j-]);
if (l<m && sy+l<m+j)
f[i][j][k][l+]=max((int)f[i][j][k][l+],f[i][j][k][l]+list[min((int)k,n-sx+i)][l+]-list[max(i-,k-sx)][l+]);
short now=max(max(f[i-][j][k][l],f[i][j][k+][l]),max(f[i][j-][k][l],f[i][j][k][l+]));
ans=max(ans,now);
}
cout<<ans;
}

F - Namori

题意:

给你一颗全白的树或环套树,每次可以选择一条连接两个同色点的边,将两个端点反色。 问变成全黑的最小步数,可能无解。

题解:

这个题好神啊……说白了就是我不会

树的做法
首先要知道,树是一个二分图。
不妨设深度为奇数(根的深度为1)的点放着一枚硬币,深度为偶数的点是空位。这样就将树染成了一个二分图
可以发现,一次操作相当于将一个硬币运到相邻的空位。
当初始硬币处都是空位,初始空位处都是硬币的时候,对应着原图全被染成黑色。
显然只有硬币数=空位数才有解。
设硬币权值为1,空位权值为-1,1为树的根。
size对应子树和,当size[1]=0的时候就对应着有解。
因为size[i]是一定要从i这个子树运出或运入的硬币数,这个值代表边(i,fa[i])被操作的次数。
$\sum_{i=1}^nabs(size_i)$显然是答案下界。我们尽可能让子树内配对就可以达到这个答案下界了。
奇环的做法
任意断开环上一条边(x,y),让基环树变成一棵树。
x点和y点一定属于二分图同一侧(要么都是空位,要么都是硬币)。
操作一次边(x,y),就是在x点和y点各增加或减少一枚硬币。
计算出变成树后多余或者缺少的硬币数量,是奇数就无解,否则就用操作(x,y)边让硬币数=空缺数。
同时修改x和y的祖先的size值。因为操作边(x,y)平衡硬币数和空位数的时候,他们的祖先对应的操作次数也会改变。
最后按照树的做法做一遍就可以了。

偶环的做法
任意断开一边(x,y),设(x,y)这条边被操作了k次(可以为负数),那么x上增加了k个硬币,y上减少了k个。
这k次操作显然只影响路径x-lca(x,y)-y的操作数(也就是size)。
为了最小化答案,显然是对这个路径上每一个位置的size加上或减去一个数,最小化这条路径上点的size和。
这个加上/减去的数显然就是路径上所有size的中位数。

 #include<iostream>
#include<cstdlib>
#include<cstdio>
#include<algorithm>
#define N (200000+1000)
using namespace std; struct Edge{int to,next;}edge[N<<];
int n,m,u,v,K[N],size[N],sum,cir_num;
int Depth[N],Father[N],stx,sty;
int head[N],num_edge,Q[N],tot; void add(int u,int v)
{
edge[++num_edge].to=v;
edge[num_edge].next=head[u];
head[u]=num_edge;
} void Dfs(int x,int fa)
{
Depth[x]=Depth[fa]+;
Father[x]=fa;
size[x]=(Depth[x]%==)?:-;
for (int i=head[x]; i; i=edge[i].next)
if (edge[i].to!=fa)
{
if (Depth[edge[i].to])
{
if (cir_num) continue;
stx=x; sty=edge[i].to;
cir_num=Depth[x]-Depth[edge[i].to]+;
continue;
}
Dfs(edge[i].to,x);
size[x]+=size[edge[i].to];
}
} int LCA(int x,int y)
{
while (Depth[x]>Depth[y]) x=Father[x];
while (Depth[y]>Depth[x]) y=Father[y];
while (x!=y) x=Father[x], y=Father[y];
return x;
} int main()
{
scanf("%d%d",&n,&m);
for (int i=; i<=m; ++i)
{
scanf("%d%d",&u,&v);
add(u,v); add(v,u);
}
Dfs(,); if (m==n-)//tree
{
if (size[]!=) printf("-1");
else
{
long long ans=;
for (int i=; i<=n; ++i) ans+=abs(size[i]);
printf("%lld",ans);
}
}
else if (cir_num%==)//odd
{
if (abs(size[])%==) printf("-1");
else
{
int sz=size[];
long long ans=abs(sz)/;
for (int u=stx; u; u=Father[u]) size[u]-=sz/;
for (int u=sty; u; u=Father[u]) size[u]-=sz/;
for (int i=; i<=n; ++i) ans+=abs(size[i]);
printf("%lld",ans);
}
}
else//even
{
if (size[]!=) printf("-1");
else
{
int lca=LCA(stx,sty);
for (int u=stx; u!=lca; u=Father[u])
Q[++tot]=size[u],Depth[u]=;
for (int u=sty; u!=lca; u=Father[u])
Q[++tot]=-size[u],Depth[u]=;
Q[++tot]=;
sort(Q+,Q+tot+);
int sz=Q[tot/]; long long ans=;
for (int i=; i<=n; ++i) if (Depth[i]) ans+=abs(size[i]);
for (int i=; i<=tot; ++i) ans+=abs(Q[i]-sz);
printf("%lld",ans);
}
}
}

AGC 005

C - Tree Restoring

题意:

给你每个点到离它最远的点的距离,问是否可以构造出这样的一颗树。

题解:

首先先把直径搞出来,看看能否满足直径,不能直接impossible。如果能满足直径的话,就判断一下剩下的点往直径上连边是否可行。(代码应该比较好懂)

 #include<algorithm>
#include<cstdio>
#define N (1001)
using namespace std; int n,a[N],Keg[N],maxn; int main()
{
scanf("%d",&n);
for (int i=; i<=n; ++i)
{
scanf("%d",&a[i]);
Keg[a[i]]++; maxn=max(maxn,a[i]);
}
for (int i=(maxn+)/; i<=maxn; ++i)
{
if (maxn%== && i==(maxn+)/) Keg[i]-=;
else Keg[i]-=;//从桶里取点往直径上放。
if (Keg[i]<){puts("Impossible"); return ;}//桶里没点可放就无解
}
for (int i=; i<=(maxn+)/; ++i)//显然若还存在最远距离在这个范围内的点,这个点是无法连到直径上的
if (Keg[i]) {puts("Impossible"); return ;}
puts("Possible");
}

D - ~K Perm Counting

题意:

给定n和k,a是n的一个排列。求满足$abs(a_i-i)!=k$的n的排列的个数。

题解:

这个题用到的算法都是基础难度但的确很妙啊_(:з」∠)_

首先令$g_i$表示有i个位置不合法的排列总数,简单容斥一下可以发现

$ans=n!+\sum_{i=0}^ng_i*(n-i)!*(-1)^i$。那么现在的问题在于怎么求$g_i$。

考虑一个n=6, k=2的情况,可以建立一个二分图,若i位置不能放数字x,就左边的i连右边的x。(如下)

AtCoder Grand Contest-LMLPHP

发现这个二分图是由若干条链构成的?!

我们可以把这些链都提取出来,任意顺序摆成一条链的形状。

AtCoder Grand Contest-LMLPHP

很显然我们选择一条红边就相当于选了一个不合法的情况,且不能连续选择两个连续的红边。

所以可以设f[i][j][0/1]表示前i个点选了j条红边,且第i个点不选/选红边。

DP的时候特判一下没有红边的地方即可。最后g[i]=f[2*n][i][0]+f[2*n][i][1]

 #include<iostream>
#include<cstdio>
#define N (4009)
#define MOD (924844033)
using namespace std; long long n,k,vis[N],tot,g[N],f[N][N][],fac[N]; int main()
{
scanf("%lld%lld",&n,&k);
for (int t=; t<=; ++t)
for (int i=; i<=k; ++i)
for (int j=i; j<=n; j+=k)
{
tot++;
if (j!=i) vis[tot]=true;
}
f[][][]=;
for (int i=; i<=*n; ++i)
for (int j=; j<=n; ++j)
{
(f[i][j][]+=f[i-][j][]+f[i-][j][])%=MOD;
if (vis[i]) (f[i][j][]+=f[i-][j-][])%=MOD;
}
for (int i=; i<=n; ++i)
g[i]=(f[*n][i][]+f[*n][i][])%MOD; fac[]=;
for (int i=; i<=n; ++i) fac[i]=fac[i-]*i%MOD;
long long ans=fac[N];
for (int i=,j=; i<=n; ++i,j=-j)
(ans+=j*g[i]*fac[n-i]%MOD)%=MOD;
printf("%lld\n",(ans+MOD)%MOD);
}

E - Sugigma: The Showdown

题意:
一个图有红边和蓝边,红边和蓝边各自构成一颗树。A走红边,B走蓝边。

两人相遇游戏结束,A想最大化时间,B想最小化时间。若B永远抓不到A输出-1

题解:

先将蓝树以B的起点为根建立出来,可以发现A在蓝树上的运动轨迹:要么往子树走,要么往兄弟走,要么往父亲走。那么B只需要一步一步往下沉底去抓A就可以了。

什么时候无解呢?当一条红边连接的两个点,在蓝树上距离超过2,且A能到达两点之一,那么A就可以通过反复横跳来让B永远抓不到。(这个地方的check函数写法我觉得挺巧妙的……)

否则就让A尽可能往蓝树的更深的点走,到一个最深的地方弃疗等B来抓它就好了。

 #include<iostream>
#include<cstring>
#include<cstdlib>
#include<cstdio>
#define N (400000+1000)
using namespace std; struct edge{int to,next;}edge[N<<];
int Father[N],Depth[N],Dfn[N],Low[N];
int n,x,y,S1,S2,cnt,ans,u[N],v[N];
int head[N],num_edge; void add(int u,int v)
{
edge[++num_edge].to=v;
edge[num_edge].next=head[u];
head[u]=num_edge;
} bool check(int x,int y)
{
if (Dfn[x]>Dfn[y]) swap(x,y);
if (Low[x]>=Low[y])
return (Depth[y]-Depth[x])>;
return Father[x]!=Father[y];
} void Dfs1(int x)
{
Dfn[x]=++cnt; Depth[x]=Depth[Father[x]]+;
for (int i=head[x]; i; i=edge[i].next)
if (edge[i].to!=Father[x])
{
Father[edge[i].to]=x;
Dfs1(edge[i].to);
}
Low[x]=++cnt;
} void Dfs2(int x,int fa,int k)
{
ans=max(ans,Depth[x]-);
if (Depth[x]<=k) return;
for (int i=head[x]; i; i=edge[i].next)
if (edge[i].to!=fa)
{
if (check(x,edge[i].to)) puts("-1"),exit();
Dfs2(edge[i].to,x,k+);
}
} int main()
{
scanf("%d%d%d",&n,&S1,&S2);
for (int i=; i<=n-; ++i)
scanf("%d%d",&u[i],&v[i]);
for (int i=; i<=n-; ++i)
scanf("%d%d",&x,&y),add(x,y),add(y,x);
Dfs1(S2);
memset(head,,sizeof(head)); num_edge=;
for (int i=; i<=n-; ++i) add(u[i],v[i]),add(v[i],u[i]);
Dfs2(S1,-,);
printf("%d\n",ans<<);
}

F - Many Easy Problems

题意:

给你一颗n个点的树和一个整数k。设S为树上某些点的集合,定义f(S)为包含S的最小的联通子图大小。

n个点选k个点有$C_n^k$种方案,求所有方案的f(S)的和。

题解:

没仔细读题一开始质数设的998244353……然而应该是924844033。第一个原根是3,第二个原根是5……

如果直接对于每个k直接求解答案好像不可做……就分开考虑每个点对答案的贡献。

容斥一下可以得到i点对于一个k的答案的贡献为$C_n^k-\sum_{fa[j]=i}C_{size[j]}^k-C_{n-size[i]}^k$

显然每个子树只会对答案贡献一次,所以最终答案为

$ans[k]=n*C_{n}^{k}-\sum_{i=0}^{n}cnt[i]*C_{i}^{k}$,其中cnt[i]表示大小为i的子树个数。

将组合数拆开后发现是$\frac{i!}{(i-k)!k!}$,将其中的$i!$和cnt[i]放到一起,$k!$提到最前面,就成了

$n*C_{n}^{k}-k!*\sum_{i=0}^{n}cnt[i]*i!*\frac{1}{(i-k)!}$,发现那个sigma可以通过翻转数组的套路卷积求得。

 #include<iostream>
#include<cstdio>
#include<algorithm>
#define N (800000+1000)
#define MOD (924844033)
#define LL long long
using namespace std; struct Edge{int to,next;}edge[N];
LL n,u,v,fn=,l,r[N],a[N],b[N];
LL fac[N],inv[N],facinv[N],ans[N];
LL head[N],num_edge,size[N],cnt[N]; void add(int u,int v)
{
edge[++num_edge].to=v;
edge[num_edge].next=head[u];
head[u]=num_edge;
} LL Qpow(LL a,LL b)
{
LL ans=,base=a;
while (b)
{
if (b&) ans=ans*base%MOD;
base=base*base%MOD; b>>=;
}
return ans;
} void NTT(LL n,LL *a,LL opt)
{
for (int i=; i<n; ++i)
if (i<r[i]) swap(a[i],a[r[i]]);
for (int k=; k<n; k<<=)
{
LL wn=Qpow(,(MOD-)/(k<<));
for (int i=; i<n; i+=(k<<))
{
LL w=;
for (int j=; j<k; ++j, w=w*wn%MOD)
{
LL x=a[i+j], y=w*a[i+j+k]%MOD;
a[i+j]=(x+y)%MOD; a[i+j+k]=(x-y+MOD)%MOD;
}
}
}
if (opt==-)
{
reverse(a+,a+n);
LL g=Qpow(n,MOD-);
for (int i=; i<n; ++i) a[i]=a[i]*g%MOD;
}
} void Init()
{
fac[]=; inv[]=; facinv[]=;
for (int i=; i<=n; ++i)
{
if (i!=) inv[i]=(MOD-MOD/i)*inv[MOD%i]%MOD;
fac[i]=fac[i-]*i%MOD; facinv[i]=facinv[i-]*inv[i]%MOD;
}
} LL C(LL n,LL m)
{
if (n<m) return ;
return fac[n]*facinv[m]%MOD*facinv[n-m]%MOD;
} void Dfs(int x,int fa)
{
size[x]=;
for (int i=head[x]; i; i=edge[i].next)
if (edge[i].to!=fa)
{
Dfs(edge[i].to,x);
cnt[size[edge[i].to]]++;
size[x]+=size[edge[i].to];
}
cnt[n-size[x]]++;
} int main()
{
scanf("%lld",&n);
for (int i=; i<=n-; ++i)
{
scanf("%lld%lld",&u,&v);
add(u,v); add(v,u);
}
Init(); Dfs(,-);
while (fn<=n+n) fn<<=, l++;
for (int i=; i<=fn; ++i)
r[i]=(r[i>>]>>)|((i&)<<(l-)); for (int i=; i<=n; ++i) a[i]=cnt[i]*fac[i]%MOD;
for (int i=; i<=n; ++i) b[i]=facinv[n-i]; NTT(fn,a,); NTT(fn,b,);
for (int i=; i<=fn; ++i) a[i]=a[i]*b[i]%MOD;
NTT(fn,a,-); for (int i=; i<=n; ++i) ans[i]=-facinv[i]*a[n+i]%MOD+MOD;
for (int i=; i<=n; ++i) ans[i]=(ans[i]+n*C(n,i))%MOD;
for (int i=; i<=n; ++i) printf("%lld\n",ans[i]);
}

AGC 006

C - Rabbit Exercise

题意:

数轴上有n只兔子,第i只兔子的坐标为xi。 有一组操作,这组操作的第i个操作是要让第ai只兔子等概率的跳到自己关于第ai+1或第ai-1只兔子的对称点。 进行K组操作,求每只兔子最后坐标的期望值。

题解:

操作编号为x的时候期望结果为$a_x=\frac{1}{2}(2a_{x-1}-a_x)+\frac{1}{2}(2a_{x+1}-a_x)=a_{x-1}+a_{x+1}-a_x$。

感性理解一下又由于期望的线性发现可以直接用期望到达的点位置替换当前点位置。

对原数组$a_1,a_2,a_3$做一个差分,就成了$a_1-a_0,a_2-a_1,a_3-a_2$。

对原数组中间的求期望,结果是$a_1,a_1+a_3-a_2$,对应到差分数组上就是$a_1-a_0,a_3-a_2,a_2-a_1$。

可以发现操作i位置的时候就相当于swap差分数组的i和i+1位置。

然后这里有一个非常巧妙的方法,就是用类似快速幂的倍增思想。

可以用一个点跳一组后的位置求跳两组后的位置……然后将k二进制分解就好了。感觉看代码可能好懂一点

 #include<iostream>
#include<cstdio>
#define N (100000+1000)
using namespace std; long long n,m,k,x,a[N],d[N],p[N],ans[N],tmp[N],sum; void Qpow()
{
for (int i=; i<=n; ++i) ans[i]=i;
while (k)
{
if (k&)
{
for (int i=; i<=n; ++i) tmp[i]=ans[p[i]];
for (int i=; i<=n; ++i) ans[i]=tmp[i];
}
for (int i=; i<=n; ++i) tmp[i]=p[p[i]];
for (int i=; i<=n; ++i) p[i]=tmp[i];
k>>=;
}
for (int i=; i<=n; ++i) tmp[i]=d[ans[i]];
for (int i=; i<=n; ++i) d[i]=tmp[i];
} int main()
{
scanf("%lld",&n);
for (int i=; i<=n; ++i)
scanf("%lld",&a[i]),d[i]=a[i]-a[i-],p[i]=i;
scanf("%lld%lld",&m,&k);
for (int i=; i<=m; ++i)
scanf("%lld",&x),swap(p[x],p[x+]);
Qpow();
for (int i=; i<=n; ++i)
sum+=d[i],printf("%lld\n",sum);
}

D - Median Pyramid Hard

题意:

AtCoder Grand Contest-LMLPHP

初始给你一个左边这样的塔,从下往上填。一个空位置的取值等于他下面三个格子的中位数,问塔顶的数是多少。

题解:

首先二分塔顶的数,把最底层大于的转化为1,小于等于的转化为0。

AtCoder Grand Contest-LMLPHP

画个图可以发现底层连续的0或者1一定是能延续到达塔顶的。如果两者只存在一个那好说,存在啥塔顶就是啥。

如果连续的0和连续的1都存在,就看谁离中间更近,近的那个能够占领塔顶。

注意判断没有连续0和连续的1的情况。

 #include<iostream>
#include<cstdio>
#define N (200000+1000)
using namespace std; int n,a[N],l,r,ans,mid;
int g(int x){return x>mid;} int check(int x)
{
for (int i=; i<=n-; ++i)
{
if (g(a[n-i+])==g(a[n-i])) return g(a[n-i]);
if (g(a[n+i-])==g(a[n+i])) return g(a[n+i]);
}
return g(a[]);
} int main()
{
scanf("%d",&n);
for (int i=; i<=*n-; ++i)
scanf("%d",&a[i]);
l=; r=*n-; ans=-;
while (l<=r)
{
mid=(l+r)>>;
if (!check(mid)) ans=mid,r=mid-;
else l=mid+;
}
printf("%d\n",ans);
}

E - Rotate 3x3

题意:

给你一个3*n的网格图,网格内乱序有一些数。有一个操作是选择一个3*3的矩形将其旋转180°。问是否能将其操作成为第一列为1,2,3,第二列为4,5,6……的网格。

题解:

对这种奇怪的结论题不是很感冒……感觉这篇题解写的挺清楚的……不过感觉复杂度有点假……也不知道是我感性理解错了还是难造数据……

 #include<iostream>
#include<cstdlib>
#include<cstdio>
#define N (100000+1000)
using namespace std; int n,a[][N],tar[N],R[]; int main()
{
scanf("%d",&n);
for (int i=; i<=; ++i)
for (int j=; j<=n; ++j)
scanf("%d",&a[i][j]);
for (int i=; i<=n; ++i)
{
tar[i]=(a[][i]+)/;
if (abs(a[][i]-a[][i])!= || abs(a[][i]-a[][i])!= || abs(i-tar[i])%)
{
puts("No");
return ;
}
R[i&]^=(a[][i]>a[][i]);
}
for (int i=; i<=n; ++i)
while (tar[i]!=i)
R[i&^]^=,swap(tar[i],tar[tar[i]]);
if (R[] || R[]) puts("No");
else puts("Yes");
}

F - Blackout

题意:

给你一个n*n的网格,初始给你m个点让你把他们染黑,同时若点(x,y)和(y,z)为黑色,则(z,x)也被染成黑色。问最后有多少个黑色的格子。

题解:

将模型转化到图上,题目就成了若存在边x->y, y->z,则会产生一条z->x的边。这显然是一个三元环。

我们可以发现两个弱连通分量之间肯定不会产生边,所以可以对于每个弱连通分量单独考虑。

对于当前弱连通分量,我们可以进行三染色染成0,1,2。若染色过程中:

1、颜色没有全用到。那么一个点最多只能往后走一步,肯定走不出来一个三元环,所以这个弱连通分量对答案的贡献是这个分量的边数。

2、成功染色。任意0点可以连任意1点,任意1点可以连任意2点,任意2点可以连任意0点。

3、染色失败,即一个点可能存在多种颜色。这样的话图中肯定是存在环的(自环也考虑)。画画图可以发现这个图可以被连成一个完全图,两两之间任意连边即可。

 #include<iostream>
#include<cstring>
#include<cstdio>
#define N (100000+1000)
using namespace std; struct Edge{int to,next,len;}edge[N<<];
int head[N],num_edge;
long long n,m,cnt[],flag,col[N];
long long ans,sum,u,v; void add(int u,int v,int l)
{
edge[++num_edge].to=v;
edge[num_edge].next=head[u];
edge[num_edge].len=l;
head[u]=num_edge;
} void Dfs(int x)
{
cnt[col[x]]++;
for (int i=head[x]; i; i=edge[i].next)
{
if (edge[i].len==) sum++;
if (col[edge[i].to]==-)
{
col[edge[i].to]=(col[x]+edge[i].len)%;
Dfs(edge[i].to);
}
else if (col[edge[i].to]!=(col[x]+edge[i].len)%)
flag=;
}
} int main()
{
scanf("%lld%lld",&n,&m);
for (int i=; i<=m; ++i)
{
scanf("%lld%lld",&u,&v);
add(u,v,); add(v,u,);
}
memset(col,-,sizeof(col));
for (int i=; i<=n; ++i)
if (col[i]==-)
{
sum=cnt[]=cnt[]=cnt[]=flag=;
col[i]=; Dfs(i);
if (flag)
{
long long t=cnt[]+cnt[]+cnt[];
ans+=t*t; continue;
}
if ((!cnt[]) || (!cnt[]) || (!cnt[]))
{
ans+=sum;
continue;
}
ans+=cnt[]*cnt[]+cnt[]*cnt[]+cnt[]*cnt[];
}
printf("%lld\n",ans);
}

AGC 007

C - Pushing Balls

题意:

平面上有n+1个洞,洞两两之间有一个球,球洞之间的距离是一个等差数列。每次随机选择一个不在洞中的球,再随机选择一个方向推动,球将会进入这个方向上距离它最近的没有球的洞。滚动过程中若洞内有球则会滚过这个洞,求球都进洞后球滚动的期望总距离。

题解:

期望题 不会 抄题解

 #include<cstdio>
double n,d,x,a;
int main()
{
scanf("%lf%lf%lf",&n,&d,&x);
for (d+=(n-0.5)*x; n; --n)a+=d,d+=d/n;
printf("%.10f",a);
}

D - Shik and Game

题意:

有一个在数轴上进行的游戏。最初,玩家位于位置0,拥有N颗糖果,出口在位置E。游戏中还有N只熊。第i熊位于$x_i$。玩家的移动速度是1,而熊不移动。
当玩家给熊一个糖果时,熊会在单位时间T后提供一个硬币。更具体地说,如果第i只熊在时刻t得到一个糖果,它将在时刻t+T在它的位置放一个硬币。这个游戏的目的是给所有的熊糖果,拿起所有的硬币,然后去出口。注意,玩家只能给每只熊一个糖果。此外,每个熊只会产生一次硬币。如果玩家访问一个有硬币的位置,玩家可以捡起硬币。硬币不会消失,直到被玩家收集。
计算收集所有硬币到出口的最短时间。

题解:

挺有意思的一个题……比C简单多了

首先观察下数据范围和部分分,基本DP没跑了。显然最优情况一定是每次放置一段连续的糖果然后取走这一段产生的硬币。

那么$n^2$的DP就比较容易了,设f[i]表示取完前i个位置的硬币的最小花费时间,转移也显然。

$f[i]=min(f[i],f[j-1]+(a[i]-a[j])*3+(a[j]-a[j-1])+max(0,t-2*(a[i]-a[j])))$

这个转移表示连续在熊$[j,i]$放了糖果并取走了他们的硬币。行走路径为$j->i->j->i$。后面的max项是为了防止出现从$i->j$后j位置的熊硬币还没有产出的情况。

考虑如何优化呢?发现式子里面有和i相关的项有和j相关的项,$i$相关的项可以在计算$f[i]$的时候直接加进去,而$j$相关的项我们可以插入到线段树中,更新的时候直接区间查询最小值。

还有一个问题,因为DP式子里有一个max,所以没法直接插入到线段树中。但是可以发现,当计算$f[i]$的时候,前面的答案一定能分成两部分,前半部分1~?是离$a[i]$足够远所以计算的时候没有max的,后半部分?+1~i-1是离i太近而计算的时候考虑max的。而这个分界线又是一直向右挪的,所以用个指针记录一下,当一个位置离当前转移位置足够远的时候就把max的影响消除掉。(注释掉的是部分分代码)

 #include<iostream>
#include<cstdio>
#define N (100000+1000)
#define LL long long
using namespace std; LL n,e,t,f[N],a[N],Segt[N<<],p; void Update(LL now,LL l,LL r,LL x,LL v)
{
if (l==r) {Segt[now]+=v; return;}
LL mid=(l+r)>>;
if (x<=mid) Update(now<<,l,mid,x,v);
else Update(now<<|,mid+,r,x,v);
Segt[now]=min(Segt[now<<],Segt[now<<|]);
} LL Query(LL now,LL l,LL r,LL l1,LL r1)
{
if (l>r1 || r<l1) return 1e17;
if (l1<=l && r<=r1) return Segt[now];
LL mid=(l+r)>>;
return min(Query(now<<,l,mid,l1,r1),Query(now<<|,mid+,r,l1,r1));
} int main()
{
scanf("%lld%lld%lld",&n,&e,&t);
for (int i=; i<=n; ++i)
scanf("%lld",&a[i]);
for (int i=; i<=n; ++i)
{
while (p<=i- && *(a[i]-a[p])>=t)
Update(,,n,p,-*a[p]),p++;
LL temp=1e17;
temp=min(temp,Query(,,n,,p-));
if (p<=i-) temp=min(temp,Query(,,n,p,i-)+t-*a[i]);
f[i]=min(temp+*a[i],f[i-]+a[i]-a[i-]+t);
Update(,,n,i,-a[i-]+f[i-]);
}
printf("%lld\n",f[n]+e-a[n]);
}
/*
#include<iostream>
#include<cstdio>
using namespace std; long long n,e,t,f[10001],a[10001]; int main()
{
scanf("%lld%lld%lld",&n,&e,&t);
for (int i=1; i<=n; ++i)
scanf("%lld",&a[i]);
for (int i=1; i<=n; ++i)
{
f[i]=f[i-1]+(a[i]-a[i-1])+t;
for (int j=i-1; j>=1; --j)
f[i]=min(f[i],f[j-1]+(a[i]-a[j])*3+(a[j]-a[j-1])+max(1ll*0,t-2*(a[i]-a[j])));
}
printf("%lld\n",f[n]+e-a[n]);
}
*/

E - Shik and Travel

题意:

给定a full binary tree,确定一个叶子排序,最小化从叶子到叶子的路径的最大值。

题解:

心态崩了 我码力怎么越来越弱了啊

可以发现这么一个性质,当我们进入一颗子树u后,我们只有遍历完u的所有叶子才能出来。

对于每一个节点,我们存储所有的二元组$(a,b)_u$,表示可以从点u向下走距离a到达一个叶子,遍历完叶子节点后可以从结束的地方向上走b距离回到点u。

可以发现对于每一个点我们存储的二元组$(a,b)$必须是a单增b单减的,其他的都是无用状态。

考虑如何用点u的两个儿子s0,s1合并出u应该有的二元组呢?先二分一个答案mid,然后就成了判断可行性了。

假设s0有一个二元组$(a1,b1)$,s1有一个二元组$(a2,b2)$

显然新二元组走法为$a1->b1$  ,  $b1->a2$  ,  $a2->b2$,只要保证$b1+a2+dis(s0,u)+dis(s1,u)<=ans$,那么这两个二元组就可以合并成$(a1,b2)$

枚举子树大小小的那个儿子的二元组,在另一个儿子里二分找a2最大的且满足可以和b1合并的,因为a2尽量大那么b2就是尽量小的了。

感觉写代码的时候脑子有点乱写的非常鬼畜……

 #include<bits/stdc++.h>
#define N (140000)
#define LL long long
using namespace std; struct Node
{
LL a,b;
bool operator < (const Node A) const
{
if (a!=A.a) return a<A.a;
return b<A.b;
}
}s;
struct Edge{int to,next,len;}edge[N<<];
LL n,a[N],v[N],size[N],dis[N],l,r,ans,mid;
LL a1,b1,a2,b2,L,R,MID,ANS;
LL head[N],num_edge;
vector<Node>A[N],T; void add(LL u,LL v,LL l)
{
edge[++num_edge].to=v;
edge[num_edge].next=head[u];
edge[num_edge].len=l;
head[u]=num_edge;
} void Update(LL x,LL s0,LL s1)
{
if (size[s0]>size[s1]) swap(s0,s1);
LL len0=A[s0].size(), len1=A[s1].size(); T.clear();
for (int i=; i<len0; ++i)
{
a1=A[s0][i].a; b1=A[s0][i].b;
L=; R=len1-; ANS=-;
while (L<=R)
{
MID=(L+R)>>;
a2=A[s1][MID].a; b2=A[s1][MID].b;
if (b1+a2+dis[s0]+dis[s1]<=mid) ANS=MID,L=MID+;
else R=MID-; }
if (ANS==-) continue;
a2=A[s1][ANS].a; b2=A[s1][ANS].b;
s.a=a1+dis[s0]; s.b=b2+dis[s1]; T.push_back(s);
s.a=b2+dis[s1]; s.b=a1+dis[s0]; T.push_back(s);
} sort(T.begin(),T.end());
LL maxn=1ll*N*N, len=T.size();
for (int i=; i<len; ++i)
if (T[i].b<maxn)
A[x].push_back(T[i]),maxn=T[i].b;
} void Dfs(int x,int fa)
{
size[x]=;
LL s0=, s1=;
for (int i=head[x]; i; i=edge[i].next)
{
if (edge[i].to==fa) continue;
if (!s0) s0=edge[i].to;
else s1=edge[i].to;
dis[edge[i].to]=edge[i].len;
Dfs(edge[i].to,x);
size[x]+=size[edge[i].to];
}
if (!s0) s.a=, s.b=,A[x].push_back(s);
else Update(x,s0,s1);
} bool check(LL x)
{
for (int i=; i<=n; ++i) A[i].clear();
Dfs(,); return A[].size()>;
} int main()
{
scanf("%lld",&n);
for (int i=; i<=n-; ++i)
{
scanf("%lld%lld",&a[i],&v[i]);
add(i+,a[i],v[i]); add(a[i],i+,v[i]);
} l=, r=1ll*N*N, ans=-;
while (l<=r)
{
mid=(l+r)>>;
if (check(mid)) ans=mid,r=mid-;
else l=mid+;
}
printf("%lld\n",ans);
}

F - Shik and Copying String

题意:

求字符串S->T经过几次变化得到。字母改变的方式有两种:S[i][j] = S[i-1][j] || S[i][j-1]

题解:

这题为什么这么神仙啊QwQ

变换的过程发现可以用折线覆盖起来(如下图S=aabab,T=aaaba,需要3步)

AtCoder Grand Contest-LMLPHP

而对于一段连续相互影响的折线,他们需要的操作步数就是折线的个数(如上图就有三条折线相互影响)

具体实现就是开个双端队列……然后T串每一个连续相同的串对应一条折线……感觉说也说不明白还是直接上代码吧……QAQ 反正我就是对着题解的代码感悟的

 #include<iostream>
#include<cstring>
#include<cstdio>
#define N (1000009)
using namespace std; int n,p,ans,head=,tail,q[N];
char s[N],t[N]; int main()
{
scanf("%d%s%s",&n,s+,t+); p=n;
if (!strcmp(s+,t+)){puts(""); return ;}
for (int i=n; i>=; --i)
{
if (t[i]==t[i-]) continue;
p=min(p,i);
while (p && t[i]!=s[p]) p--;
if (!p) {puts("-1"); return ;} while (head<=tail)
if (q[head]-i-(tail-head+)>=) head++;
else break;
q[++tail]=p;
if (i!=p) ans=max(ans,tail-head+);
}
printf("%d\n",ans+);
}

AGC 008

C - Tetromino Tiling

题意:

给你一些奇形怪状的面积为4的图形,问最多能拼起来一个面积多大的2*2K的矩形。

题解:

可以发现能用到的只有I,O,J,L四种,而O种是可以直接计算进入答案的。分类讨论一波I,J,L的奇偶性就好了。

 #include<cstdio>
long long ans,a[];
int main()
{
for (int i=; i<=; ++i)
scanf("%lld",&a[i]);
ans=a[];
if (!a[]||!a[]||!a[])
ans+=a[]/*+a[]/*+a[]/*;
else
{
ans+=a[]+a[]+a[];
if (!(a[]%==a[]%&&a[]%==a[]%)) ans--;
}
printf("%lld",ans);
}

D - K-th K

题意:

给出数组x[1..n],要求构造一个长度为n*n的数组a,使得a中1到n每个数恰好出现n次,且对于任意的i,有从左到右第i个出现的i的下标为x[i]。

题解:

将所有数字按出现的位置从小到大sort一下,从前往后把一个数字前面应该有的填了,填完了再从后往前把一个数字后面应该有的填了。看代码应该非常好懂……

 #include<iostream>
#include<cstdio>
#include<algorithm>
#define N (250000+1000)
using namespace std; int n,ans[N],now;
struct Node{int num,pos;}a[N];
bool cmp(Node a,Node b){return a.pos<b.pos;} int main()
{
scanf("%d",&n);
for (int i=; i<=n; ++i)
{
scanf("%d",&a[i].pos);
a[i].num=i; ans[a[i].pos]=i;
}
sort(a+,a+n+,cmp); now=;
for (int i=; i<=n; ++i)
{
for (int j=; j<a[i].num; ++j)
{
while (ans[now]) now++;
ans[now]=a[i].num;
}
if (now>a[i].pos){puts("No"); return ;}
}
now=n*n;
for (int i=n; i>=; --i)
{
for (int j=; j<=n-a[i].num; ++j)
{
while (ans[now]) now--;
ans[now]=a[i].num;
}
if (now<a[i].pos){puts("No"); return ;}
}
puts("Yes");
for (int i=; i<=n*n; ++i)
printf("%d ",ans[i]);
}

E - Next or Nextnext

没看懂题解所以弃了

F - Black Radius

题意:

​给定一棵树,树上有一些点是可选点。可以选中某个可选点和一个半径d,把与选定点距离不超过距离d的点全染黑,求染色方案数。

题解:

瞻仰了一发国家队队长的代码QvQ……果然神仙的代码思路就是清晰

每个点单独考虑,对于以每个点为中心,我们卡出ta的一个d的上界和d的下界,保证对于每个可选点的可选范围取并集能做到方案不重不漏。为了方便计算我们中途不取全集,最后再把全集的答案加上。以下讨论均把当前点当作根来看

(1)如果当前点是可选点,那么下界L肯定是0,即只选中本身。上界R怎么卡呢?可以发现如果以当前点为中心的话,半径肯定是和最深深度次大的子树相关联的。如下图

AtCoder Grand Contest-LMLPHP

最深深度次大的子树为2,显然在半径d不超过子树2的最大深度的时候,染色染出来的连通块的直径的中心肯定一直是当前点(根)。

(2)如果当前点不是可选点,我们就要看哪棵子树里面有可选点。如下图

AtCoder Grand Contest-LMLPHP

假设1子树有可选点,下界显然是d刚好等于1的最深深度的时候,也就是1子树内的可选点不停向周围扩散,填满了1子树且把2和3子树染了d。上界和上一个情况同理。

所以我们要求的就是一个点的最短路和次短路,DP就可以了。

 #include<iostream>
#include<cstdio>
#define N (300000+1000)
using namespace std; struct Edge{int to,next;}edge[N<<];
int head[N],num_edge,Maxdep[N],size[N],sum,n,u,v;
long long ans;
char s[N]; void add(int u,int v)
{
edge[++num_edge].to=v;
edge[num_edge].next=head[u];
head[u]=num_edge;
} void Dfs1(int x,int fa)
{
size[x]=(s[x]=='');
for (int i=head[x]; i; i=edge[i].next)
if (edge[i].to!=fa)
{
Dfs1(edge[i].to,x);
size[x]+=size[edge[i].to];
Maxdep[x]=max(Maxdep[x],Maxdep[edge[i].to]+);
}
} void Dfs2(int x,int fa,int maxdis)
{
int l=(s[x]==''?:(<<)), r, max1=maxdis, max2=;
if (sum-size[x]) l=min(l,maxdis);
for (int i=head[x]; i; i=edge[i].next)
if (edge[i].to!=fa)
{
if (Maxdep[edge[i].to]+>max2) max2=Maxdep[edge[i].to]+;
if (max2>max1) swap(max1,max2);
if (size[edge[i].to]) l=min(l,Maxdep[edge[i].to]+);
}
if (max1==max2) r=max2-;
else if (max1==max2+) r=max2;
else r=max2+;
if (l<=r) ans+=r-l+;
for (int i=head[x]; i; i=edge[i].next)
if (edge[i].to!=fa)
{
if (Maxdep[edge[i].to]+!=max1) Dfs2(edge[i].to,x,max1+);
else Dfs2(edge[i].to,x,max2+);
}
} int main()
{
scanf("%d",&n);
for (int i=; i<n; ++i)
{
scanf("%d%d",&u,&v);
add(u,v); add(v,u);
}
scanf("%s",s+);
for (int i=; i<=n; ++i) sum+=(s[i]=='');
Dfs1(,); Dfs2(,,);
printf("%lld\n",ans+);
}

AGC 009

C - Division into Two

题意:

给n个不同的数,划分成两个集合X,Y。满足X中的数两两之间的差>=A,y中的数两两之间的差>=B

题解:

假设A<B,那么若存在s[i]-s[i-2]<A则无解,因为i,i-1,i-2的三个数两两间不能放到同一集合里。
设f[i]表示第i个元素放到Y集合的方案数。若f[j]可以转移到f[i],则一定满足s[i]-s[j]>=B 且[j+1,i-1]的数可以一起被放到X集合里。
也就是说可转移的范围是f[?,j]。而f[?,j]这个区间是随着i右移而右移的,所以用一个变量sum时刻存储一下这段区间的和就好了。

 #include<iostream>
#include<cstdio>
#include<algorithm>
#define N (100009)
#define MOD (1000000007);
using namespace std;
long long n,a,b,r,sum,s[N],f[N];
int main()
{
scanf("%lld%lld%lld",&n,&a,&b);
if (a>b) swap(a,b);
for (int i=; i<=n; ++i)
scanf("%lld",&s[i]);
for (int i=; i<=n; ++i)
if (s[i]-s[i-]<a){puts(""); return ;}
f[]=; s[]=-b; sum=; r=;
for (int i=; i<=n; ++i)
{
while (r<i && s[i]-s[r]>=b) sum=(sum+f[r++])%MOD;
f[i]=sum;
if (s[i]-s[i-]<a) sum=, r=i-;
}
for (int i=r; i<=n; ++i)
sum=(sum+f[i])%MOD;
printf("%lld\n",sum);
}

D - Uninity

题意:

给定一棵树,让你自己确定点分治的分治中心,问点分树的最小树高是多少

题解:

这题真是神仙题,托爷比赛的时候都没A掉……

首先由点分治的性质可以发现,若我们给每个点编号为它在点分树上的深度,那么若x点编号为k,y点编号为k,则x->y的路径上肯定存在一个点的编号小于k。这个很容易由点分治的过程得出。为了方便计算,后面将叶子标号为0,根标号为最大深度。

下面考虑原树,对于一个点来说,若它的子树里面有两个编号为k的,那么这个点的编号就至少为k+1。又因为如果按点分治的流程走,标号最大为log,所以我们可以开一个数组f[x][i]来存储子树x内有多少个编号为i的点。

若一个点标号为lim,那么它的子树里所有标号小于lim的点都合法了,因为这些点一定是合法的了。看代码可能更好懂……?

 #include<iostream>
#include<cstdio>
#define N (100000+1000)
using namespace std; struct Edge{int to,next;}edge[N<<];
int n,u,v,dep[N],f[N][],ans;
int head[N],num_edge; void add(int u,int v)
{
edge[++num_edge].to=v;
edge[num_edge].next=head[u];
head[u]=num_edge;
} void Dfs(int x,int fa)
{
for (int i=head[x]; i; i=edge[i].next)
if (edge[i].to!=fa)
{
Dfs(edge[i].to,x);
for (int j=; j<=; ++j)
f[x][j]+=f[edge[i].to][j];
}
int lim=;
for (int i=; i>=; --i)
if (f[x][i]>) {lim=i+; break;}
while (f[x][lim]) lim++;
dep[x]=lim;
for (int i=; i<=lim-; ++i) f[x][i]=;
f[x][lim]++; ans=max(ans,dep[x]);
} int main()
{
scanf("%d",&n);
for (int i=; i<=n-; ++i)
{
scanf("%d%d",&u,&v);
add(u,v); add(v,u);
}
Dfs(,);
printf("%d\n",ans);
}

E - Eternal Average

题意:

给你n个0,m个1,和一个k。每次操作你选择k个数,擦去这k个数并加入他们的平均数(1个),问最后会有多少种不同的实数。

题解:

我也就指望着题解苟活一下了……

首先转换一下模型,假设现在有一个k叉树,有n+m个叶子,n个叶子为0,m个叶子为1。非叶子节点的权值等于它儿子权值的平均数。设n个为0的点深度分别为$a_1…a_n$,m个为1的点深度为$b_1…b_m$,那么可以列出式子$\sum_{i=1}^nk^{-ai}+\sum_{i=1}^mk^{-bi}=1$

这是显然正确的,因为这个式子就相当于把0看成1求平均数,最后平均数一定为1.

而又显然根节点权值为$ans=\sum_{i=1}^mk^{-bi}$,那么ans可以表示成m个k的多少次幂的和,1-ans可以表示成n个k的多少次幂的和。不妨用k进制来表示ans

设$ans=0.s_1s_2...s_l$

如果不考虑进位的话,所有s的和就是m,那么如果考虑进位呢?
因为是k进制所以只要所有s的和%(k-1)==m%(k-1)就可以了。1-ans同理。

接下来我们只需要构造一个ans,使得ans和1-ans满足条件就好了。

用$f[i][j][0/1]$表示DP到小数点后第i位,前i位的和为j,第i位是否为0。转移比较显然,可以用前缀和优化。

 #include<iostream>
#include<cstdio>
#define N (4009)
#define MOD (1000000000+7)
using namespace std;
long long n,m,k,sum[N],f[N][N][],ans;
int main()
{
scanf("%lld%lld%lld",&n,&m,&k); m--; k--;
f[][][]=;
for (int i=; i<=n+m; ++i)
{
for (int j=; j<=n; ++j)
sum[j+]=(sum[j]+f[i-][j][]+f[i-][j][])%MOD;//因为可能有sum[-1]所以sum数组整体右移了一位
for (int j=; j<=n; ++j)
{
f[i][j][]=(sum[j+]-sum[j]+MOD)%MOD;
f[i][j][]=(sum[j]-sum[max(j-k,0ll)])%MOD;
}
for (int j=; j<=n; ++j)
if (j%k==n%k && (i*k-j)%k==m%k && i*k-j<=m)
ans=(ans+f[i][j][])%MOD;
}
printf("%lld\n",(ans+MOD)%MOD);
}

AGC 010

C - Cleaning

题意:

给你一颗树,每个节点上有一些石子,你可以选中两个叶子节点,并将路径上的节点的石子数量减一。必须保证选中路径上每个节点都有石子。问是否可以把石子删光。

题解:

一个exciting的贪心应该是……

假设现在只有一个两层的树,包含一个根节点和若干个叶子。要想使叶子节点全部变成0,需要满足:

1、叶子权值和一定≥父亲权值和,因为叶子节点减1的时候父亲节点肯定也要减1.。

2、叶子权值和≤2*父亲权值和,因为叶子节点的权值每次是减2的,而父亲节点是减1。

3、叶子权值的最大值应该小于等于父亲节点。

然后从下往上推就好了……

 #include<iostream>
#include<cstdio>
#define N (100009)
using namespace std; struct Edge{int to,next;}edge[N<<];
int n,u,v,a[N],Ind[N],flag=true;
int head[N],num_edge; void add(int u,int v)
{
edge[++num_edge].to=v;
edge[num_edge].next=head[u];
head[u]=num_edge;
} void Dfs(int x,int fa)
{
if (!flag) return;
int maxn=;
long long sum=;
for (int i=head[x]; i; i=edge[i].next)
if (edge[i].to!=fa)
{
Dfs(edge[i].to,x);
sum+=a[edge[i].to];
maxn=max(maxn,a[edge[i].to]);
}
if (!maxn) return;
if (sum<a[x] || sum>*a[x] || maxn>a[x]) flag=false;
a[x]=a[x]*-sum;
} int main()
{
scanf("%d",&n);
for (int i=; i<=n; ++i)
scanf("%d",&a[i]);
for (int i=; i<=n-; ++i)
{
scanf("%d%d",&u,&v);
add(u,v); add(v,u);
Ind[u]++; Ind[v]++;
} if (n==)
{
if (a[]==a[]) puts("YES");
else puts("NO");
return ;
} int root=;
while (Ind[root]<=) root++;
Dfs(root,);
if (a[root]!=) flag=false;
if (flag) puts("YES");
else puts("NO");
}

D - Decrementing

题意:

有一个gcd为1的序列,每次可以选择一个数减1,然后将整个序列除以他们的gcd。先把序列变成全1的获胜。问是先手胜还是后手胜。

题解:

博弈题玩弄人性(雾

显然当序列为x,x,x,x+1的形式的时候,游戏才能一步结束。另外一个人肯定要避免让对手到达这样的局面,当且仅当x=1的时候,游戏才会结束。
1、偶数个数为奇数的时候,先手必胜。因为先手可以通过控制奇偶性不变,来到达上述必胜局面。
2、偶数个数为偶数的时候,先手要尝试改变偶数个数的奇偶性。
(1)显然当奇数个数大于1或者奇数中存在1的时候,无法改变,先手必败。因为要改变个数奇偶性的唯一方法是同除一个偶数,而这显然是不可能的……
(2)如果上面都不满足就递归模拟。每次当前操作者肯定不能选偶数进行操作,不然就会把对方送进必胜局面。每次一方随便选择一个奇数,直到出现前面提到的局面的时候就可以停止递归了。最多递归log层。

 #include<iostream>
#include<cstdio>
using namespace std; int n,a[];
int gcd(int a,int b){return b==?a:gcd(b,a%b);} bool Dfs(int x)
{
int odd=,even=,flag=;
for (int i=; i<=n; ++i)
if (a[i]%) {a[i]--; break;}
int g=a[];
for (int i=; i<=n; ++i) g=gcd(g,a[i]);
for (int i=; i<=n; ++i)
{
a[i]/=g;
if (!(a[i]%)) even++;
else odd++;
if (a[i]==) flag=;
}
if (even%) return !x;
if (odd!= || !flag) return x;
return Dfs(x^);
} int main()
{
int odd=,even=,flag=;
scanf("%d",&n);
for (int i=; i<=n; ++i)
{
scanf("%d",&a[i]);
if (a[i]%) odd++;
else even++;
if (a[i]==) flag=false;
}
if (even%) puts("First");
else if (odd!= || !flag) puts("Second");
else if (Dfs()) puts("First");
else puts("Second");
}
05-04 05:34