[COI 2025] 象掌兽 / Lirili Larila 题解

题目传送门:[COI 2025] 象掌兽 / Lirili Larila

我宣布 夏虫[Ynoi2007] rgxsxrs 的代码难度在这题面前都弱爆了。

下面的题解几乎完全复制了模拟赛的出题人的题解,稍微修改了一些小错误并加上了一些细节。但是因为我不知道出题人是谁,所以无法 @ 出他,对此我深感抱歉。如果这位出题人看到了这篇博客麻烦告知一下,我方便指出出处。顺便让我知道一下是哪位毒瘤把这个东西放 T2

算法一

我会暴力!

枚举 \(s,t\) 然后 \(O(n + m)\) BFS 跑最短路并判断。
时间复杂度 \(O(n^2(n + m))\),期望得分 \(6\)

算法二

我会树!

以下称 \(dis(s, u) < dis(t, u)\) 的点为 \(A\) 类点,\(dis(s, u) > dis(t, u)\) 的点为 \(B\) 类点,\(dis(s, u) = dis(t, u)\) 的点为 \(C\) 类点。
\(dis(s, t) \ge 3\),那么让 \(s\)\(t\) 分别朝向对方移动一步,所有点的类别不变。所以只用考虑 \(dis(s, t) \le 2\) 的情况。
\(dis(s, t) = 1\),枚举这条边,那么 \(A\) 类点在 \(s\) 子树内,\(B\) 类点在 \(t\) 子树内。

\(dis(s, t) = 2\),枚举路径中点 \(x\),那么以 \(x\) 为根,\(x\) 的所有子树中,恰好有一棵子树,其中所有点都是 \(A\) 类点,恰好有一棵子树,其中所有点都是 \(B\) 类点。其他点都是 \(C\)类点。

时间复杂度 \(O(n)\),结合算法一期望得分 \(39\)

算法三

我会基环树且 \(s,t\) 都在环上!

设环长为 \(l\)。若 \(l\) 是奇数,那么环上有长度为 \(\frac{l−1}{2}\) 的一段子树都是 \(A\) 类点,有长度为 \(\frac{l−1}{2}\) 的一段子树都是 \(B\) 类点,恰好有一棵子树是 \(C\) 类点。

\(l\) 是偶数,有两种情况。
第一种情况是环上有长度为 \(\frac{l}{2}\) 的一段子树都是 \(A\) 类点,有长度为 \(\frac{l}{2}\) 的一段子树都是 \(B\) 类点,没有 \(C\) 类点。

第二种情况是环上有长度为 \(\frac{l}{2}−1\) 的一段子树都是 \(A\) 类点,有长度为 \(\frac{l}{2}−1\) 的一段子树都是 \(B\) 类点,两个区间之间有两棵子树是 \(C\) 类点。

枚举选的 \(A\) 类点子树区间然后讨论 \(C\) 类点子树的位置即可确定 \(B\) 类点子树区间。
时间复杂度 \(O(n)\),结合算法一和算法二期望得分 \(56\)

算法四

我会基环树!

\(s,t\) 的路径不经过环,用树的做法即可。
\(s,t\) 的路径经过环,且 \(s\)\(t\) 都不在环上,那么让 \(s\)\(t\) 分别朝向对方移动一步,所有点的类别不变。所以只用考虑 \(s\)\(t\) 至少有一个在环上的情况。

若在环上的点为 \(t\),设 \(s\) 在环上点 \(x\) 的子树内,那么需要满足 \(s\)\(x\) 的最短路小于等于 \(t\)\(x\) 的最短路,否则同样可以通过移动归约为 \(s,t\) 的路径不经过环的情况。反之亦然。

发现此时点的类别的情况和 \(s,t\) 都在环上的大致相同。设环长为 \(l\)
\(l\) 是奇数,那么环上有长度为 \(k (1 \le k \le l − 2)\) 的一段子树都是 \(A\) 类点,有长度为 \(l − 1 − k\) 的一段子树都是 \(B\) 类点,恰好有一棵子树是 \(C\) 类点。

\(k \le l − 1 − k\)(如上图),那么 \(t\) 在环上。我们需要在 \(A\) 类子树找一个到环的距离 \(=\frac{l−1−2k}{2}\) 的点(可以用 ST 表查询区间最大值位置求出在哪棵子树内)。然后根据 \(s\) 在哪棵子树内就可以确定 \(t\) 的位置。
\(k > l − 1 − k\) 的情况无非就是变成 \(s\) 在环上,交换一下 \(A,B\) 再做一遍即可。

