原文链接https://www.cnblogs.com/zhouzhendong/p/AtCoder-Grand-Contest-from-1-to-10.html
考虑到博客内容较多,编辑不方便的情况,我决定把做题小记拆开写。
题解中的低级错误请指出,但是由于这里写的都是简要题解,所以具体细节就不要问我了。
咕咕咕
AGC009E
突然发现 AGC001F 怎么没做
AGC001
D 出现奇数的个数大于2时一定无解(构造图,从图的连通性方面考虑)。然后,如果有奇数,把他们放到头尾,然后 b 数组就是 a 数组的基础上,b[1]++,b[m]-- 。注意一些特殊情况要分讨。
Code
#include <bits/stdc++.h> using namespace std; const int M=105; int n,m,a[M],b[M]; bool cmp(int a,int b){ return (a&1)>(b&1); } int main(){ scanf("%d%d",&n,&m); for (int i=1;i<=m;i++) scanf("%d",&a[i]); if (m==1){ if (a[1]==1) printf("%d %d %d",a[1],1,a[1]); else printf("%d %d %d %d",a[1],2,a[1]-1,1); return 0; } int t=0; for (int i=1;i<=m;i++) t+=a[i]&1; if (t>2) return puts("Impossible"),0; sort(a+1,a+m+1,cmp); if (a[2]&1) swap(a[2],a[m]); for (int i=1;i<=m;i++) b[i]=a[i]; int _m=m; b[1]++,b[m]--; if (b[m]==0) _m--; for (int i=1;i<=m;i++) printf("%d ",a[i]); puts(""); printf("%d ",_m); for (int i=1;i<=_m;i++) printf("%d ",b[i]); return 0; }
E 题意相当于求 $sum_{1leq i < jleq n} inom{a_i+a_j+b_i+b_j}{a_i+b_i}$ ,其中 $n,a_i,b_i$给定。$ (nleq 2 imes 10^5, 1leq a_i,b_ileq 2000)$
Sol 对于 $i,j$ ,$inom{a_i+a_j+b_i+b_j}{a_i+b_i}$ 就相当于从 $(-a_i,-b_i)$ 走到 $(a_j,b_j)$ (只能向上或者向右走)的方案总数。考虑对于每一个点在二维平面上打标记,直接 $O(4000^2)$ 递推一下,然后得到总和。但是这样会把 $i=j$ 以及 $i>j$ 的也算进去,做法是先把 $i=j$ 的减掉,然后答案除以 2 。
#include <bits/stdc++.h> using namespace std; const int N=200005,mod=1e9+7; int n,a[N],b[N]; int Fac[N],Inv[N]; int Pow(int x,int y){ int ans=1; for (;y;y>>=1,x=1LL*x*x%mod) if (y&1) ans=1LL*ans*x%mod; return ans; } int sum[4002][4002],_0=2001; int C(int n,int m){ if (m<0||m>n) return 0; return 1LL*Fac[n]*Inv[m]%mod*Inv[n-m]%mod; } int add(int a,int b){ a+=b; if (a>=mod) a-=mod; return a; } int del(int a,int b){ a-=b; if (a<0) a+=mod; return a; } int main(){ Fac[0]=1; for (int i=1;i<N;i++) Fac[i]=1LL*Fac[i-1]*i%mod; Inv[N-1]=Pow(Fac[N-1],mod-2); for (int i=N-1;i>=1;i--) Inv[i-1]=1LL*Inv[i]*i%mod; scanf("%d",&n); memset(sum,0,sizeof sum); for (int i=1;i<=n;i++){ scanf("%d%d",&a[i],&b[i]); sum[-a[i]+_0][-b[i]+_0]++; } for (int i=1;i<4002;i++) for (int j=1;j<4002;j++) sum[i][j]=add(sum[i][j],add(sum[i-1][j],sum[i][j-1])); int ans=0; for (int i=1;i<=n;i++){ ans=add(ans,sum[a[i]+_0][b[i]+_0]); ans=del(ans,C(a[i]+a[i]+b[i]+b[i],b[i]+b[i])); } ans=1LL*ans*Pow(2,mod-2)%mod; printf("%d",ans); return 0; }
AGC002
D 首先考虑到整体二分 + 按秩合并并查集 (用于维护连通信息,要可删除) ,这里显然是对于最大边编号整体二分。我们考虑把这个整体二分的过程写成 bfs ,注意到我们在 bfs 的过程中,各组询问被分成了 log 层,同一层中二分的值递增。于是我们就不需要按秩合并了。考虑在每一层的开始暴力重构并查集,最多只会重构 log 次,所以复杂度就对了。
Code - 注意这份代码看着很像dfs,实际是bfs。
#include <bits/stdc++.h> using namespace std; const int N=100005; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n,m,Q; int fa[N],size[N],id[N],cur=0; struct Edge{ int x,y; }e[N]; struct Query{ int x,y,z,ans; }q[N]; void clear_fa(){ for (int i=1;i<=n;i++) fa[i]=i,size[i]=1; } int getf(int x){ return fa[x]==x?x:fa[x]=getf(fa[x]); } void Merge(int x,int y){ x=getf(x),y=getf(y); if (x!=y) size[fa[y]=x]+=size[y]; } struct Node{ int L,R,Q; int *id; Node(){} Node(int a,int b,int *d,int c){ L=a,R=b,Q=c,id=d; } }s[N<<5]; int head,tail; int tL[N],cL; int tR[N],cR; void Insert(int L,int R,int *id,int Q){ s[++tail]=Node(L,R,id,Q); } int calc(int x,int y){ x=getf(x),y=getf(y); return x==y?size[x]:(size[x]+size[y]); } void solve(int L,int R,int *id,int Q){ if (L>R||Q==0) return; int mid=(L+R)>>1; if (cur>mid) clear_fa(),cur=0; while (cur<mid) cur++,Merge(e[cur].x,e[cur].y); cL=cR=0; for (int i=1;i<=Q;i++){ int ID=id[i]; if (calc(q[ID].x,q[ID].y)>=q[ID].z) tL[++cL]=ID,q[ID].ans=min(q[ID].ans,mid); else tR[++cR]=ID; } for (int i=1;i<=cL;i++) id[i]=tL[i]; for (int i=1;i<=cR;i++) id[cL+i]=tR[i]; Insert(L,mid-1,id,cL); Insert(mid+1,R,id+cL,cR); } void BFS(int L,int R,int *id,int Q){ head=tail=0; clear_fa(); Insert(L,R,id,Q); while (head<tail) head++,solve(s[head].L,s[head].R,s[head].id,s[head].Q); } int main(){ n=read(),m=read(); for (int i=1;i<=m;i++) e[i].x=read(),e[i].y=read(); Q=read(); for (int i=1;i<=Q;i++) q[i].x=read(),q[i].y=read(),q[i].z=read(),id[i]=i,q[i].ans=m; BFS(1,m,id,Q); for (int i=1;i<=Q;i++) printf("%d ",q[i].ans); return 0; }
E 考虑将原序列降序排序,对于每一个 $a_i$ ,对于第 $i$ 行,填充前 $a_i$ 个位置。于是构成了一个杨氏矩阵。原题的操作就相当于每次只能删掉第一行或者第一列。可以证明格子 $(i,i)$ 的胜负性都相同。于是我们只需要尽量取一个大的 $i$ ,然后根据 $(i,i)$ 位置向上和向右最多能走的步数的奇偶性判断即可。
Code
#include <bits/stdc++.h> using namespace std; const int N=100005; int n,a[N]; int main(){ scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%d",&a[i]); sort(a+1,a+n+1); reverse(a+1,a+n+1); for (int i=1;i<=n;i++) if (a[i+1]<i+1){ int j=i; while (j<n&&a[j+1]==i) j++; int f1=(j-i)&1,f2=(a[i]-i)&1; puts(f1||f2?"First":"Second"); return 0; } return 0; }
F 一道简单 DP 题。传送门 - https://www.cnblogs.com/zhouzhendong/p/AGC002F.html
AGC003
C 给定一个长度为 $n$ 的序列 $a$ ,其中任意两个数都不同。你有两种操作,分别是翻转某一个长度为 2 或 3 的连续子序列。你需要将它排序,问最少要使用多少次 “交换相邻两个数” 这种操作。$(1leq nleq 10^5,a_ileq 10^9$
Sol 首先离散化,然后考虑到交换三个数的操作效果就是对于奇偶性相同的数位进行任意变换。于是最终答案就是所有数值和其位置奇偶性不同的数的个数除以2.
Code
#include <bits/stdc++.h> using namespace std; const int N=100005; int n,a[N]; int Ha[N]; int main(){ scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%d",&a[i]),Ha[i]=a[i]; sort(Ha+1,Ha+n+1); int tot=0; for (int i=1;i<=n;i++){ a[i]=lower_bound(Ha+1,Ha+n+1,a[i])-Ha; if ((a[i]^i)&1) tot++; } printf("%d",tot/2); return 0; }
D 给定 $n$ 个正整数 $s_i$ ,从中找出最多的数,使得它们两两的乘积中,没有完全立方数。$(1leq nleq 10^5,1leq s_ileq 10^{10})$。
Sol 对于每一个数,首先除掉其所有立方因子。然后对于除掉立方因子后的某一类数,和他乘积为立方数的是另一类数,这两类只能选一类,所以选择多的。注意所有完全立方数中只能选一个。
Code
#include <bits/stdc++.h> using namespace std; typedef long long LL; const int N=100005; int n; LL a[N],psqr[N]; int vis[N],prime[N],pcnt; void Get_Prime(int n){ pcnt=0; memset(vis,0,sizeof vis); for (int i=2;i<=n;i++){ if (vis[i]) continue; prime[++pcnt]=i; for (int j=i<<1;j<=n;j+=i) vis[j]=1; } } map <LL,int> cubic,sqr,cnt; int check_cubic(){ int f=0; int _n=0; for (int i=1;i<=n;i++) if (cubic[a[i]]) f=1; else a[++_n]=a[i]; n=_n; return f; } LL Get(LL x){ LL res=1; for (int i=1;i<=pcnt;i++) if (x%prime[i]==0){ LL cnt=1; x/=prime[i]; if (x%prime[i]==0) x/=prime[i],cnt++; cnt=3-cnt; while (cnt>0){ cnt--,res*=prime[i]; if (res>1e11) return -1; } } if (x>1){ LL t=x; if (sqr[x]>0) t=sqr[x]; else { if (x>1e8) return -1; t=x*x; } if (t>1e11/res) return -1; res*=t; } return res>1e10?-1:res; } int main(){ scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%lld",&a[i]); cubic.clear(),sqr.clear(); for (int i=1;i<=2500;i++) cubic[1LL*i*i*i]=i; for (int i=1;i<=100000;i++) sqr[1LL*i*i]=i; int ans=check_cubic(); Get_Prime(2500); for (int i=1;i<=pcnt;i++) psqr[i]=1LL*prime[i]*prime[i]*prime[i]; cnt.clear(); for (int i=1;i<=n;i++){ for (int j=1;j<=pcnt;j++) while (a[i]%psqr[j]==0) a[i]/=psqr[j]; cnt[a[i]]++; } for (int i=1;i<=n;i++){ if (!cnt[a[i]]) continue; LL b=Get(a[i]); ans+=max(cnt[a[i]],cnt[b]); cnt[a[i]]=cnt[b]=0; } printf("%d",ans); return 0; }
E 序列 $a[1 cdots n]$ 一开始满足 $a[i]=i$ 。现在有 $m$ 次操作,第 $i$ 次给定一个参数 $Q_i$ ,操作的具体效果:将原序列复制无限次,取前 $Q_i$ 个构成新序列,替换当前序列。输出最终序列中,数字 $1cdots n$ 各出现了几次。$(n,mleq 10^5,Q_ileq 10^{18})$
Sol 首先把操作序列转化成一个升序序列,然后考虑一种暴力:记 solve(i,x) 为前 i 次操作后,取长度为 k 的前缀,所得到的答案。那么显然:solve(i,x) = x/Q[i] * solve(i-1,Q[i]) + solve(i-1,x mod Q[i]) 。考虑 $x<Q_i$ 的特殊情况,有 solve(i,x) = solve(i-1,x) 。一个数不断对比它小的数取模,最多只会取 log 次,所以 solve(i,x) 就被分成了一些 solve(a,Q[b]) 的和,与一个前缀。具体实现见代码。
Code
#include <bits/stdc++.h> using namespace std; typedef long long LL; const int N=100005; LL read(){ LL x=0,f=1; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+ch-48,ch=getchar(); return x; } int n,t,m=0; LL a[N],s[N],ans[N]; int Binary(int L,int R,LL v){ int ans=0; while (L<=R){ int mid=(L+R)>>1; if (a[mid]>v) R=mid-1; else L=mid+1,ans=mid; } return ans; } int main(){ n=read(),t=read(); a[++m]=n; for (int i=1;i<=t;i++){ LL x=read(); while (m>0&&a[m]>=x) m--; a[++m]=x; } memset(s,0,sizeof s); memset(ans,0,sizeof ans); s[m]=1; for (int i=m;i>=1;i--){ LL x=a[i],j=i; while (j=Binary(1,j-1,x)) s[j]+=x/a[j]*s[i],x%=a[j]; ans[x]+=s[i]; } for (int i=n-1;i>=1;i--) ans[i]+=ans[i+1]; for (int i=1;i<=n;i++) printf("%lld ",ans[i]); return 0; }
F 给定的原图的黑色区域连通是一个非常好的性质。那么,如果存在一行,使得这一行的开始元素和结尾元素都为黑色,那么我们称其为“行连通”;类似地,定义“列连通”。 如果给定的图行连通且列连通,那么显然不管 k 是多少,最终都是连通的,即答案为 1 。 否则: 我们把一次分形扩展之后,通过行或者列的首尾所连通的块造成的连通块数缩减量,称为额外减量。 设 $c_0$ 为原图中黑色格子的个数。 如果给定的图行列都不连通,那么,不管怎么扩展,都不会出现额外减量,所以答案就是 $c_0^{k-1}$ 。 否则,如果行不连通,我们可以通过翻转行列把原问题转化成仅行连通的问题。 考虑求解行连通的问题。首先,列不会对额外减量做出贡献。每一次的额外减量非常好算,设使得边连通的行的个数为 $c_1$ ,设原图中,有 $f_1$ 对左右相邻的格子都是黑色。则我们需要知道的仅仅是当前状态下,所有行的最右侧格子中,黑格子属于了多少个连通块,显然这个东西就是每次扩展翻 $c_1$ 倍。每次的额外减量就是这个东西再乘上 $f_1$ 。于是只需要构造一个 2*2 的矩阵再跑快速幂就好了。
Code
#include <bits/stdc++.h> using namespace std; typedef long long LL; const int N=1005,mod=1e9+7; int n,m; LL k; char s[N][N]; int Pow(int x,LL y){ int ans=1; for (;y;y>>=1,x=1LL*x*x%mod) if (y&1LL) ans=1LL*ans*x%mod; return ans; } int f1=0,f2=0; int check(){ for (int i=1;i<=n;i++) if (s[i][1]=='#'&&s[i][m]=='#') f1++; for (int i=1;i<=m;i++) if (s[1][i]=='#'&&s[n][i]=='#') f2++; return (bool)f1+(bool)f2; } int cnts(){ int cnt=0; for (int i=1;i<=n;i++) for (int j=1;j<=m;j++) if (s[i][j]=='#') cnt++; return cnt; } struct Mat{ int v[2][2]; Mat(){} Mat(int x){ memset(v,0,sizeof v); for (int i=0;i<2;i++) v[i][i]=x; } void set(int c0,int c1,int c3){ v[0][0]=c3 ,v[0][1]=mod-c1 ; v[1][0]=0 ,v[1][1]=c0 ; } }M; Mat operator * (Mat A,Mat B){ Mat C(0); for (int i=0;i<2;i++) for (int j=0;j<2;j++) for (int k=0;k<2;k++) C.v[i][j]=(1LL*A.v[i][k]*B.v[k][j]+C.v[i][j])%mod; return C; } Mat Pow(Mat x,LL y){ Mat ans(1); for (;y;y>>=1,x=x*x) if (y&1LL) ans=ans*x; return ans; } int main(){ scanf("%d%d%lld",&n,&m,&k); if (k==0) return puts("1"),0; for (int i=1;i<=n;i++) scanf("%s",s[i]+1); int v=check(),c0=cnts(); if (v==0) return printf("%d ",Pow(c0,k-1)),0; if (v==2) return puts("1"),0; int c1=0,c2=0; for (int i=1;i<=n;i++) for (int j=1;j<=m;j++){ if (j<m&&s[i][j]=='#'&&s[i][j+1]=='#') c1++; if (i<n&&s[i][j]=='#'&&s[i+1][j]=='#') c2++; } if (f2) swap(c1,c2),swap(f1,f2); M.set(c0,c1,f1); M=Pow(M,k-1); printf("%d",(M.v[0][1]+M.v[1][1])%mod); return 0; }
AGC004
C 简单构造题。题目里说到:第一行和最后一行都是空的。所以直接构造“梳子”形即可。
Code
#include <bits/stdc++.h> using namespace std; const int N=505; int n,m; char g[N][N],R[N][N],B[N][N]; int main(){ scanf("%d%d",&n,&m); for (int i=1;i<=n;i++) scanf("%s",g[i]+1); for (int i=1;i<=n;i++) for (int j=1;j<=m;j++) R[i][j]=B[i][j]='.'; for (int i=1;i<=m;i++) R[1][i]=B[n][i]='#'; for (int i=2;i<n;i++) for (int j=1;j<=m;j++) if (j&1) R[i][j]='#'; else B[i][j]='#'; for (int i=1;i<=n;i++) for (int j=1;j<=m;j++) if (g[i][j]=='#') R[i][j]=B[i][j]='#'; for (int i=1;i<=n;i++) puts(R[i]+1); puts(""); for (int i=1;i<=n;i++) puts(B[i]+1); return 0; }
D 简单贪心题。注意到题目中,首都也是 town 。所以首都必须连向自己,形成自环。于是问题被转化成:修改一些节点的父亲,使得所有节点到根距离都不大于 k 。只需要从下到上贪心地保证距离不大于 k 就可以了。
Code
#include <bits/stdc++.h> using namespace std; const int N=100005; int n,k,fa[N]; int ans=0,Maxd[N]; vector <int> e[N]; void solve(int x){ Maxd[x]=0; for (auto y : e[x]){ solve(y); Maxd[x]=max(Maxd[x],Maxd[y]+1); } if (Maxd[x]==k-1&&fa[x]!=1) ans++,Maxd[x]=-1; } int main(){ scanf("%d%d",&n,&k); for (int i=1;i<=n;i++) e[i].clear(); for (int i=1;i<=n;i++){ scanf("%d",&fa[i]); if (i>1) e[fa[i]].push_back(i); } if (fa[1]!=1) fa[1]=1,ans++; solve(1); printf("%d",ans); return 0; }
E 没想到是个 $O(n^4)$ 的 DP 。瞄了一眼题解,然后推了一下,写了个记忆化dfs ,跑的好慢。 考虑把移动机器人看做移动终点和矩形边框,用 $dp[x1][y1][x2][y2]$ 表示边框包括这个子矩形,在这个子矩形内继续操作得到的答案。显然在不影响自矩形边界的情况下,终点位置范围也是一个矩形区域,而且终点可以在这个矩形区域内自由移动(注意到如果能转移到这个状态的前提条件是终点在合法区域内)。考虑转移,就是对于四条边,分别: 获取该边上能自由获取的贡献,然后删除边,子矩阵的长或宽会减一,递归处理子问题即可。结合代码理解更佳。
Code
#include <bits/stdc++.h> #define y1 __zzd001 using namespace std; const int N=105; int n,m,ax,ay,bx,by; char s[N][N]; short g[N][N],dp[N][N][N][N]; short Get(int x1,int y1,int x2,int y2){ return g[x2][y2]-g[x2][y1-1]-g[x1-1][y2]+g[x1-1][y1-1]; } short max(short a,short b){ return a>b?a:b; } short DP(int x1,int y1,int x2,int y2){ // printf("DP %d %d %d %d ",(int)x1,(int)y1,(int)x2,(int)y2); short &v=dp[x1][y1][x2][y2]; if (~v) return v; if (x1>x2||y1>y2) return v=0; short xL=max(x2-bx+1,x1),xR=min(x1+ax-1,x2); short yL=max(y2-by+1,y1),yR=min(y1+ay-1,y2); return v=max( max(DP(x1,y1,x2,y2-1)+((yR==y2)?Get(xL,y2,xR,y2):0), DP(x1,y1+1,x2,y2)+((yL==y1)?Get(xL,y1,xR,y1):0)), max(DP(x1,y1,x2-1,y2)+((xR==x2)?Get(x2,yL,x2,yR):0), DP(x1+1,y1,x2,y2)+((xL==x1)?Get(x1,yL,x1,yR):0))); } int main(){ memset(g,0,sizeof g); scanf("%d%d",&n,&m); for (int i=1;i<=n;i++) scanf("%s",s[i]+1); for (int i=1;i<=n;i++) for (int j=1;j<=m;j++) if (s[i][j]=='o') g[i][j]++; else if (s[i][j]=='E') ax=i,bx=n-i+1,ay=j,by=m-j+1; for (int i=1;i<=n;i++) for (int j=1;j<=m;j++) g[i][j]=g[i][j]+g[i][j-1]+g[i-1][j]-g[i-1][j-1]; memset(dp,-1,sizeof dp); printf("%d",(int)DP(1,1,n,m)); return 0; }
F 转自 博客 https://blog.csdn.net/werkeytom_ftd/article/details/78393489
这题很优秀。
Code
#include <bits/stdc++.h> using namespace std; typedef long long LL; const int N=100005; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n,m; vector <int> e[N]; namespace Tree{ LL ans=0; int v[N],c[N]; void solveT(int x,int pre,int d){ c[x]=v[x]=d; for (auto y : e[x]) if (y!=pre) solveT(y,x,-d),v[x]+=v[y]; ans+=abs(v[x]); } void solve(){ solveT(1,0,1); printf("%lld ",v[1]==0?ans:-1); } } int c[N],vis[N]; int vx,vy,tag[N]; void dfs(int x,int pre,int d){ vis[x]=1,c[x]=d; for (auto y : e[x]) if (y!=pre&&!(tag[x]&&tag[y])) if (vis[y]) vx=x,vy=y,tag[x]=tag[y]=1; else dfs(y,x,-d); vis[x]=0; } void RE(){ int k=-1; while (1) k-=84,c[k]++; } namespace Odd{ int v[N]; LL ans=0; void dfs(int x,int pre,int d){ v[x]=(c[x]+=d); for (auto y : e[x]) if (y!=pre&&!(tag[x]&&tag[y])){ dfs(y,x,-d); v[x]+=v[y]; } ans+=abs(v[x]); } void solve(){ memset(c,0,sizeof c); dfs(1,0,1); int V=v[1]; if (V%2!=0) return (void)puts("-1"); memset(c,0,sizeof c); ans=abs(c[vx]=c[vy]=-V/2); dfs(1,0,1); printf("%lld",ans); } } namespace Even{ int v[N],k[N]; void dfs(int x,int pre,int d){ v[x]=c[x]=d; for (auto y : e[x]) if (y!=pre&&!(tag[x]&&tag[y])){ dfs(y,x,-d); v[x]+=v[y]; k[x]+=k[y]; } } vector <int> a; LL ans=0; void solve(){ memset(c,0,sizeof c); memset(k,0,sizeof k); k[vx]=1,k[vy]=-1; dfs(1,0,1); if (v[1]!=0) return (void)puts("-1"); a.clear(); a.push_back(0); for (int i=1;i<=n;i++) if (k[i]==0) ans+=abs(v[i]); else a.push_back(-k[i]*v[i]); sort(a.begin(),a.end()); int s=((int)a.size()+1)/2; int M=a[s-1]; for (int i=0;i<a.size();i++) ans+=abs(a[i]-M); printf("%lld",ans); } } int main(){ n=read(),m=read(); for (int i=1;i<=n;i++) e[i].clear(); for (int i=1;i<=m;i++){ int a=read(),b=read(); e[a].push_back(b); e[b].push_back(a); } if (m==n-1) return (Tree :: solve()),0; memset(vis,0,sizeof vis); dfs(1,0,1); if (c[vx]==c[vy]) Odd :: solve(); else Even :: solve(); return 0; }
AGC005
C 首先,与距离一个点最远的两个点必然是直径端点之一。先给 a 排序,找到直径长度 d。那么显然,如果存在 $a_i<leftlceil frac d2 ight ceil $ ,那么无解。由于至少要构成一条直径,直径上各个点距离其最长距离已知,如果 a 中不包含这些距离,那么也无解。否则删除相应的 $a_i$ ,考虑剩下的点直接把直径当作主干,旁边连出去就好了。但是有一个特殊情况,如果还存在未被删除的 $a_i$ 满足 $a_i=leftlceil frac d2 ight ceil $ ,那么由于连出去至少要贡献 1 距离,所以这类点显然不能构造出来了,所以这样也无解。剩下的情况就是有解了。
Code
#include <bits/stdc++.h> using namespace std; const int N=105; int n,a[N]; int tot[N]; int main(){ scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%d",&a[i]),tot[a[i]]++; sort(a+1,a+n+1); reverse(a+1,a+n+1); int d=a[1]; for (int i=1;i<=n;i++) if (a[i]<((d+1)/2)) return puts("Impossible"),0; for (int i=1;i<=d+1;i++) if ((--tot[max(i-1,d+1-i)])<0) return puts("Impossible"),0; if (tot[(d+1)/2]>0) return puts("Impossible"),0; puts("Possible"); return 0; }
D 考虑容斥,所以我们需要对于每一个 $i$ ,求出有 $i$ 个位置不合法的情况总数。显然对于 $k$ 取模相同的位置会互相占位,我们只需要用dp预处理出来就好了。然后再总体整合一下即可。
Code
#include <bits/stdc++.h> using namespace std; const int N=2005,mod=924844033; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n,k; int Fac[N]; int dp1[N][N][4],dp[N]; void add(int &x,int y){ if ((x+=y)>=mod) x-=mod; } int main(){ n=read(),k=read(); int t=n/k+1; memset(dp1,0,sizeof dp1); dp1[0][0][2]=1; for (int i=0;i<t;i++) for (int j=0;j<t;j++){ /* 0 = __ --> ___ | __* , *__ 1 = _* --> _*_ | _** , **_ 2 = *_ --> *__ | *_* 3 = ** --> **_ | *** */ add(dp1[i+1][j][0],dp1[i][j][0]); add(dp1[i+1][j][2],dp1[i][j][1]); add(dp1[i+1][j][0],dp1[i][j][2]); add(dp1[i+1][j][2],dp1[i][j][3]); add(dp1[i+1][j+1][0],dp1[i][j][0]); add(dp1[i+1][j+1][1],dp1[i][j][0]); add(dp1[i+1][j+1][3],dp1[i][j][1]); add(dp1[i+1][j+1][2],dp1[i][j][1]); add(dp1[i+1][j+1][1],dp1[i][j][2]); add(dp1[i+1][j+1][3],dp1[i][j][3]); } dp[0]=1; for (int i=1;i<=k;i++){ int t=n/k+(i<=n%k?1:0); for (int j=n;j>=0;j--) for (int k=1;k<=t&&k+j<=n;k++) dp[j+k]=(1LL*dp[j]*(dp1[t][k][0]+dp1[t][k][2])+dp[j+k])%mod; } for (int i=Fac[0]=1;i<=n;i++) Fac[i]=1LL*Fac[i-1]*i%mod; int ans=0; for (int i=0;i<=n;i++){ dp[i]=1LL*dp[i]*Fac[n-i]%mod; if (i&1) ans=(ans-dp[i]+mod)%mod; else ans=(ans+dp[i])%mod; } printf("%d",ans); return 0; }
E 首先必须想到一个结论: 如果 A 能走到一条边 (x,y) ,满足在 另一棵树上 , $x,y$ 的距离大于 2 ,那么 答案就是 -1 。否则, B 一定可以抓住 A ,B 只需要一步一步把 A 的活动范围逼小,而 A 只能到一个点等死。
Code
#include <bits/stdc++.h> using namespace std; const int N=200005; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n,X,Y; vector <int> er[N],eb[N]; int depth[N],fa[N][20]; int tag[N]; void dfs1(int x,int pre,int d){ depth[x]=d,fa[x][0]=pre; for (int i=1;i<20;i++) fa[x][i]=fa[fa[x][i-1]][i-1]; for (auto y : eb[x]) if (y!=pre) dfs1(y,x,d+1); } int LCA(int x,int y){ if (depth[x]<depth[y]) swap(x,y); for (int i=19;i>=0;i--) if (depth[x]-(1<<i)>=depth[y]) x=fa[x][i]; if (x==y) return x; for (int i=19;i>=0;i--) if (fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i]; return fa[x][0]; } int Dis(int x,int y){ return depth[x]+depth[y]-2*depth[LCA(x,y)]; } int vis[N],ans=0; void dfs(int x,int d){ vis[x]=1; int k1=Dis(x,Y); if (d>=k1){ ans=max(ans,d*2-(d>k1)); return; } if (tag[x]){ ans=max(ans,10000000); return; } ans=max(ans,k1*2); for (auto y : er[x]) if (!vis[y]) dfs(y,d+1); } int main(){ n=read(),X=read(),Y=read(); for (int i=1;i<=n;i++) er[i].clear(),eb[i].clear(); for (int i=1;i<n;i++){ int a=read(),b=read(); er[a].push_back(b); er[b].push_back(a); } for (int i=1;i<n;i++){ int a=read(),b=read(); eb[a].push_back(b); eb[b].push_back(a); } dfs1(1,0,0); memset(tag,0,sizeof tag); memset(vis,0,sizeof vis); for (int x=1;x<=n;x++) for (auto y : er[x]) if (Dis(x,y)>2) tag[x]=tag[y]=1; dfs(X,0); printf("%d",ans<2e6?ans:-1); return 0; }
F 首先考虑计算一个 k 的答案。方法是容斥。考虑分开计算每一个点对答案的贡献,显然,点 x 对于答案的贡献是 $inom{n}{k} - inom{n-size_x}{k} - sum_{y is a son of x} inom{size_y}{k}$ ,这里需要注意一下树根的特殊化。然后我们发现我们只需要统计出对于每一个 $v$ 我们在计算 $k$ 的答案时要减掉多少个 $inom{v}{k}$ 即可。假设 $tax[i]$ 表示我们需要减掉的 $inom{i}{k}$ 个数,那么我们发现计算所有的答案时,只需要构造卷积式 FFT 一下就好了,构造方法见代码最后的注释。这里的 924844033 是一个 NTT 模数。$(924844033=441 imes 2^{21} +1)$
Code
#include <bits/stdc++.h> using namespace std; const int N=1<<19,mod=924844033; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int Pow(int x,int y){ int ans=1; for (;y;y>>=1,x=1LL*x*x%mod) if (y&1) ans=1LL*ans*x%mod; return ans; } int n; vector <int> e[N]; int size[N],tax[N]; void dfs(int x,int pre){ size[x]=1; for (auto y : e[x]) if (y!=pre){ dfs(y,x); size[x]+=size[y]; } tax[size[x]]++; tax[n-size[x]]++; } int w[N],R[N],A[N],B[N]; void FFT(int a[N],int n){ for (int i=0;i<n;i++) if (R[i]<i) swap(a[i],a[R[i]]); for (int t=n>>1,d=1;d<n;d<<=1,t>>=1) for (int i=0;i<n;i+=(d<<1)) for (int j=0;j<d;j++){ int tmp=1LL*w[t*j]*a[i+j+d]%mod; a[i+j+d]=(a[i+j]-tmp+mod)%mod; a[i+j]=(a[i+j]+tmp)%mod; } } void Mul(){ int m,d; for (d=0,m=1;m<n*2;m<<=1,d++); w[0]=1,w[1]=Pow(5,(mod-1)/m); for (int i=2;i<m;i++) w[i]=1LL*w[i-1]*w[1]%mod; for (int i=0;i<m;i++) R[i]=(R[i>>1]>>1)|((i&1)<<(d-1)); FFT(A,m); FFT(B,m); for (int i=0;i<m;i++) A[i]=1LL*A[i]*B[i]%mod; w[1]=Pow(w[1],mod-2); for (int i=2;i<m;i++) w[i]=1LL*w[i-1]*w[1]%mod; FFT(A,m); int inv=Pow(m,mod-2); for (int i=0;i<m;i++) A[i]=1LL*A[i]*inv%mod; } int Fac[N],Inv[N]; int C(int n,int m){ if (m<0||m>n) return 0; return 1LL*Fac[n]*Inv[m]%mod*Inv[n-m]%mod; } int main(){ n=read(); for (int i=1;i<=n;i++) e[i].clear(); for (int i=1;i<n;i++){ int a=read(),b=read(); e[a].push_back(b); e[b].push_back(a); } memset(tax,0,sizeof tax); dfs(1,0); Fac[0]=Inv[0]=1; for (int i=1;i<=n;i++){ Fac[i]=1LL*Fac[i-1]*i%mod; Inv[i]=1LL*Inv[i-1]*Pow(i,mod-2)%mod; } for (int i=1;i<n;i++) A[i]=1LL*tax[i]*Fac[i]%mod; for (int i=0;i<=n;i++) B[i]=Inv[n-i]; Mul(); for (int i=1;i<=n;i++) A[i+n]=1LL*A[i+n]*Inv[i]%mod; for (int i=1;i<=n;i++) printf("%lld ",(1LL*C(n,i)*n-A[i+n]+mod)%mod); return 0; } /* C(n,k) - ΣC(size[son],k) - C(n-size[x],k) 多项式大小 小于子树size 问题变成了 ans_i = Σ C(a[j], i) = a[j]! / i! / (a[j]-i)! a[j] + (a[j]-i) = i 令 A[i] = i! 令 B[i] = 1/C[i-n] 令 C[i] = (-i)! (i<=0) = 0 (i>0) ans[i] = Σ A[j] * B[i-j] */
AGC006
C 题意题解代码链接
https://www.cnblogs.com/zhouzhendong/p/AGC006C.html
D 二分答案,把判断中位数是否小于等于当前值。把小于等于当前值的数赋值成 0 ,否则赋值成 1 。于是取 3 个数的中位数就变成了区间众数。考虑如果两个连续的数字相同,那么他们上面填的所有数都一定和它相同。如果有连续一段都是 01 间隔的,那么他们上面也是 01 间隔的,发现这些性质之后就足够做出这题了。这里引用一个图片示意一下。
图片来自 beginend 的博客 :https://blog.csdn.net/qq_33229466/article/details/79327710
Code
#include <bits/stdc++.h> using namespace std; const int N=200005; int n,a[N]; int b[N]; int check(int k){ for (int i=1;i<n*2;i++) b[i]=a[i]>k; int m=n; int L=m,R=m; while (L>1&&b[L]!=b[L-1]) L--; while (R<n*2-1&&b[R]!=b[R+1]) R++; if (L==m||R==m) return b[m]; if (b[L]==b[R]) return b[L]; return m-L>R-m?b[R]:b[L]; } int main(){ scanf("%d",&n); for (int i=1;i<n*2;i++) scanf("%d",&a[i]); int L=1,R=n*2-1,mid,ans=R; while (L<=R){ mid=(L+R)>>1; if (!check(mid)) R=mid-1,ans=mid; else L=mid+1; } printf("%d",ans); return 0; }
E 首先排除一些基础错误:考虑一开始在同一列的三个数永远在同一列。而且原来在奇数列的一定永远在奇数列,偶数列同理。一列只有可能发生上下翻转。
根本想不到。
考虑一种特殊的变换:(来自官方题解)
• a b c d e
• C B A d e
• C B E D a
• e b c D a
• e b A d C
• a B E d C
• a B c D e (1)
• a d C b e
• c D A b e
• c B a d e
• A b C d e (2)
效果就是可以翻转任意两列奇偶性形同的列。
考虑一次翻转操作,不仅会交换左侧列和右侧列,还会翻转中间的那个列。然后注意我们要将所有数列归位,再执行翻转操作。我们对于奇数列和偶数列分开考虑,设原序列中奇数列被翻转的个数为 a1 ,偶数列被翻转的个数为 a2 ,使得奇数列归位的操作次数为 c1 ,偶数列归位的操作次数为 c2 。由于 c1 次操作每一次都会使欧数列的翻转奇偶性改变, c2 次同理;而我们只能找到这种翻转两个列的操作,所以,当且仅当 a1 与 c2 奇偶性相同,a2 与 c1 的奇偶性相同才是 Yes ,否则就是 No 。
Code
#include <bits/stdc++.h> using namespace std; const int N=100005; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n,a[N][4]; void No(){ puts("No"); exit(0); } int c[N]; void add(int x,int d){ for (;x<=n;x+=x&-x) c[x]^=d; } int ask(int x){ int ans=0; for (;x;x-=x&-x) ans^=c[x]; return ans; } int main(){ n=read(); for (int i=1;i<=3;i++) for (int j=1;j<=n;j++) a[j][i]=read(); for (int i=1;i<=n;i++){ int t=(a[i][2]-1)/3; for (int j=1;j<=3;j++) if ((a[i][j]-1)/3!=t) No(); if (a[i][2]!=t*3+2) No(); if ((t&1)==(i&1)) No(); } int a1=0,a2=0,c1=0,c2=0; for (int i=n;i>=1;i-=2) if (a[i][1]>a[i][3]) a1^=1; for (int i=n-1;i>=1;i-=2) if (a[i][1]>a[i][3]) a2^=1; memset(c,0,sizeof c); for (int i=n;i>=1;i-=2){ int t=a[i][2]/3+1; c1^=ask(t); add(t,1); } memset(c,0,sizeof c); for (int i=n-1;i>=1;i-=2){ int t=a[i][2]/3+1; c2^=ask(t); add(t,1); } puts(a1==c2&&a2==c1?"Yes":"No"); return 0; }
F 首先转化题意,把题意看成一个有向图中,如果存在边 (x,y),(y,z) ,那么可以生成 (z,x) ,问总共可以有多少边。考虑先把原图看做一个若连通图,然后对于每一个连通块进行求解。对于每一个若连通块,我们将它三染色。我们不妨把他们看成 A,B,C 三个集合,如果三种集合的元素都存在,那么必然所有 A 类点都连向所有 B 类点,同理, B 连向 C ,C 连向 A 。如果存在这些边之外的边,那么必然不能完成三染色,答案也必然是节点数的平方; 否则,答案就是 |A|*|B|+|B|*|C|+|C|*|A| 。再者,如果 A, B, C 中有一个集合没有元素,那么答案就是该连通块原先的边数。
Code
#include <bits/stdc++.h> using namespace std; typedef long long LL; const int N=100005; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n,m; vector <int> e[N]; int vis[N]; LL ans=0; int cnt[3],cnte=0; bool dfs(int x,int c){ cnt[vis[x]=c]++; bool res=1; for (auto t : e[x]){ int y=t>>1,z=t&1; if (!z) cnte++; int _c=(c+(z?1:-1)+3)%3; if (~vis[y]) res&=vis[y]==_c; else res&=dfs(y,_c); } return res; } LL solve(int x){ cnte=0; memset(cnt,0,sizeof cnt); if (!dfs(x,0)){ int s=cnt[0]+cnt[1]+cnt[2]; return 1LL*s*s; } if (!cnt[0]||!cnt[1]||!cnt[2]) return cnte; return 1LL*cnt[0]*cnt[1]+1LL*cnt[1]*cnt[2]+1LL*cnt[2]*cnt[0]; } int main(){ n=read(),m=read(); for (int i=1;i<=n;i++) e[i].clear(); for (int i=1;i<=m;i++){ int a=read(),b=read(); e[a].push_back(b<<1|1); e[b].push_back(a<<1); } memset(vis,-1,sizeof vis); for (int i=1;i<=n;i++) if (!~vis[i]) ans+=solve(i); printf("%lld",ans); return 0; }
AGC007
C 万万没想到。通过先百度再推式子可以发现这个东西不管怎么操作,距离的期望永远是等差数列。接下来放公式:(含义:对于当前操作,新增的变换)
$$ans+=frac{(d_1) + (d_1+(2n-1)x)}{2}$$
考虑第 $i$ 段线段段期望增量。这里的主要思路是:如果操作的球在当前线段左边,那么显然操作后的第 $i$ 条线段就是原先的第 $i+2$ 条线段,增量为 $2x$ ;如果操作的球使得当前线段和另一条线段合并了,那么特殊处理;否则操作的球在后面,没有影响。这里分 $i=2k+1$ 和 $i=2k$ 讨论:
$$i=2k+1 : d_i+=frac{2 imes cfrac{i-1}{2} imes 2x + 2x + (d+ix) +(d+(i+1)x)}{2n}$$
$$i=2k: d_i+=frac{2 imes cfrac{i}{2} imes 2x + (d+ix) +(d+(i+1)x)}{2n}$$
上述二式都等于
$$d_i+=frac{4ix+2d+x}{2n}$$
故
$$d_1+=frac{5x+2d}{2n}, x+=frac{2x}{n},n--$$
Code
#include <bits/stdc++.h> using namespace std; typedef long long LL; typedef long double LD; const int N=200005; int n; LD d,x,ans=0; int main(){ scanf("%d%Lf%Lf",&n,&d,&x); while (n>0){ ans+=(1.0L*(2*n-1)*x+d+d)/2.0L; d+=1.0L*(5.0L*x+2.0L*d)/(2.0L*n); x+=2.0L*x/n; n--; } printf("%.12Lf",ans); return 0; }
D 简单题。先写出时间复杂度为 O(n^2) 的 DP 方程,然后考虑一个贪心,就是当你再往后走一个位置的并回到当前段的起点的用时仍然没有超过 T ,那么显然继续走。对于超过的部分,直接搞个单调指针,取个 min 就好了。
Code
#include <bits/stdc++.h> #define mp make_pair #define pb push_back using namespace std; typedef long long LL; const int N=132000; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n,son[N][2],a[N],v[N]; vector <pair <LL,LL> > dp[N],tmpL,tmpR,tmp; LL Min[N],val[N]; LL Limit; bool cmp1(pair <LL,LL> a,pair <LL,LL> b){ if (a.first!=b.first) return a.first<b.first; return a.second<b.second; } bool cmp2(pair <LL,LL> a,pair <LL,LL> b){ if (a.second!=b.second) return a.second<b.second; return a.first<b.first; } void update(vector <pair <LL,LL> > &v){ if (v.empty()) return; sort(v.begin(),v.end(),cmp2); tmp.clear(); tmp.pb(v[0]); for (int i=1;i<v.size();i++) if (v[i].second!=v[i-1].second) tmp.pb(v[i]); v.clear(); v.pb(tmp[0]); for (int i=1;i<tmp.size();i++) if (tmp[i].first!=tmp[i-1].first) v.pb(tmp[i]); } void solve(int x){ if (!son[x][0]){ dp[x].clear(); dp[x].pb(mp(0LL,0LL)); return; } int ls=son[x][0],rs=son[x][1]; solve(ls+1); solve(rs+1); tmpL.clear(),tmpR.clear(); for (auto i : dp[ls+1]) tmpL.pb(mp(i.first+v[ls],i.second+v[ls])); for (auto i : dp[rs+1]) tmpR.pb(mp(i.first+v[rs],i.second+v[rs])); if (tmpL.size()>tmpR.size()) swap(tmpL,tmpR); sort(tmpR.begin(),tmpR.end(),cmp1); for (int i=0;i<tmpR.size();i++){ if (i>0) Min[i]=min(Min[i-1],tmpR[i].second); else Min[i]=tmpR[i].second; val[i]=tmpR[i].first; } dp[x].clear(); for (int i=0;i<tmpL.size();i++){ int p=upper_bound(val,val+(int)tmpR.size() ,Limit-tmpL[i].second)-val-1; if (p==-1) continue; dp[x].pb(mp(tmpL[i].first,Min[p])); dp[x].pb(mp(Min[p],tmpL[i].first)); } update(dp[x]); } bool check(LL x){ Limit=x; solve(1); return ((int)dp[1].size())>0; } int main(){ n=read(); for (int i=1;i<n;i++){ a[i]=read(); v[i]=read(); son[a[i]][(bool)son[a[i]][0]]=i; } LL L=0,R=1LL<<36,mid,ans=R; while (L<=R){ mid=(L+R)>>1; if (check(mid)) R=mid-1,ans=mid; else L=mid+1; } printf("%lld",ans); return 0; }
E 没想到去证明简化后的状态是 $O(nlog n)$ 的。做法:首先考虑二分答案,然后就变成了从每一个子节点引出两条路径,在每一个非叶子节点中合并一对合法路径,判定是否有方法。还是相当于每一个节点向上走有两个路径。可以表示成一个二元组 $(a,b)$ 。考虑合并两个子树的某一组状态: $(a,b)+(c,d)$ ,如果满足 $b+cleq Limit$ 那么,结果就是 $(a,d)$ 。对于每一个 $a$ ,必然只取一个合法的最小 $d$ ;同理对于每一个 $d$ 也只取一个合法的最小 $a$ 。这样的话,状态数就只和较小的那个子树有关了。于是所有节点的状态数之和就是 $O(nlog n)$ 级别的了。于是总的复杂度可以做到 $O(nlog nlog v)$ 。但是我选择了偷懒,用了 $log^3$ 的做法。
Code
#include <bits/stdc++.h> #define mp make_pair #define pb push_back using namespace std; typedef long long LL; const int N=132000; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n,son[N][2],a[N],v[N]; vector <pair <LL,LL> > dp[N],tmpL,tmpR,tmp; LL Min[N],val[N]; LL Limit; bool cmp1(pair <LL,LL> a,pair <LL,LL> b){ if (a.first!=b.first) return a.first<b.first; return a.second<b.second; } bool cmp2(pair <LL,LL> a,pair <LL,LL> b){ if (a.second!=b.second) return a.second<b.second; return a.first<b.first; } void update(vector <pair <LL,LL> > &v){ if (v.empty()) return; sort(v.begin(),v.end(),cmp2); tmp.clear(); tmp.pb(v[0]); for (int i=1;i<v.size();i++) if (v[i].second!=v[i-1].second) tmp.pb(v[i]); sort(tmp.begin(),tmp.end(),cmp1); v.clear(); v.pb(tmp[0]); for (int i=1;i<tmp.size();i++) if (tmp[i].first!=tmp[i-1].first) v.pb(tmp[i]); } void solve(int x){ if (!son[x][0]){ dp[x].clear(); dp[x].pb(mp(0LL,0LL)); return; } int ls=son[x][0],rs=son[x][1]; solve(ls+1); solve(rs+1); tmpL.clear(),tmpR.clear(); for (auto i : dp[ls+1]) tmpL.pb(mp(i.first+v[ls],i.second+v[ls])); for (auto i : dp[rs+1]) tmpR.pb(mp(i.first+v[rs],i.second+v[rs])); if (tmpL.size()>tmpR.size()) swap(tmpL,tmpR); sort(tmpR.begin(),tmpR.end(),cmp1); for (int i=0;i<tmpR.size();i++){ if (i>0) Min[i]=min(Min[i-1],tmpR[i].second); else Min[i]=tmpR[i].second; val[i]=tmpR[i].first; } dp[x].clear(); for (int i=0;i<tmpL.size();i++){ int p=upper_bound(val,val+(int)tmpR.size() ,Limit-tmpL[i].second)-val-1; if (p==-1) continue; dp[x].pb(mp(tmpL[i].first,Min[p])); dp[x].pb(mp(Min[p],tmpL[i].first)); } update(dp[x]); } bool check(LL x){ Limit=x; solve(1); return ((int)dp[1].size())>0; } int main(){ n=read(); for (int i=1;i<n;i++){ a[i]=read(); v[i]=read(); son[a[i]][(bool)son[a[i]][0]]=i; } LL L=0,R=1LL<<36,mid,ans=R; while (L<=R){ mid=(L+R)>>1; if (check(mid)) R=mid-1,ans=mid; else L=mid+1; } printf("%lld",ans); return 0; }
F 罕见的既不是一眼题也不是看题解的题目。 首先考虑转化题目。我们记一个串 $A$ 的前一个串为 $^PA$ 。如果存在方案,那么对于 $T$ 串的每一个连续段,设其开头位置为 $x$ ,则必然有 $T[x]=^PT[x]$ 。于是问题被转化成:在 $S$ 串中选择一些字符与 $^PT[x]$ 的每一个确定的字符一一对应,并在一个网格图中找到一组从每一个 $S$ 串中选择的点向右下走到对应点的不相交路径,使得这个网格图的高度最小。很容易知道我们在满足条件的情况下,在 $S$ 串中选择的位置会尽量靠后,所以我们可以贪心地倒着扫一遍求出来。然后,仍然贪心地使每次增加高度尽量小。从后往前加路径,会在某一个时刻形成如下情况:
第一次加入蓝色,相当于 8~10 列 +1 。第二次绿色,相当于原先的全部左移一格,然后 6~8 加一。第三次黄色,同理相当于原先的全部左移一格,然后 4~7 列加一。红色同理。
这个东西用个数组维护一下就好了。
注意要特判答案 $in{-1,0,1}$ 的情况。
Code
#include <bits/stdc++.h> using namespace std; const int N=1000005; int n,m; int b[N],tb=0,a[N],ta; int st[N],top=0; char S[N],T[N]; int check(){ for (int i=1;i<=m;i++) if (a[i]!=b[i]) return 0; return 1; } int checksame(){ for (int i=1;i<=n;i++) if (S[i]!=T[i]) return 0; return 1; } int main(){ scanf("%d%s%s",&n,S+1,T+1); T[0]='*'; for (int i=1;i<=n;i++) if (T[i]!=T[i-1]) b[++tb]=i; ta=tb; for (int i=n;i>=1;i--) if (i<=b[ta]&&S[i]==T[b[ta]]) a[ta--]=i; m=tb; if (ta>0) return puts("-1"),0; if (checksame()) return puts("0"),0; if (check()) return puts("1"),0; st[top]=n+1; int ans=0,add=0; for (int i=m;i>=1;i--){ int L=1,R=top,mid,now=top+1; while (L<=R){ mid=(L+R)>>1; if (b[i]>=st[mid]-add) R=mid-1,now=mid; else L=mid+1; } ans=max(ans,top-now+1+1); add++; st[++top]=a[i]+add; } printf("%d",ans+1); return 0; }
AGC008
C 简单题。可以发现只有 方块 、L 形 、 一 形 会被用到。方块直接放就可以了。所以只要考虑其他三种。具体不展开介绍。
#include <bits/stdc++.h> using namespace std; typedef long long LL; LL ans=0; int a,b,c,d,e,f,g; int x,y,z; int main(){ scanf("%d%d%d%d%d%d%d",&a,&b,&c,&d,&e,&f,&g); ans=b; x=a,y=d,z=e; int t=(x&1)+(y&1)+(z&1); if (y&1) swap(x,y); if (z&1) swap(z,y); if (t>=2){ if (z>0) ans+=3,x--,y--,z--; } ans+=x/2*2; ans+=y/2*2; ans+=z/2*2; printf("%lld",ans); return 0; }
D 考虑将二元组 $(i,x_i)$ 按照 $x_i$ 升序排列,然后正着插入数字一遍,反着插一遍,中间顺便判掉无解就好了。
#include <bits/stdc++.h> using namespace std; const int N=505*505; int n; int a[N]; struct Node{ int i,x; friend bool operator < (Node A,Node B){ return A.x<B.x; } }x[N]; int main(){ scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%d",&x[i].x),x[i].i=i; sort(x+1,x+n+1); int L=1,R=n*n; memset(a,0,sizeof a); for (int i=1;i<=n;i++) a[x[i].x]=x[i].i; for (int i=1;i<=n;i++){ int t=x[i].i-1; while (t--){ while (L<=n*n&&a[L]) L++; if (L>x[i].x) return puts("No"),0; a[L]=x[i].i; } } for (int i=n;i>=1;i--){ int t=n-x[i].i; while (t--){ while (R>=1&&a[R]) R--; if (R<x[i].x) return puts("No"),0; a[R]=x[i].i; } } puts("Yes"); for (int i=1;i<=n*n;i++) printf("%d ",a[i]); return 0; }
E 居然肛出来了。特殊情况好多。首先把给定的 $a_i$ 序列生成一个基环树森林,然后发现,如果非环部分存在分叉一定无解;然后可以把环分成两类:裸环,环节点上带非环点的环。对于环节点上带非环节点的环,特殊处理,详见代码;对于裸环,任意两个大小相同的环可以合并,或者任意一个奇环可以有两种独立的构造方法,直接dp 就好了。具体看官方题解吧。
#include <bits/stdc++.h> using namespace std; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } const int N=100005,mod=1e9+7; int n,a[N],in[N],size[N],vis[N],cro[N]; int q[N],head,tail; int k[N],m; int r[N]; void add(int &x,int y){ if ((x+=y)>=mod) x-=mod; } int sc=0; int Pre(int k,int n){ return k==1?n:(k-1); } int solve(int n){ int ans=1,f=0; // printf("solve :: n = %d ",n); // for (int i=1;i<=n;i++) // printf(" %d",k[i]); // puts(""); for (int i=1;i<=n;i++){ if (k[i]==0) continue; f=1; int x=Pre(i,n); while (!k[x]) x=Pre(x,n); int t=(i-x+n)%n; if (t==0) t+=n; // printf("%d :: t = %d ",i,t); if (t<k[i]) return 0; if (t>k[i]) ans=2LL*ans%mod; } if (!f) r[n]++; return ans; } int dp[N]; int main(){ n=read(); memset(in,0,sizeof in); for (int i=1;i<=n;i++){ in[a[i]=read()]++; size[i]=1; } head=tail=0; for (int i=1;i<=n;i++){ if (in[i]>2) return puts("0"),0; if (in[i]==0) q[++tail]=i; if (in[i]==2) cro[i]=1; } while (head!=tail){ int x=q[++head],y=a[x]; size[y]+=size[x]; if (!(--in[y])) q[++tail]=y; } for (int i=1;i<=n;i++) if (cro[i]&&!in[i]) return puts("0"),0; int ans=1; memset(vis,0,sizeof vis); for (int i=1;i<=n;i++){ if (in[i]==0||vis[i]) continue; m=0; for (int x=i;!vis[x];x=a[x]) vis[x]=1,k[++m]=size[x]-1; ans=1LL*ans*solve(m)%mod; } for (int i=1;i<=n;i++) if (r[i]>0){ dp[0]=1,dp[1]=i>1&&(i&1)?2:1; for (int j=2;j<=r[i];j++) dp[j]=(1LL*(j-1)*i%mod*dp[j-2]+dp[j-1]*(i>1&&(i&1)?2:1))%mod; ans=1LL*ans*dp[r[i]]%mod; } printf("%d ",ans); return 0; }
F 一道毒瘤题。
1300分 :
首先考虑在什么情况下会重复:
1. d 很大,覆盖了整个树。
2. 假设将点 x 设为根,对于一种覆盖情况 (x,d) ,如果 x 有一个子节点 y 被全选了,那么操作 (y,d+1) 的结果 必然和 (x,d) 相同。
于是,我们要想办法去重:
1. 对于第一种情况,设 lim[x] 表示以 x 为根时的树的深度。我们将每一个点 x 的 d 的取值范围控制在 [0,lim[x]-1] 之间,最后答案加上一个 1 即可。
接下来记 Max[x] 表示 在节点 x 的操作中 d 最大可以是多少。
2. 对于第二种情况,设子树 x 的最大深度为 Maxd[x] 。则,如果 y 是 x 的一个儿子,由于之前提到的第二种情况下会出现重复,我们考虑对于 x 的结果去重,令 Max[x]=min(Max[x],Maxd[y]+1) 。如果子树 y 中存在方案 (y,d) ,那么,如果 (x,d-1) 与 (y,d) 相同,那么如果 (x,d-1) 满足限制,则必然有 d-1≥MaxD[y]+1,即 d≥MaxD[y]+2>MaxD[y]≥Max[y], 所以 (y,d) 不会重复统计。
对于每一个点为根的情况都做出如上限制,就可以得到 1300 分了。
1900分:
考虑非关键点的情况:虽然不能让非关键点作为操作中心,但是由于一些关键点也可以做到的局面被去重了,有的保留在了非关键点的答案里面。如果您仔细理解了之前去重的第二种情况的做法,仔细观察可以发现,只需要调整这些非关键点的下限就可以了。那么一个非关键点 x 的 d 的取值下限是多少呢?把 x 当作树根,对于所有 存在关键点的 x 的子树 y,取 MaxD[y] 的最小值,即 x 的 d 的取值下限。
Code 注:代码中的 Min 和 Max 定义和题解中恰好相反
#include <bits/stdc++.h> using namespace std; typedef long long LL; const int N=200005; int n; int f[N],MaxD[N],FaD[N],Lim[N],Min[N],Max[N],tot[N]; vector <int> e[N]; void dfs(int x,int pre){ MaxD[x]=0; tot[x]=f[x]; for (auto y : e[x]) if (y!=pre){ dfs(y,x); MaxD[x]=max(MaxD[x],MaxD[y]+1); tot[x]+=tot[y]; } } multiset <int> S; LL ans=0; void getlim(int x,int pre){ Lim[x]=max(FaD[x],MaxD[x]); S.clear(); S.insert(FaD[x]); for (auto y : e[x]) if (y!=pre) S.insert(MaxD[y]+1); for (auto y : e[x]) if (y!=pre){ S.erase(S.find(MaxD[y]+1)); FaD[y]=(*--S.end())+1; S.insert(MaxD[y]+1); } for (auto y : e[x]) if (y!=pre) getlim(y,x); } void solve(int x,int pre){ for (auto y : e[x]) if (y!=pre){ solve(y,x); Min[y]=min(Min[y],MaxD[y]+1); Min[x]=min(Min[x],FaD[y]); if (!f[y]&&tot[1]-tot[y]>0) Max[y]=min(Max[y],FaD[y]); if (!f[x]&&tot[y]>0) Max[x]=min(Max[x],MaxD[y]+1); } } int main(){ scanf("%d",&n); ans=1; for (int i=1;i<=n;i++) e[i].clear(); for (int i=1,a,b;i<n;i++){ scanf("%d%d",&a,&b); e[a].push_back(b); e[b].push_back(a); } for (int i=1;i<=n;i++) scanf("%1d",&f[i]); dfs(1,0); getlim(1,0); for (int i=1;i<=n;i++) Min[i]=Lim[i]-1,Max[i]=f[i]?0:1e9; solve(1,0); for (int i=1;i<=n;i++) ans+=max(Min[i]-Max[i]+1,0); printf("%lld",ans); return 0; }
AGC009
B 简单题,只要看懂题目就好了。
#include <bits/stdc++.h> using namespace std; const int N=100005; int n; int ans[N],a[N],at; vector <int> son[N]; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } void solve(int x){ ans[x]=1; for (auto y : son[x]) solve(y); at=0; for (auto y : son[x]) a[++at]=ans[y]; sort(a+1,a+at+1); reverse(a+1,a+at+1); for (int i=1;i<=at;i++) ans[x]=max(ans[x],a[i]+i); } int main(){ n=read(); for (int i=1;i<=n;i++) son[i].clear(); for (int i=2;i<=n;i++) son[read()].push_back(i); memset(ans,0,sizeof ans); solve(1); printf("%d",ans[1]-1); return 0; }
C 发现 dp 的过程中只要支持单点加和区间清零,为了避免写线段树,写两个队列就好了。
#include <bits/stdc++.h> using namespace std; typedef long long LL; const int N=100005,mod=1e9+7; LL read(){ LL x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n; LL A,B,s[N]; int dpA[N],sumA[N],hA; int dpB[N],sumB[N],hB; int del(int x,int y){return x-y<0?x-y+mod:x-y;} int add(int x,int y){return x+y>=mod?x+y-mod:x+y;} void Del(int &x,int y){if ((x-=y)<0)x+=mod;} void Add(int &x,int y){if ((x+=y)>=mod)x-=mod;} int main(){ n=read(),A=read(),B=read(); for (int i=1;i<=n;i++) s[i]=read(); s[0]=-1.5e18; dpA[1]=dpB[1]=sumA[1]=sumB[1]=hA=hB=1; for (int i=2;i<=n;i++){ if (s[i]-s[hB-1]<A) dpA[i]=0; else { int k=upper_bound(s+hB-1,s+i-1,s[i]-A)-s; dpA[i]=del(sumB[k],sumB[hB-1]); } if (s[i]-s[hA-1]<B) dpB[i]=0; else { int k=upper_bound(s+hA-1,s+i-1,s[i]-B)-s; dpB[i]=del(sumA[k],sumA[hA-1]); } sumA[i]=add(sumA[i-1],dpA[i]); sumB[i]=add(sumB[i-1],dpB[i]); if (s[i]-s[i-1]<A) hA=i; if (s[i]-s[i-1]<B) hB=i; } printf("%d",add(del(sumA[n],sumA[hA-1]) ,del(sumB[n],sumB[hB-1]))); return 0; }
D 一道有趣的题。我看了题解。首先考虑直接点分治的话答案就在 O(log n) 范围内。现在考虑点分树性质。如果标记每一个节点所代表的点分子树中最多能向下走的长度为 d[x] ,那么,把这些 d[x] 写回原树中,就可以发现一个规律:任意两个点 x,y,如果 d[x] = d[y] ,那么在 x 到 y 的简单路径上,至少存在一个点的 d 值大于 d[x] 。于是,反过来,如果满足了后面的,也可以证明它是一个点分树。于是我们考虑根据这个条件来做。于是我们可以对于子树 x 记录 f[x][i] 表示子树 x 中是否存在一个 d[y] = i 且节点 y 还没有任何一个祖先的 d 值大于 y ,那么我们显然可以贪心地转移。因为 d[x] 的值不大于 log n ,所以可以直接状压表示。
#include <bits/stdc++.h> using namespace std; const int N=100005; int read(){ int x=0,f=1; char ch=getchar(); while (!isdigit(ch)&&ch!='-') ch=getchar(); if (ch=='-') f=-1,ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x*f; } int n; vector <int> e[N]; int Log[N],ans=0,f[N]; void solve(int x,int pre){ int t=0; for (auto y : e[x]) if (y!=pre){ solve(y,x); t|=f[x]&f[y]; f[x]|=f[y]; } int k=t?Log[t]+1:0; while (k<30&&(f[x]>>k&1)) k++; ans=max(ans,k); f[x]|=1<<k; f[x]&=~((1<<k)-1); } int main(){ n=read(); for (int i=1;i<=n;i++) e[i].clear(); for (int i=1;i<n;i++){ int a=read(),b=read(); e[a].push_back(b); e[b].push_back(a); } Log[1]=0; for (int i=2;i<=n;i++) Log[i]=Log[i>>1]+1; solve(1,0); cout << ans; return 0; }
E 毒瘤不会。咕咕咕。
AGC010
C 简单题。
#include <bits/stdc++.h> using namespace std; typedef long long LL; const int N=100005; int n,A[N],in[N]; vector <LL> e[N],v[N]; LL solve(int x,int pre){ vector <LL> &a=v[x]; for (auto y : e[x]) if (y!=pre) a.push_back(solve(y,x)); if (a.empty()) return A[x]; sort(a.begin(),a.end()); if (!~a[0]) return -1; reverse(a.begin(),a.end()); LL tot=0; for (auto y : a) tot+=y; LL mx=min((LL)A[x],min(tot/2,tot-a[0])),Need=tot-A[x]; return Need>mx||Need<0?-1:(A[x]-Need); } int main(){ scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%d",&A[i]); for (int i=1,a,b;i<n;i++){ scanf("%d%d",&a,&b); e[a].push_back(b); e[b].push_back(a); in[a]++,in[b]++; } if (n==2) return puts(A[1]==A[2]?"YES":"NO"),0; for (int i=1;i<=n;i++) if (in[i]>1) return puts(solve(i,0)==0?"YES":"NO"),0; }
D 我好菜啊。看了题解。想必陈老爷一眼秒的题啊。考虑所有数的总和的奇偶性, -1 的话会改变,在除以 gcd 的时候,只有 gcd 为偶数的时候才会再变一次。(下面说道的“最终局面是指所有数都变成了 1”)如果当前局面存在两个及以上的奇数或者存在 1 ,那么胜负已定:如果先手的奇偶性和最终局面的奇偶性相同,那么对手一定不会让先手得到 “gcd 为偶数” 的机会来扭转局面;相反,如果先手的奇偶性和最终局面的奇偶性不同,那么先手必胜。 否则由于当前局面是保证 gcd 为奇数的,故不可能全是偶数,必然存在奇数 。那么如果先手将一个偶数 -1 之后会必胜,先手必然胜利(这句话好像是废话);否则,先手必然会选择使那个唯一的奇数 -1 ,于是局面就变成了全部都是偶数,gcd 也为偶数,于是除以 gcd 之后,所有数的和的奇偶性就和这次操作之前一样,先手就有可能胜利;这时还是有可能出现“只有一个奇数”的情况 ,于是把决策权交给对手,递归判定是否必胜即可。
#include <bits/stdc++.h> using namespace std; const int N=100005; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n,a[N]; int gcd(int a,int b){ return b?gcd(b,a%b):a; } bool check(){ int tot=n&1; for (int i=1;i<=n;i++) tot^=a[i]&1; for (int i=1;i<=n;i++) if (a[i]==1) return tot; int o=0; for (int i=1;i<=n;i++) o+=a[i]&1; if (o>1) return tot; for (int i=1;i<=n;i++) if (a[i]&1) a[i]--; int g=a[1]; for (int i=2;i<=n;i++) g=gcd(g,a[i]); for (int i=1;i<=n;i++) a[i]/=g; return !check(); } int main(){ n=read(); for (int i=1;i<=n;i++) a[i]=read(); puts(check()?"First":"Second"); return 0; }
E 这题好坑啊。我只想到了转成图,看相对顺序。
接下来继续转化模型。具体地:给每一对不互质的点连无向边,然后给所有边定向,使得最终是一个拓扑图且其字典序最大的拓扑序字典序最小。
接下来的做法非常巧妙:考虑对于一个图,我们对其进行 dfs ,每次尽量选择未被遍历过的相邻节点中权值最小的先遍历;这棵 dfs 生成树的字典序最大的拓扑序就是答案。
为什么这样是对的?—— 首先考虑到,选择完当前节点之后,删除该节点,会将该子图分成若干个互相没有边的极大连通分量,于是,这些子图的拓扑序之间是可以任意排列的,我们必然取最大的;而每一个连通分量的拓扑序仍然需要确定,对于每一个连通分量,我们必然会选择最小的一个与当前根连通的点作为该连通分量的根;依次类推。
#include <bits/stdc++.h> using namespace std; const int N=2005; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n; int a[N]; int gcd(int a,int b){ return b?gcd(b,a%b):a; } vector <int> e[N],ans[N],id[N]; vector <int> Merge(vector <int> a,vector <int> b){ vector <int> ans; ans.clear(); reverse(a.begin(),a.end()); reverse(b.begin(),b.end()); while (!a.empty()&&!b.empty()) if (a.back()>b.back()) ans.push_back(a.back()),a.pop_back(); else ans.push_back(b.back()),b.pop_back(); while (!a.empty()) ans.push_back(a.back()),a.pop_back(); while (!b.empty()) ans.push_back(b.back()),b.pop_back(); return ans; } int vis[N]; bool cmp(int x,int y){ return a[x]<a[y]; } void solve(int x){ vis[x]=1; id[x].clear(); for (auto y : e[x]) if (!vis[y]) id[x].push_back(y); sort(id[x].begin(),id[x].end(),cmp); ans[x].clear(); for (auto y : id[x]) if (!vis[y]){ solve(y); ans[x]=Merge(ans[x],ans[y]); } reverse(ans[x].begin(),ans[x].end()); ans[x].push_back(a[x]); reverse(ans[x].begin(),ans[x].end()); } int main(){ n=read(); for (int i=1;i<=n;i++){ a[i]=read(); e[i].clear(); } for (int i=1;i<=n;i++) for (int j=i+1;j<=n;j++) if (gcd(a[i],a[j])!=1){ e[i].push_back(j); e[j].push_back(i); } for (int i=1;i<=n;i++) e[0].push_back(i); solve(0); reverse(ans[0].begin(),ans[0].end()); ans[0].pop_back(); reverse(ans[0].begin(),ans[0].end()); for (auto y : ans[0]) printf("%d ",y); return 0; }
F 考虑对于每一个点为起点的情况分开考虑。如果点 x 为起点,那么将整棵树看做一个以 x 为根的有根数。
于是对于从某一个节点向子树走的过程,有如下分析:
1. 走一个子树,对手只需要在子树里就可以打败你。
2. 可以强制走一个子树使得对手在这个子树里操作,且对手在这个子树中操作是必败的。
3. 对手可以肛回来打败你 ($A[x]leq A[son]$)。
当且仅当第 2 种情况可以胜利。
于是直接 dfs 一遍就好了。
#include <bits/stdc++.h> using namespace std; const int N=3005; int read(){ int x=0; char ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return x; } int n,a[N]; vector <int> e[N]; bool dfs(int x,int pre){ bool res=0; for (auto y : e[x]) if (y!=pre) res|=a[x]>a[y]&&!dfs(y,x); return res; } bool check(int x){ return dfs(x,0); } int main(){ n=read(); for (int i=1;i<=n;i++){ a[i]=read(); e[i].clear(); } for (int i=1;i<n;i++){ int a=read(),b=read(); e[a].push_back(b); e[b].push_back(a); } for (int i=1;i<=n;i++) if (check(i)) printf("%d ",i); return 0; } /* 1. 走一个子树,对手只需要在子树里就可以打败你。 2. 可以强制走一个子树使得对手在这个子树里并获胜。 3. 对手可以肛回来打败你。 当且仅当第 2 种情况可以胜利。 */