目录
一、欧拉路
在数学上有一类重要的问题:图论与几何拓扑,或称欧拉图问题。这篇文章就来说说这类问题。
首先区分一些概念:欧拉路径、欧拉回路、欧拉图
欧拉路径:给定一个连通图,若存在一条路径,经过图中每条边一次且仅一次。
欧拉回路:在欧拉路径的基础上,需要最终回到起点。
欧拉图:具有欧拉回路的图,称为欧拉图。
图如果要存在欧拉路,就必须是所有边要连通的!
无向图:
(1)欧拉路径的充要条件:奇点为0个或者是2个
(2)欧拉回路的充要条件:奇点为0个
有向图:
(1)欧拉路径的充要条件:要么所有点的出入度都相等;要么除了起点和终点外,所有点的出入度相等,起点的出度比入度多1,终点的入度比出度多1
(2)欧拉回路的充要条件:有点的出入度都相等
二、例题:欧拉路
欧拉路
时间限制:1秒 内存限制:128M
题目描述
有一个图,图中要么有两个奇点要么0奇点,如果是欧拉回路请从第一个点为起点开始遍历,如果有两个奇点,则以字典序小的为起点开始遍历,在遍历的过程中,字典序小的先遍历。
输入描述
第一行两个整数,n和e,表示有n个节点,e条边,n<50.
输出描述
只有一行,为欧拉路或欧拉回路。
样例
输入
5 5 1 2 2 3 3 4 4 5 5 1输出
1 2 3 4 5 1
思路
输入图的信息,同时存储每个点的度。
根据每个点的度,找出奇点,即开始点(若无奇点,则从1开始即可)。
进行dfs,dfs过程为对于每个点,遍历其所有的邻接点,若走过的边,就进行删除,确保不存在重复。
在回溯时候,记录路径。
代码
#include<iostream>
using namespace std;
int path[100]; //存储路径的值
int k = 0; //记录存储路径的下标,从k=1开始存放
void dfs(int x) {
for (int i = 1; i <= n; i++) { //按编号从小到大的顺序找x相邻的边
if (tu[x][i]) {
tu[x][i]--, tu[i][x]--; //在图中删掉已经走过的边,避免重复
dfs(i); //从相邻的边开始继续深搜
}
}
path[++k] = i; // i走到“尽头”,将i存入path中
}
int main() {
cin >> n >> e;
for (int i = 0; i < e; i++) {
cin >> a >> b;
g[a][b]++, g[b][a]++;
d[a]++, d[b]++; //记录每个点的度
}
int sta = 1; //找出奇点
for (int i = 1; i <= n; i++) {
if (d[i] % 2 == 1) {
sta = i;
break;
}
}
dfs(sta); //起点:sta
for (int i = cnt; i >= 1; i--)
cout << ans[i] << " ";
return 0;
}
三、例题:地下通道迷宫
地下通道迷宫
时间限制:1秒 内存限制:32M
题目描述
小明最近看了著名电影《地道战》,从此对地道产生了浓厚的兴趣。地道战是在抗日战争时期,在华北平原上抗日军民利用地道打击日本侵略者的作战方式。地道网是房连房、街连街、村连村的地下工事,如下图所示。
小明很想体验一下在地道中穿梭的感觉,于是他来到了一处供人们娱乐的地下通道迷宫。已知它的通道都是直的,而通道所有交叉点(包括通道的端点)上都有一盏灯和一个开关。请你编程告诉小明如何从某个起点开始在迷宫中点亮所有的灯并回到起点?
一个地下通道点灯迷宫及其对应的图如下。输入描述
输入包含多组测试数据,每组输入的第1行给出三个正整数,分别表示地下迷宫的节点数N(1<N<=1000,表示通道所有交叉点和端点)、边数M(<=3000,表示通道数)和探索起始节点编号S(节点从1到N编号)。随后的M行对应M条边(通道),每行给出一对正整数,分别是该条边直接连通的两个节点的编号。
输出描述
对于每组输入,若可以点亮所有节点的灯,则输出从S开始并以S结束的包含所有节点的序列,序列中相邻的节点一定有边(通道);否则虽然不能点亮所有节点的灯,但还是输出点亮部分灯的节点序列,最后输出0,此时表示迷宫不是连通图。
为了使得输出具有唯一的结果,我们约定以节点小编号优先的次序访问(点灯)。在点亮所有可以点亮的灯后,以原路返回的方式回到起点。
样例
输入
6 8 1 1 2 2 3 3 4 4 5 5 6 6 4 3 6 1 5 6 6 6 1 2 1 3 2 3 5 4 6 5 6 4输出
1 2 3 4 5 6 5 4 3 2 1 6 4 5 4 6 0
思路
对于每个点,都遍历到其最小的邻接点,然后不断搜索下去,每走到一个点,都进行打印。
如何找到回路? 回溯时的点,即回路,进行打印即可。
代码
#include<iostream>
#include<cstring>
using namespace std;
int n,m,s;
int g[1050][1050];
int st[1050];
void dfs(int u){
cout<<u<<" "; //每次遍历的点都输出
st[u]=1;
for(int i=1;i<=n;i++){
if(!st[i]&&g[u][i]==1){
dfs(i);
cout<<u<<" "; //输出回溯点
}
}
}
int main(){
int x,y;
while(cin>>n>>m>>s){
memset(g,0,sizeof(g));
memset(st,0,sizeof(st));
for(int i=1;i<=m;i++){
cin>>x>>y;
g[x][y]=g[y][x]=1;
}
dfs(s);
for(int i=1;i<=n;i++){
if(!st[i]){
cout<<0<<" ";
break;
}
}
cout<<endl;
}
return 0;
}
欧拉路的例题有不少,我在这里先不放那么多。
四、DAG图
有向无环图,即无环的有向图,在此图中,无法从一个点出发,经过若干条边重新回到起点。
有向无环图也称为DAG(Directed Acycline Graph)图。
五、AOV网
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网 ,简称 AOV (Activity On Vertex network) 网。
例如:学习之间的一些关系,你影响先学习C++语言,然后才能开始学习数据结构与算法。
若从 i 到 j 有一条有向路径,则 i是 j 的前驱;j 是 i 的后继。
若 < i , j > 是网中有向边,则 i 是 j 的直接前驱; j 是 i 的直接后继。
AOV 网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。
六、拓扑排序
在 AOV 网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若 AOV 网中有弧 <i, j>存在,则在这个序列中, i 一定排在 j 的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序。
且该序列必须满足下面两个条件:
1、每个顶点出现且只出现一次。
2、有弧 <i, j>存在,则在这个序列中,i 一定排在 j 的前面。
注: 拓扑排序针对的是有向无环图(DAG)
如何在一个有向图中找到拓扑排序
首先在一个有向图中找到一个入度为0的点(无前驱),然后输出。
然后删除此点以及此点的所有出边。
不断重复这两个步骤,直至全部顶点都输出或者出现了环。
如上图所示的拓扑排序得到的序列应该是:a,b,c,e,d,g,i,j,f,k,l,h
考虑:一个图的拓扑序列一定唯一吗?
答案是不唯一的,以上图为例,i,k,l,j,a,b,c,e,d,g,f,h也是其一个拓扑排序序列。
拓扑排序的BFS形式
首先将入度为0的顶点入队
while(队列不空){
队头的所有邻接点入度-1
如果存在邻接点入度变为0时则入队。
出队
}
出队序列就是拓扑序列
考虑:如何得到字典序最小的拓扑序列?
可以将队列替换成优先队列,这样,每次从队列中取出的队头都是目前为止度为0中权值最小的一个点。
拓扑排序的DFS形式
首先选取入度为0的开始dfs,若遇到出度为0的点,则进行回溯,回溯时使用栈存储每一个点,最终出栈序列即答案。
首先找到一个入度为0的点-A,从A开始,进行dfs
A->B->C->H
到H遇到出度为0的点,进行回溯,回溯至B点时,可以继续访问E点,然后继续回溯到A点。出栈结果为:
A->B->E->C->H
分别从其他入度为0的点进行此过程,注意,访问过的点需要被标记(防止重复访问)。
D点开始的dfs回溯结果:D
F点开始的dfs回溯结果:FG
反向输出:FGDHCEBA
拓扑排序也有很多例题,但是都很麻烦,作者能力和精力不太允许,因此在这就只讲讲概念性的内容,感兴趣的读者可以自行查阅。
本篇文章到这就结束了,喜欢的朋友可以点赞/收藏/评论/关注,对我来说这些鼓励真的很重要,是我的动力来源。谢谢观看!