day 10 图论选讲之欧拉回路
图论选讲之欧拉回路
欧拉回路
定义:
- 欧拉路径:经过图中每条边恰好一次的路径(点可以重复经过)。
- 欧拉回路:起点和终点相同的欧拉路径(也是每条边恰好一次)。
存在条件:
- 图必须连通。
- 对于有向图,若所有点的入度出度都相同,则存在欧拉回路;如果有两个点,其中一个入度比出度少 1,另一个入度比出度多 1,那么存在一个欧拉路径,这两个点分别为路径的起点和终点。
- 对于无向图,若所有点的度都为偶数,则存在欧拉回路;若奇点(度数为奇数的点)个数为 0 或 2,那么存在一个欧拉路径,奇点为 2 的点分别为这个路径的起点和终点。
求法:Hierholzer算法
有向图(P7771 【模板】欧拉路径)
从一个度数为奇数的点开始 dfs,每次转移,要把走的边删掉,最后倒序输出。
注意删边的处理,可以开数组记录每个点已经处理了前几条边,跳过它们。(开一个数组 del[u] 记录节点 u 的邻接表当前遍历到的位置,每次访问一条边后,del[u]++,确保每条边只被扫描一次,保证复杂度正确)
如果想得到字典序最小的欧拉序,在 DFS 前将每个点的邻接表按终点编号从小到大排序即可。
void dfs(int u) {
// del[u] 是节点 u 的邻接表“当前待处理位置”指针(从 0 开始)
// 循环条件:i 从 del[u] 开始,直到邻接表末尾
// 每次循环后 i 更新为 del[u] 的新值(因为 del[u] 会在循环体内修改)
for (int v : g[u]) {
// 关键:立即将 del[u] 后移一位,表示“这条边已经被尝试过了”
// 这样无论后续是否递归或跳过,下次循环都不会再访问这条边
del[u] = i + 1;
// 递归进入目标节点,继续深度优先搜索
dfs(v);
}
// 当 u 的所有邻接边都处理完后,将 u 压入栈
// 这是后序入栈,最终栈从栈底到栈顶是逆序的欧拉路径
st.push(u);
}
无向图(P2731 骑马修栅栏)
与刚刚不同的是,无向图的边有两个方向,我们不知道这条边的另一个方向之前走没走过,所以需要对边进行编号,然后开一个 used 数组,表示每条边是否已经被走过。每次走边的时候要标记正反两个方向的边都走过,有向图有重边的情况也可以这样处理。
void dfs(int u) {
for (int i = del[u]; i < g[u].size(); i = del[u]) {
// 取出当前边:g[u][i] 是一个 pair<int,int>,first 是邻接点,second 是边的编号
int v = g[u][i].first;
int id = g[u][i].second;
del[u] = i + 1;
// 如果这条边已经被使用过(正反任一次),则直接跳过
if (used[id]) continue;
// 标记这条无向边的正反两个方向为已使用
// 无向边在邻接表中存了两份,且编号成对(id 和 id^1 互为反向)
used[id] = used[id ^ 1] = 1;
dfs(v);
}
st.push(u);
}
下面看一些例题。
T4 P1341 无序字母对
给定 n 个各不相同的无序字母对(区分大小写,无序即字母对中的两个字母可以位置颠倒)。请构造一个有 (n+1) 个字母的字符串使得每个字母对都在这个字符串中出现。(每对的两个字母在字符串中相邻)要求线性。
不同的字母相当于点,字母对相当于一条连接两个字母的无向边,因为走一条边就相当于走了无序字母对的两个字母。
要让所有无序字母对出现,就相当于要走所有边,跑一遍欧拉路径即可,正好得到走过的所以字母,注意要先判是否有解,上面讲过了。
#include<bits/stdc++.h>
using namespace std;
const int N=505,M=2048;
int n,m;
vector<pair<int,int>>g[N];
bool used[M];
stack<int>st;
int du[N];
void dfs(int u){
for(auto &e:g[u]){
int v=e.first,id=e.second;
if(used[id])continue;
used[id]=used[id^1]=1;
dfs(v);
}
st.push(u);
}
int main(){
cin>>m;n=200;
int cnt=0;int s=10000;
for(int i=1;i<=m;i++){
char u,v;cin>>u>>v;
g[u].push_back({v,cnt++});
g[v].push_back({u,cnt++});
du[u]++;du[v]++;
s=min(s,min((int)u,(int)v));
}
for(int i=1;i<=n;i++)sort(g[i].begin(),g[i].end());
int flag=0;
for(int i=1;i<=n;i++){
if(du[i]&1){
if(!flag)s=i;
flag++;
}
}
if(flag!=0&&flag!=2){
cout<<"No Solution";
return 0;
}
dfs(s);
while(!st.empty()){
cout<<(char)st.top()<<"";
st.pop();
}
return 0;
}
T6 [POI2011] SMI-Garbage
给定一张 n 个点 m 条边的无向图,每条边居黑白二色之一,且有一个黑或白的目标颜色。
每次操作,能把一个环反色。
请给出一个合法的方案,使得每条边最终都变为目标颜色,或判定不可行。
\(n\le 10^5,m\le10^6\)
显然,有些边需要边颜色,有些不需要,可以证明一条边变换 1 次以上颜色不是最优的,所以每条边只需要变换 0/1 次颜色,我们把初始颜色和目标颜色不同的边拿出来,这些边就是我们要走的。
问题转化为对这张图进行若干次对环反色的操作,使每条边经过一次。
注意到一个环的首尾点相同,剩下所以点出现一次,所以我们在求欧拉回路的 dfs 中,不断走到的点栈中是否有,如果有,把这中间的元素全弹出来,这些元素构成一个环。
#include<bits/stdc++.h>
using namespace std;
const int N=200005,M=4000005;
int n,m;
vector<pair<int,int>>g[N];
bool used[M];
stack<int>st;
int du[N];
int vis[N];
vector<int>ans[N];
int cnt;
void dfs(int u){
while(!g[u].empty()){
// 这里运用了直接删边的方式
auto [v,id]=g[u].back();
g[u].pop_back();
if(used[id])continue;
used[id]=used[id^1]=1;
dfs(v);
}
st.push(u);
if(vis[u]){
cnt++;
st.pop();
ans[cnt].push_back(u);
while(st.top()!=u){
ans[cnt].push_back(st.top());
vis[st.top()]=0;
st.pop();
}
ans[cnt].push_back(u);
}
vis[u]=1;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>m;
int id=0;
for(int i=1;i<=m;i++){
int u,v,s,t;
cin>>u>>v>>s>>t;
if(s==t)continue;
g[u].emplace_back(v,id++);
g[v].emplace_back(u,id++);
du[u]++;du[v]++;
}
int ji=0;
for(int i=1;i<=n;i++){
if(du[i]&1)ji++;
}
if(ji!=0){
cout<<"NIE\n";
return 0;
}
for(int i=1;i<=n;i++){
if(!vis[i])dfs(i);
}
cout<<cnt<<'\n';
for(int i=1;i<=cnt;i++){
cout<<ans[i].size()-1<<' ';
for(int j:ans[i])cout<<j<<' ';
cout<<'\n';
}
return 0;
}
T9 CF209C Trails and Glades
给定一张 n 个点 m 条边的无向图,求最少添加多少条边后,使得图存在从1号点出发又回到1号点的欧拉回路。
回忆欧拉回路的判断条件:若所有点的度都为偶数,则存在欧拉回路。显然我们需要把所以奇点都变成度数为偶数的点。
假设图是连通的,那么设奇点数量为 num,那么我们可以连 num/2 条边把他们相互连接,使得他们的度都变为偶数。
那么如果图不连通,对于每个连通块:
- 如果这个连通块中没有边,且没有结点 1(因为是从结点 1 开始走的,需要把结点 1 与其他连通块联络),那么这个连通块可以不管,贡献为 0。
- 否则如果这个连通块有奇点,那么可以把奇点与其他连通块的奇点相连,需要建
num/2条边。 - 特殊地,如果没有奇点,但是我们还是要和其他连通块联络的,我们只能选任意一个点,与其他连通块相连,贡献为 1。
都加起来就做完了。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m;
int du[N];
bool vis[N];
int cnt,a[N];
vector<int>g[N];
vector<int>k[N];
bool e[N];
int ji[N];
void dfs(int u){
a[u]=cnt;
k[cnt].push_back(u);
vis[u]=1;
for(int v:g[u]){
e[cnt]=1;
if(vis[v])continue;
dfs(v);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v;cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
du[u]++;du[v]++;
}
for(int i=1;i<=n;i++){
if(!vis[i]){
cnt++;
dfs(i);
}
}
int x=0;
for(int i=1;i<=cnt;i++){
if(a[1]==i||e[i])x++;
}
for(int i=1;i<=cnt;i++){
for(int j:k[i]){
if(du[j]%2)ji[i]++;
}
}
if(x==1)cout<<ji[1]/2;
else{
int ans=0;
for(int i=1;i<=cnt;i++){
if(!(a[1]==i||e[i]))continue;
ans+=ji[i]/2;
if(ji[i]==0)ans++;
}
cout<<ans;
}
return 0;
}
T12 CF2110E Melody
机器人拥有一种特殊的乐器,能够产生 n 种不同的声音。每种声音由其音量和音高来表征。如果任意两个连续的声音仅在音量或仅在音高上有所不同,则该音乐被认为是优美的。如果存在三个连续的声音在音量或音高上相同,则该音乐被认为是单调的。你需要创作一段优美且不单调的音乐,其中包含乐器产生的每个声音恰好一次。\(n\le 2\times 10^5,v_i,p_i\le10^9\)。
两个连续声音在一方面不同、三个连续声音在一方面不同,这两个性质就相当于两个方面交替变化,即如果第一、二个声音音量不同,那么第二、三个声音必定音调不同,那么第三、四个声音必定音量不同……以此类推。
那么,一个声音可以看作一个 \((v,p)\) 的连边,注意边无向,那么一段优美且不单调的音乐,就是图上的一个路径,因为这条路径上的 v,p 交替改变。
所以,在这个无向图上找一个欧拉路径,就是每个声音出现一次且满足要求的答案。
细节:要先对 \(v_i,p_i\) 离散化,注意要一起离散化,否则可能出现一个 v 和一个 p 在图上的信息相同。
#include<bits/stdc++.h>
using namespace std;
const int N=400005;
int n,m,t;
vector<pair<int,int>>g[N];
int deg[N];
bool vis[N];
struct node{
int x,y,id;
}v[N],p[N];
bool cmp(node a,node b){
return a.x<b.x;
}
bool cmp2(node a,node b){
return a.id<b.id;
}
bool used[N*2];
int a[N*2];
stack<int>st;
void dfs(int u){
while(!g[u].empty()){
auto pr=g[u].back();
g[u].pop_back();
int v=pr.first;
int id=pr.second;
if(used[id])continue;
used[id]=used[id^1]=1;
dfs(v);
st.push(a[id]);
}
}
void dfs2(int u){
vis[u]=1;
for(auto pr:g[u]){
int v=pr.first;
if(!vis[v])dfs2(v);
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>t;
while(t--){
cin>>m;
for(int i=1;i<=m;i++){
cin>>v[i].x>>p[i].x;
v[i].id=p[i].id=i;
}
sort(v+1,v+m+1,cmp);
int cnt1=0;
for(int i=1;i<=m;i++){
if(i==1||v[i].x!=v[i-1].x)cnt1++;
v[i].y=cnt1;
}
sort(p+1,p+m+1,cmp);
int cnt2=0;
for(int i=1;i<=m;i++){
if(i==1||p[i].x!=p[i-1].x)cnt2++;
p[i].y=cnt2;
}
sort(v+1,v+m+1,cmp2);
sort(p+1,p+m+1,cmp2);
int tot=cnt1+cnt2;
for(int i=1;i<=tot;i++){
g[i].clear();
deg[i]=vis[i]=0;
}
while(!st.empty())st.pop();
int cnt=1;
for(int i=1;i<=m;i++){
int u=v[i].y;
int w=cnt1+p[i].y;
int id1=++cnt;
int id2=++cnt;
g[u].push_back({w,id1});
g[w].push_back({u,id2});
a[id1]=a[id2]=i;
deg[u]++;
deg[w]++;
}
int s=-1;
int odd=0;
for(int i=1;i<=tot;i++){
if(deg[i]==0)continue;
if(deg[i]&1){
odd++;
s=i;
}
}
if(odd>2){
cout<<"NO\n";
continue;
}
if(s==-1){
for(int i=1;i<=tot;i++){
if(deg[i]>0){
s=i;
break;
}
}
}
fill(used,used+cnt+2,0);
dfs2(s);
bool ok=1;
for(int i=1;i<=tot;i++){
if(deg[i]&&!vis[i]){
ok=0;
break;
}
}
if(!ok){
cout<<"NO\n";
continue;
}
dfs(s);
if(st.size()!=m){
cout<<"NO\n";
continue;
}
cout<<"YES\n";
while(!st.empty()){
cout<<st.top()<<" ";
st.pop();
}
cout<<"\n";
}
return 0;
}
浙公网安备 33010602011771号