欧拉路径
“一笔画”问题,对于有向图和无向图
基本概念
对于连通图:
- 欧拉路径:不重复地遍历完所有边
- 欧拉回路:经过欧拉路径回到起点 ,欧拉回路也是欧拉路径
- 半欧拉图:图中存在欧拉路径
- 欧拉图:图中存在欧拉回路,欧拉图也是半欧拉图
非连通图一定没有欧拉路径
判断
先判断是否是连通图
对于非起点和非终点,入这个点的边数需要等于出这个点的边数,才能保证每条边只走一次
无向图
- 存在欧拉路径的充要条件:图中度为奇数的点为0个(欧拉回路)或2个(起点和终点不同)
- 存在欧拉回路的充要条件:图中度为奇数的点为0个
有向图
- 存在欧拉路径的充要条件:图中所有点的出度等于入度(欧拉回路)或起点的出度等于入度加一,终点的入度等于出度加一,非起点和非终点的入度等于出度
- 存在欧拉回路的充要条件:图中所有点的入度等于出度
Hierholzer算法
首先找到一个子回路,并逐步将其他回路合并到该子回路中,最终形成完整的欧拉回路
有向图/无向图的欧拉路径/欧拉回路可以拆成从起点到终点的直线,欧拉路径不唯一
先找到一条不完整的子回路,确定终点,再排除这个点,回溯,找另一个回路,确定终点的上一个点,直到所有子回路找完
- 选择一个合理的点作为起点,开始dfs,对于欧拉图,找任一点都可以作为起点,对于非欧拉图的半欧拉图,找度为奇数的点(无向图)或出度等于入度加一的点(有向图)作为起点
- 遍历邻边,经过的边都标记,标记过的边不能访问
- 若当前遍历的节点没有可走的边,将此节点入栈
- 遍历完成后将栈边输出边出栈,得到欧拉路径

如图,每个回路中省略了其它节点
- 先找到回路1,节点1入栈,回溯到节点3
- 找到回路2,节点3入栈,回溯到节点4
- 找到回路3,节点4入栈,依次回溯到节点6,7,5,2,1,并入栈
- 最终确定1-->3-->4-->6-->7-->5-->2-->1的逆欧拉路径,逆序输出得到欧拉路径
实现
struct edge{
int u;
int next;
int uid; //边的编号
bool visited;
}edges[2*MaxE];
vector<int> ans1; //用点表示路径(逆序)
vector<int> ans2; //用边表示路径(逆序)
void dfs(int x){
int u;
for(int i=head[x];i!=-1;i=edges[i].next){
if(edges[i].visited)cintinue;
//若i是偶数,i^1=i+1,若i是奇数,i^1=i-1,根据存边的顺序,边和反向边是一奇一偶的关系,标记边需要将该边的反向边一起标记
edges[i].visited=edges[i^1].visited=true;
dfs(edges[i].u);
ans2.push_back(edges[i].uid); //由于dfs传参只传点,所以只能在下层dfs结束后收录边
}
ans1.push_back(x);
}
对于一个有x条边的点,这个点会被访问x/2次(一次进入一次出去),所以对于有e条边的图,整个图会被遍历e/2次,每访问一个节点,需要遍历所有邻边,所以总的时间复杂度为O(E*(E+V))
优化
每访问过一条边,直接将这条边删掉,就不用每个点都遍历所有邻边
struct edge{
int u;
int next;
int uid; //边的编号
bool visited;
}edges[2*MaxE];
vector<int> ans1; //用点表示路径(逆序)
vector<int> ans2; //用边表示路径(逆序)
void dfs(int x){
int u;
for(int i=head[x];i!=-1;i=head[x]){
if(edges[i].visited){ //第二次访问同一条边时执行删除
head[x]=edges[i].next; //头删
continue;
}
edges[i].visited=edges[i^1].visited=true;
//head[x]=edges[i].next 若在第一次访问后就删边,删不掉反向边,若是有向图可以在访问后直接删
dfs(edges[i].u);
ans2.push_back(edges[i].uid);
}
ans1.push_back(x);
}
时间复杂度O(E+V)
求字典序最小的欧拉路径
由于dfs是先遍历到的点后入栈,所以先遍历到的点在欧拉路径中靠前,所以需要尽可能先遍历编号小的点,需要对每个点的出度排个序(小到大)

浙公网安备 33010602011771号