\(l\) 是偶数,有两种情况。
第一种情况是环上有长度为 \(k (1 \le k \le l − 1)\) 的一段子树都是 \(A\) 类点,有长度为 \(l − k\) 的一段子树都是 \(B\) 类点,没有 \(C\) 类点。

\(k \le l − k\)(如上图),那么 \(t\) 在环上。我们需要在 \(A\) 类子树找一个到环的距离 \(=\frac{l−2k}{2}\) 的点。\(k > l − k\) 的情况大致相同。

第二种情况是环上有长度为 \(k (1 \le k \le l − 1)\) 的一段子树都是 \(A\) 类点,有长度为 \(l − 2 − k\) 的一段子树都是 \(B\) 类点,两个区间之间有两棵子树是 \(C\) 类点。

\(k \le l − 2 − k\)(如上图),那么 \(t\) 在环上。我们需要在 \(A\) 类子树找一个到环的距离 \(=\frac{l−2−2k}{2}\) 的点。\(k > l − 2 − k\) 的情况大致相同。

以上情况都可以枚举 \(A\) 类子树区间的左端点然后双指针出第一个满足区间子树和 \(\ge A\)(这个 \(A\) 是题面给的 \(A\)) 的右端点,讨论 \(C\) 类点子树的位置即可确定 \(B\) 类点子树区间,从而算出 \(t\) 的位置。

不过有一种特殊情况。
不妨设在环上的点为 \(t\),设 \(s\) 在环上点 \(x\) 的子树内,则特殊情况为 \(t\)\(x\) 的最短路恰好等于 \(s\)\(x\) 的最短路。此时环上恰好有一段 \(C\) 类点和一段 \(B\) 类点。特别地,\(x\)\(s\) 方向的子树是 \(A\) 类点,\(x\) 的其他子树是 \(C\) 类点。

此时我们去双指针 \(B\) 类点子树区间,并讨论 \(x\) 是在这个区间的左边还是右边,设这个区间长度为 \(k\),环长为 \(l\)。那么 \(s\)\(x\) 的距离需要等于 \(x\)\(t\) 的距离也就是 \(k−\lfloor \frac{l−1}{2} \rfloor+1\)(注意这个时候得 check 一下 \(x\) 是否真的有大小为 \(A\) 的子树)。

时间复杂度 \(O(n \log n)\),结合算法一和算法二期望得分 \(77\)

算法五

我会正解!

考虑直接把树和基环树的想法结合起来。
先考虑 \(dis(s, t) = 1\)\(s, t\) 不在同一个环的情况。再考虑 \(dis(s, t) = 2\) 且若设 \(x\)\(s\)\(t\) 路径中点则 \(s, x, t\) 都不在同一个环内的情况。

接下来的想法和基环树类似。若 \(s,t\) 两个点都不在环上,那么让 \(s\)\(t\) 分别朝向对方移动一步,所有点的类别不变。
所以枚举一个环使得 \(s, t\) 至少有一个在这个环上。不妨设 \(t\) 在环上,\(x\)\(s\) 到这个环的必经点,那么需要满足 \(s\)\(x\) 的最短路小于等于 \(t\)\(x\) 的最短路。

对原图建出圆方树后,我们需要求出每个点 \(u\) 子树内(记作 \(d_1\))和子树外(记作 \(d_2\))所有点到 \(u\) 的最短路的长度最大值。可以通过换根 DP 求出。
换根时,圆点向方点的转移是简单的,只需要处理一下儿子的 \(d_1\) 的前缀和后缀 \(\max\) 即可快速转移。
在方点向圆点转移时,如果这个方点代表的环上的点是 \(u_1,u_2,...,u_l\) 要转移的原点是 \(u_i\),相当于是说我们要求 \(\max_{j\ne i} (min(|i − j|, l − |i − j|)+d(a_j))\),其中 \(d(u)\) 表示点 \(u\) 在环以外的子树的最大深度(根据他的位置可能是 \(d_1\) 也可能是 \(d_2\))。用 ST 表维护区间 \(d(a_j)+j\)\(d(a_j)-j\) 的最大值,转移时根据 \(|i − j|\)\(l − |i − j|\) 的关系分成两部分转移即可。

接下来的讨论和算法四的部分没有区别,故不赘述。
时间复杂度 \(O(n \log n)\),期望得分 \(100\) 分。

