题解:CF858F Wizard's Tour

本文在编写完成后使用了 Qwen3-Max 进行润色。

这是一道非常巧妙的好题。

我们考虑通过一次 DFS 来完成整个构造过程。

在 DFS 的过程中,我们可以自然地构建出一棵 DFS 树。对于树中除根节点外的任意一个节点 $ v $,其连出的边可以分为三类:

  • 父向边:连接 $ v $ 与其父节点;
  • 子向边:连接 $ v $ 与其子节点;
  • 返祖边:连接 $ v $ 与某个祖先(非父节点)。

关于返祖边的处理

本题的关键在于:我们不关心一个点被经过多少次,只关心每条边是否被使用恰好一次。因此,返祖边可以被“转化”为一条虚拟的子向边来处理(你可以理解为把祖先“拉下来”当作一个叶子儿子)。(祖宗:好好好,你小子把我薅下来当你儿子,还是个叶子?)

这样做是安全的,因为这个“假儿子”不会“夺舍”这条边——它没有其他出边可供匹配,所以这条边一定可以参与当前节点 $ v $ 的匹配


关于子向边的处理

对于真实的子节点,我们在递归处理完子树后,就能知道该子节点是否会“夺舍”它与 $ v $ 之间的那条边(即子节点是否已用这条边进行匹配)。

  • 如果子节点已经用了这条边,那么 $ v $ 就不能再用;
  • 否则,这条边对 $ v $ 是可用的。

关于父向边的决策

当我们处理完所有子向边和返祖边后,统计当前节点 $ v $ 可用于匹配的边数(不包括父向边)。

  • 如果这个数量是偶数,说明 $ v $ 已经能内部配对完毕,那么父向边就可以留给父节点使用;
  • 如果是奇数,则 $ v $ 会“多出”一条边无法配对,此时就将父向边“夺舍”过来,与这条多余的边配成一对。

这样自底向上贪心地构造,即可保证每条边恰好被使用一次,并且所有路径长度均为 2(即合法的“Wizard's Tour”)。


通过一次 DFS 即可完成全部匹配,时间复杂度为 $ O(n + m) $,简洁而高效。

(什么?你说我不给代码?受着)

代码

#include<bits/stdc++.h>
using namespace std;
constexpr int N=200005;
int n,m;
bitset<N> vis;
bitset<N> use;
struct edge{
	int v,i;
};
vector<edge> g[N];
stringstream ans;
int cnt;
bool dfs(int u,int f,int fi){
	int lzy=0,lzyi=0;
	vis[u]=1;
	for(auto [v,i]:g[u]){
		if(v==f||use[i])continue;
		bool enable=vis[v]?1:!dfs(v,u,i);
		if(enable){
			if(lzy){
				cnt++;
				ans<<lzy<<" "<<u<<" "<<v<<"\n";
				use[i]=use[lzyi]=1;
				lzy=lzyi=0;
			}else{
				lzy=v;lzyi=i;
			}
		}
	}
	if(lzy&&f){
		cnt++;
		ans<<lzy<<" "<<u<<" "<<f<<"\n";
		use[fi]=use[lzyi]=1;
		return 1;
	}else{
		return 0;
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int a,b;
		cin>>a>>b;
		g[a].push_back({b,i});
		g[b].push_back({a,i});
	}
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			dfs(i,0,0);
		}
	}
	cout<<cnt<<"\n"<<ans.rdbuf();
	return 0;
}
posted @ 2026-01-28 16:23  积分守恒  阅读(1)  评论(0)    收藏  举报