[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;
}

浙公网安备 33010602011771号