点击查看代码
#include<bits/stdc++.h>
#define Debug puts("-------------------------")
#define eb emplace_back
#define P pair<pair<int,pair<int,int>>,int> 
#define PII pair<int,int>
#define fi first
#define se second 
#define mk make_pair
#define None mk(mk(0,mk(0,0)),0)
using namespace std;
const int N=2e5+5,M=4e5+5,inf=0x3f3f3f3f;

inline int read(){
	int w=1,s=0;
	char c=getchar();
	for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
	for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
	return w*s;
}
int T,n,m,A,B,s,t,tot,head[N],to[M<<1],Next[M<<1];
void add(int u,int v){ to[++tot]=v,Next[tot]=head[u],head[u]=tot; }
int c[N],dfn[N],low[N],siz[N<<1],cnt,num,sta[N],top;
int nxt[N],id[N],real_siz[N],real_dep[N];  //每个点在环上的后继,编号,往外的子树的大小以及最大深度
vector<int> G[N<<1];
void tarjan(int u){
	dfn[u]=low[u]=++num,sta[++top]=u;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]){
				++cnt;
				vector<int> vec;
				int x;
				do{
					siz[cnt]++;
					x=sta[top--];
					vec.eb(x);
					G[x].eb(cnt),G[cnt].eb(x);
				}while(x!=v);
				siz[cnt]++;
				G[u].eb(cnt),G[cnt].eb(u);
				if(siz[cnt]>=3){
					int lst=u;
					c[u]=cnt;
					for(int x:vec) c[x]=cnt,nxt[x]=lst,lst=x;
					nxt[u]=lst;
				}
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
int fa[N<<1],Size[N<<1],mxdep[N<<1];  
void dfs(int u){   
	if(fa[u]) G[u].erase(find(G[u].begin(),G[u].end(),fa[u]));
	if(u>n&&siz[u]>=3){
		id[fa[u]]=0;
		for(int x=fa[u];nxt[x]!=fa[u];x=nxt[x]) id[nxt[x]]=id[x]+1;
		sort(G[u].begin(),G[u].end(),[&](int x,int y){return id[x]<id[y];});
	} 
	
	Size[u]=(u<=n)?1:0,mxdep[u]=0;
	int v_A=0,v_B=0;
	for(int v:G[u]){
		fa[v]=u;
		dfs(v);
		Size[u]+=Size[v];
		if(u<=n) mxdep[u]=max(mxdep[u],mxdep[v]);
		else if(siz[u]==2) mxdep[u]=mxdep[v]+1;
		else mxdep[u]=max(mxdep[u],mxdep[v]+min(id[v],siz[u]-id[v]));
		if(u<=n&&siz[v]==2){
			if(Size[v]==A&&!v_A) v_A=G[v][0]; 
			if(Size[v]==B) v_B=G[v][0]; 
			if(Size[v]==B&&n-Size[v]==A) s=u,t=G[v][0];
			if(Size[v]==A&&n-Size[v]==B) t=u,s=G[v][0];
		}
	}
	
	if(u>n&&siz[u]>=3){
		real_siz[fa[u]]=n-Size[u];
		for(int v:G[u]) real_siz[v]=Size[v],real_dep[v]=mxdep[v];
	}
	
	if(u<=n&&siz[fa[u]]==2){
		if(n-Size[u]==A&&!v_A) v_A=fa[fa[u]]; 
		if(n-Size[u]==B) v_B=fa[fa[u]]; 
	}
	if(v_A&&v_B&&v_A!=v_B) s=v_A,t=v_B;
}
int mxdep2[N<<1],pre[N],suf[N];
struct ST{
	PII st[20][N<<1];
	int len;
	void Insert(int x){++len,st[0][len]={x,len};} 
	void Init(){
		for(int t=1;t<=__lg(len);t++){
			for(int i=1;i+(1<<t)-1<=len;i++){
				st[t][i]=max(st[t-1][i],st[t-1][i+(1<<(t-1))]);
			}
		}
	}
	PII RMQ(int l,int r){
		if(l>r) return {-inf,0};
		int k=__lg(r-l+1);
		return max(st[k][l],st[k][r-(1<<k)+1]);
	}
}st1,st2;
void dfs2(int u){  //换根 dp 求子树外的最大深度 
	if(!G[u].size()) return;
	if(u<=n){ //原点向方点贡献,求出儿子的 mxdep 的前缀 max 和后缀 max 就可以快速转移了 
		for(int i=0;i<G[u].size();i++){
			pre[i]=mxdep[G[u][i]];
			if(i) pre[i]=max(pre[i],pre[i-1]);
		}
		suf[G[u].size()]=0;
		for(int i=G[u].size()-1;i>=0;i--) suf[i]=max(mxdep[G[u][i]],suf[i+1]);
		for(int i=0;i<G[u].size();i++){
			int v=G[u][i];
			if(siz[v]==2)  mxdep2[v]=max({mxdep2[u],(i==0)?0:pre[i-1],suf[i+1]})+1;
			else{
				mxdep2[v]=max({mxdep2[u],(i==0)?0:pre[i-1],suf[i+1]});
				real_dep[u]=mxdep2[v];
			}
		}
		for(int v:G[u]) dfs2(v); //注意都更新完了再往下递归,否则 pre,suf 被改掉了 
	}
	else{  
	/*
		方点向原点贡献:环上的点 u 对点 v 的贡献是 mxdep[u]+min(|id[v]-id[u]|,len-|id[v]-id[u]|),
		所以用 ST 表维护区间 mxdep[u]+id 和 mxdep[u]-id 的最大值即可 
	*/	
		if(siz[u]>=3){
			int len=siz[u];
			st1.len=st2.len=0;
			for(int v:G[u]) st1.Insert(mxdep[v]+id[v]),st2.Insert(mxdep[v]-id[v]);
			st1.Insert(mxdep2[u]+len),st2.Insert(mxdep2[u]-len);
			for(int v:G[u]) st1.Insert(mxdep[v]+id[v]+len),st2.Insert(mxdep[v]-(id[v]+len));  //复制两遍 
			st1.Insert(mxdep2[u]+2*len),st2.Insert(mxdep2[u]-2*len);
			st1.Init(),st2.Init();
			for(int v:G[u]){
				int l=id[v]+1,r=id[v]+len-1,mid=(len+2*id[v])/2;
				mxdep2[v]=max(st1.RMQ(l,mid).fi-id[v],st2.RMQ(mid+1,r).fi+len+id[v]);
			}
			for(int v:G[u]) dfs2(v);  //注意都更新完了再往下递归,否则 st 表被改掉了 
		}
		else mxdep2[G[u][0]]=mxdep2[u],dfs2(G[u][0]);
	}
}
int have_subtree(int u,int S,int d){ //check u 在环外是否有大小 =S,深度超过 d 的子树 
	if(siz[fa[u]]==2&&n-Size[u]==S&&mxdep2[fa[u]]>=d) return fa[fa[u]];
	for(int v:G[u]){
		if(siz[v]==2&&Size[v]==S&&mxdep[v]>=d) return G[v][0];
	}
	return 0;
}
namespace Solve{
	ST st;
	int A,B,a[N<<1],siz[N<<1],dep[N<<1],n,len,S;
	void Init(int x,int numA,int numB){
		A=numA,B=numB,n=S=0;
		int u=x;
		do{
			a[++n]=u,siz[n]=real_siz[u],dep[n]=real_dep[u],S+=siz[n];
			u=nxt[u];
		}while(u!=x);
		len=n;
		for(int i=1;i<=len;i++) a[++n]=a[i],siz[n]=siz[i],dep[n]=dep[i];
		st.len=0;
		for(int i=1;i<=n;i++) st.Insert(dep[i]);
		st.Init();
	}
	int dist(int x,int y){return min(y-x,x+len-y);}
	P solve1(){  //环长是奇数 
		for(int l=1,r=1,sum=0;l<=len;l++){
			while(r<=l+len-1&&sum<A) sum+=siz[r],r++;  //注意 [l,r) 左闭右开 
			int k=r-l,d=(len-2*k-1)/2;  //k 算出来的是 A 区间的点的个数 
			PII mx=st.RMQ(l,r-1);
			int x=mx.se,t=x+2*(r-x)+d;
			if(sum!=A||2*k>len-1||mx.fi<d){sum-=siz[l]; continue;} 
			if(S-sum-siz[r]==B&&d<dist(x,t)){  //C 类点是 r 
				return {{a[x],{d,-1}},a[t]};
			} 
			t--;
			if(S-sum-siz[l+len-1]==B&&d<dist(x,t)){ //C 类点是 l-1 
				return {{a[x],{d,-1}},a[t]};
			}
			sum-=siz[l];
		}
		return None;
	}
	P solve2(){	//环长是偶数,没有 C 类点 
		for(int l=1,r=1,sum=0;l<=len;l++){
			while(r<=l+len-1&&sum<A) sum+=siz[r],r++;
			int k=r-l,d=(len-2*k)/2;
			PII mx=st.RMQ(l,r-1);
			int x=mx.se,t=r+(d+(r-x)-1);
			if(sum!=A||2*k>len||mx.fi<d){sum-=siz[l]; continue;} 
			if(S-sum==B&&d<dist(x,t)){  
				return {{a[x],{d,-1}},a[t]};
			} 
			sum-=siz[l];
		}
		return None;
	}
	P solve3(){	//环长是偶数,有 C 类点 
		for(int l=1,r=1,sum=0;l<=len;l++){
			while(r<=l+len-1&&sum<A) sum+=siz[r],r++;
			int k=r-l,d=(len-2*k-2)/2;
			PII mx=st.RMQ(l,r-1);
			int x=mx.se,t=r+(d+(r-x));
			if(sum!=A||2*k>len-2||mx.fi<d){sum-=siz[l]; continue;} 
			if(S-sum-siz[r]-siz[l-1+len]==B&&d<dist(x,t)){  
				return {{a[x],{d,-1}},a[t]};
			} 
			sum-=siz[l];
		}
		return None;		
	}
	P solve4(){ //特殊情况,此时需要双指针 B 类区间 
		for(int l=1,r=1,sum=0;l<=len;l++){
			while(r<=l+len-1&&sum<B) sum+=siz[r],r++;
			if(sum!=B){sum-=siz[l]; continue;} 
			int y=(len-1)/2;
			// x 是 l-1 
			int t=r-1-y,x=l-1+len,d=dist(l,t)+1;
			if(t>=l&&d>0&&have_subtree(a[x],A,d)){
				return {{a[x],{d,A}},a[t]};
			}
			
			// x 是 r 
			t=l+y,x=r,d=dist(t,x);
			if(t<r&&d>0&&have_subtree(a[x],A,d)){
				return {{a[x],{d,A}},a[t]};
			}
			sum-=siz[l];
		}
		return None;
	}
	P work(){  //返回值 {{x,{d,size}},y} 表示 s 是 x 大小是 size 的子树内深度为 d 的点,t 是 y  
		P res=solve4();
		if(res!=None) return res;
		if(len&1) return solve1();
		else{
			res=solve2();
			if(res==None) res=solve3();
			return res;
		}
	}
};
bool vis[N];
int find(int x,int d,int S){  //BFS 找要求的点 
	for(int i=1;i<=n;i++) vis[i]=0;
	queue<PII> Q;
	if(S==-1) Q.push({x,0}),vis[x]=true;
	else{
		int y=have_subtree(x,S,d);
		Q.push({y,1}),vis[y]=true;
	}
	while(Q.size()){
		int u=Q.front().fi,dis=Q.front().se; Q.pop();
		if(dis==d) return u;
		for(int i=head[u];i;i=Next[i]){
			int v=to[i];
			if(vis[v]||c[v]==c[x]) continue;
			Q.push({v,dis+1}),vis[v]=true;
		}
	}
} 
void Init(){
	tot=num=s=t=0;
	for(int i=1;i<=n;i++) c[i]=head[i]=dfn[i]=low[i]=0;
	for(int i=1;i<=cnt;i++) G[i].clear(),siz[i]=0,mxdep2[i]=0;
}
void work(){
	Init();
	
	n=read(),m=read(),A=read(),B=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read();
		add(u,v),add(v,u);
	}
	
	cnt=n;
	for(int i=1;i<=n;i++) if(!dfn[i]) top=0,tarjan(i);
	dfs(1); if(s) return;
	dfs2(1);
	
	for(int i=n+1;i<=cnt;i++){
		if(siz[i]==2) continue;
		bool stater=false;
		Solve::Init(fa[i],A,B);
		P res=Solve::work();
		if(res==None){
			stater=true;
			Solve::Init(fa[i],B,A);
			res=Solve::work();
		}
		if(res==None) continue;
		s=find(res.fi.fi,res.fi.se.fi,res.fi.se.se),t=res.se;
		if(stater) swap(s,t);
		return;
	} 
}
signed main(){
//	freopen("recallrevenge.in","r",stdin);
//	freopen("recallrevenge.out","w",stdout);
	T=read();
	while(T--) work(),printf("%d %d\n",s,t);
	return 0;
}
posted @ 2025-06-05 10:36  Green&White  阅读(110)  评论(0)    收藏  举报