「省选联考2025-图排列」题解
P11832 [省选联考 2025] 图排列
sol
题意翻译
给出一个图,要求你将图上所有点排成一个排列,使得所有边不相交。最小化这个序列的字典序。大致就是这个意思。
树部分
考虑顺序构造答案序列,每次尽量加入更小的值。
考虑一棵树,不难发现,其各个子节点为根的子树,必然各是答案序列中一个连续段,否则必然有边相交,这很显然。
考虑根节点,如果我们每次以编号最小的为根,那么根节点必然位于整个序列最前面。
再考虑一棵子树,这棵子树的根节点必然与其父节点有一条连边,那么这个根节点就不能放在以其任何子节点为根的子树内部,否则根与父亲的连边必然与那个子树内的边相交。
各子节点为根子树之间是任意排列的,我们考虑预处理出每棵子树可以放在最前面的最小值,对每个节点,将其所有子节点为根子树按这个预处理出的最小值排好序即可。考虑这个节点本身放在哪里,可以直接用其自身的值和所有子节点一起排序,轮到自己的时候把自己入队即可。
图部分
图相对于树而言其实就是多了环的存在。
考虑一个环所具有的性质,放到答案序列上,如果只考虑这个环上的点,那么就是相邻两个点依次相连,最后首尾相连,从而使得边无交。
如果我们以环中一个节点为“根节点”,那么剩下的点在序列上的位置顺序必然按环上顺时针或逆时针依次排布,而根节点可以位于环上所有点之前或之后,从而满足上述性质。
也就是说,每个环上各点之间存在特殊位置关系限制。但是一个图内的环这么复杂,这个限制难以实现。
这道题的图具有一个性质,一个点双内只存在一个极大简单环即哈密顿回路。考虑反证一下,倘若存在多个极大简单环,那么必然存在环内两条边相交的情况,形似 \(\oplus\) 状(借鉴这篇文章的形象比喻),那么必然无解,而题目保证有解。所以每个点双都是广义串并联图。
而倘若极大简单环符合条件,可能存在的小环显然也会符合条件。
因此,其实就是每个点双内的各点之间存在特殊位置关系限制。
如何快速处理点双的限制呢?考虑圆方树,将所有点双压成方点,那么一个方点的父节点与其所有子节点就对应了这个环上的所有点。
在这部分开始就给出的环的合法构造方法,其实是为了适配这一部分,我们将方点的父节点视作环的“根节点”,这样在边上的话,它向外的连边就不会与跨越整个环的那一条连边相交了。那么这个限制就变成,其所有子节点在序列上必须按环上顺时针或逆时针顺序排布。我们对比找出两种情况下能放在最前面的最小值,选更优的一种即可。
对于圆点,不难发现与之相连的各个点双同样满足答案序列上连续、互不相交、任意排列的性质,使用与树一样的方法处理即可。
考虑如何找点双内的极大简单环,经过上述推导我们已经得出一个点双必然是一个广义串并联图,我们使用广义串并联图方法,通过链表维护倒序加点即可。
多连通块
各个连通块之间相互独立,要保证没有边相交,那么两两之间,在答案序列上的对应位置要么完全无交,要么一个被另一个完全包括,不能有类似于飞地的情况。这是很显然的。
每次加入前判断一下有没有别的连通块可行最前最小值比即将加入的值更优,有的话处理完那个连通块再接着处理当前块即可。
code
const int N=2e5+5;
int n,m;
vec<int> g[N],G[N];
int nn,dcnt;
int dfn[N],low[N];
stack<int> stk;
void tarjan(int x){
dfn[x]=low[x]=++dcnt;stk.push(x);
for(auto y:g[x]){
if(!dfn[y]){
tarjan(y);
chmin(low[x],low[y]);
if(low[y]>=dfn[x]){
++nn;
while(1){
int t=stk.top();stk.pop();
G[nn].pub(t);G[t].pub(nn);
if(t==y)break;
}
G[nn].pub(x);G[x].pub(nn);
}
}else chmin(low[x],dfn[y]);
}
}
vec<int> rts;
vec<int> rnk[N];
int val[N];
set<int> st;
set<int> gg[N];
int d[N];
vec<int> e;
set<int> q;
bool del[N];
int ne[N];
inline void work(int x,int f){
st.clear();
for(auto i:G[x])st.insert(i),gg[i].clear(),d[i]=0,del[i]=0;
for(auto i:G[x])for(auto j:g[i])if(st.count(j))gg[i].insert(j),++d[i];
e.clear();q.clear();
for(auto i:G[x])if(d[i]==2)q.insert(i);
while(q.size()){
int x=*q.begin();q.erase(q.begin());
int a=*gg[x].begin(),b=*++gg[x].begin();
if(gg[a].count(b)){
if(d[a]==2)q.erase(a);
if(d[b]==2)q.erase(b);
if(--d[a]==2)q.insert(a);
if(--d[b]==2)q.insert(b);
}else gg[a].insert(b),gg[b].insert(a);
gg[a].erase(x),gg[b].erase(x);
e.pub(x);del[x]=1;
}
vec<int> res;
for(auto i:G[x])if(!del[i])res.pub(i);
ne[res[0]]=res[1],ne[res[1]]=res[0];
reverse(e.begin(),e.end());
for(auto x:e){
int a=*gg[x].begin(),b=*++gg[x].begin();
if(ne[a]==b)ne[x]=b,ne[a]=x;
else ne[x]=a,ne[b]=x;
}
res.clear();res.pub(ne[f]);
for(int i=ne[ne[f]];i!=f;i=ne[i])res.pub(i);
rnk[x]=res;
}
void init(int x,int f){
for(auto y:G[x])if(y!=f)init(y,x);
if(x<=n){
val[x]=x;rnk[x].pub(x);
for(auto y:G[x])if(y!=f)rnk[x].pub(y);
sort(rnk[x].begin(),rnk[x].end(),[&](int a,int b){return val[a]<val[b];});
val[x]=val[rnk[x].front()];
}else{
work(x,f);
if(val[rnk[x].front()]>val[rnk[x].back()])reverse(rnk[x].begin(),rnk[x].end());
val[x]=val[rnk[x].front()];
}
}
set<pii> can;
vec<int> ans;
void solve(int x){
for(auto i:rnk[x]){
while(can.size()&&can.begin()->fir<(i==x?x:val[i])){
int x=can.begin()->sec;can.erase(can.begin());
solve(x);
}
if(i==x)ans.pub(x);
else solve(i);
}
}
inline void Main(){
read(n,m);
rep(i,1,n<<1)g[i].clear(),G[i].clear();
rep(i,1,m){
int u,v;read(u,v);
g[u].pub(v);g[v].pub(u);
}
nn=n,dcnt=0;
rep(i,1,n)dfn[i]=0,val[i]=inf;
rts.clear();
rep(i,1,n)if(!dfn[i]){
while(stk.size())stk.pop();
tarjan(i);
rts.pub(i);
}
can.clear();ans.clear();
rep(i,1,nn)rnk[i].clear();
for(auto i:rts)init(i,0),can.insert({val[i],i});
while(can.size()){
int x=can.begin()->sec;can.erase(can.begin());
solve(x);
}
for(auto i:ans)put(i,' ');puts("");
}
aft
缝缝补补,修修改改,最后只能到这个样子了。只能说至少把要讲的都讲了,大致存在一个模糊的思维链,但语言确实很冗余繁琐。
代码写的也是常数巨大,很多地方根本无需使用 set,很多 vector stack 之类的 STL 完全可以使用数组实现。不管怎样,至少洛谷能过吧。
这道题的思维难度其实不大吧,思维链也比较顺畅(只要会广义串并联图之类一些小科技),实现难度也不很大。
那么……
……

浙公网安备 33010602011771号