欧拉图

定义

欧拉回路:通过图中每条边恰好一次的回路(起点与终点在同一个点)。

欧拉路径:通过图中每条边恰好一次的通路。

欧拉图:具有欧拉回路的图。

半欧拉图:具有欧拉路径但不具有欧拉回路的图。

无向图的判定

连通无向图 \(G\) 有欧拉路径的充要条件为:

  • \(G\) 中奇点(度数为奇数的点)有 \(0\) 个或者 \(2\) 个。其中当奇点有 \(2\) 个时,欧拉路径的起点和终点分别为这两个点(半欧拉图)。有 \(0\) 个奇点时为欧拉图。

有向图的判定

底图连通的有向图 \(G\) 有欧拉路径的充要条件为:

  • \(G\) 的所有顶点入度和出度都相等。(欧拉图)

  • 或者只有两个顶点的入度和出度不相等,且其中一个顶点的出度与入度之差为 \(1\),另一个顶点的入度与出度之差为 \(1\)。这时欧拉路径的起点为前者,终点为后者。(半欧拉图)

求有向图的欧拉路径

题目链接

先考虑不存在欧拉路径的情况,我们需要统计每个点的入度和出度,然后判断上面定理的条件就行,如果发现不符合直接输出 No

先确定起点,沿用我们刚刚用来判无解的入度和出度点,找到出度 \(-\) 入度为 \(1\) 的点,这个点就是起点。

接下来从起点开始搜索,然后选择一条没有走过的边走,继续搜索下一个点。每次返回的时候,将点入栈,最后的答案就是栈顶到栈底。

由于题目让我们求字典序最小的欧拉路径,为了保证字典序最小,我们每次走的时候尽量选择连向较小的结点那条边。又因为链式前向星存点的顺序与遍历的顺序相反,所以我们只需要把输入的所有边 u->v\(v\) 从大到小排序再建图即可。

这个时候就会发现只能拿到 90pts,最后一个点会 TLE。我们被卡的地方就是因为只是用一个 bool 数组来标记一条边是否走过,这样每次还是要遍历所有的边,考虑最坏的情况,数据给出的是稠密图,每次要遍历很多条边,一个点又会走到很多次,时间复杂度能达到大约 \(O(n^2)\),就会被卡。所以我们考虑使用类似 dinic 中的当前弧优化来优化这一点。因为我们每次一定会取尽量小边,又因为一个点往外的边我们已经排好序,所以一个点被走的边的顺序是第 \(1\) 条、第 \(2\) 条、第 \(3\) 条……所以只需要在每次走过一条边的时候更新边的起点就好了嘛。这样一来我们就不用记录每条边有没有走过了,改变遍历的起点,每次遍历到的都是没走过的边。

第一次看链式前向星的当前弧优化是不太好理解的,所以可以先看看好理解的 vector 存图的当前弧优化:

for(int i=del[now];i<G[now].size();i=del[now])
{ 
	del[now]=i+1;
	dfs(G[now][i]);
}

实际上就是用了一个 \(del\) 数组来记录每个点遍历边的起点。

再来看看链式前向星的就好理解了:

for(int i=t[x];i;i=t[x])
{
	t[x]=a[i].last;
	dfs(a[i].id);
}

一开始我在 for 循环里面写了 i=a[i].last,这样是错误的,应该像上面一样是 i=t[x]。这两种我一开始看不出来什么区别,仔细想了下,我们在循环里面 dfs,可能在 dfs 的时候用掉了一些边,a[i].last 这条边就被用掉了,但是 t[x] 是实时更新的,一定是第一条没被走过的边。

讲一个我在学习这道题目一开始不理解的一个点:为什么要回溯时入栈,倒着输出,而不是边遍历边输出?

给一组 hack 数据:

3 4
1 2
2 1
2 3
3 2

后者跑出来的是 1 2 1 3 2,显然是错误的。

至于为什么,模拟一下可能就理解了。

边遍历边输出:

dfs(1):输出1 包含边2 
	dfs(2):输出2 包含边1 3
		dfs(1):输出1 没有边  
    	end
   		dfs(3):输出3 包含边2 
        	dfs(2):输出2 没有边 
            end
        end
    end
end

回溯时入栈,倒着输出:

dfs(1):包含边2
	dfs(2):包含边1 3
		dfs(1):没有边
		end s.push(1) 
		dfs(3):包含边2
			dfs(2):没有边
			end s.push(2)
		end s.push(3)
	end s.push(2)
end s.push(1) 

问题在于「没有边」。

如果没有边,说明这个点已经遍历完毕,应该在路径的末尾输出,但 dfs 时会先进入这个点,告知程序这个点是路径末端的。但如果采用第一种代码的写法,这个点会直接计入路径并输出,这并不是我们想要的结果。

因此,路径末端的点会首先被找到,最先结束递归,并入栈,表明在结束 dfs 时入栈就是正确的。

最后贴上这题的完整代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=1e5+100,M=2e5+100;
int n,m,k,t[N],d1[N],d2[N],st,cnt1,cnt2,s[N],sk;
bool flag[M]; 
struct edge
{
	int u,v;
}e[M];
struct node
{
	int id,last;
}a[M];
bool cmp(edge a1,edge a2)
{
	return a1.v>a2.v;
}
void add(int a1,int a2)
{
	a[++k].id=a2;
	a[k].last=t[a1];
	t[a1]=k;
}
void dfs(int x)
{
	//printf("%d ",x);
	for(int i=t[x];i;i=t[x])
	{
		t[x]=a[i].last;
		dfs(a[i].id);
	}
	s[++sk]=x;
} 
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++) 
	{
		scanf("%d%d",&e[i].u,&e[i].v);
		d1[e[i].v]++,d2[e[i].u]++;
	}
	sort(e+1,e+m+1,cmp);
	for(int i=1;i<=m;i++) add(e[i].u,e[i].v);
	for(int i=1;i<=n;i++)
	{
		if(d1[i]==d2[i]) continue;
		if(d2[i]-d1[i]==1) cnt1++,st=i;
		else if(d1[i]-d2[i]==1) cnt2++;
		else 
		{
			printf("No");
			return 0;
		}
	}
	if(!(!cnt1&&!cnt2||cnt1==1&&cnt2==1)) 
	{
		printf("No");
		return 0;
	}
	if(!st) st=1;
	dfs(st);
	for(int i=sk;i;i--) printf("%d ",s[i]);
	return 0;
}

时间复杂度 \(O(m+mlogm)\)

posted @ 2024-10-24 16:52  MinimumSpanningTree  阅读(147)  评论(0)    收藏  举报