欧拉通路与欧拉回路
之前,写了图系列一二三,现在出四啦!这也意味着,对于图的部分,可以说50%以上常用的内容就已经过了一遍了。欧拉路的部分会稍微难一点,主要是我们要和定义打交道了。至于其他图的理论,我感觉比较有用的就不剩下多少了。可能就还有同构什么的,还会有一些探讨的空间。好长一段时间没有写东西啦!这篇文章,大致会经过几次修改完成。主要参考了Leetcode的这道题——重新安排行程。其实,这道题目,用图一二三的方法也能解决,但是非常复杂,我花了很长时间。
定义
下面这段话,请同学们略过。
我真是越来越佩服欧拉了,哈哈哈!因为最近看数学比较多,所以经常看到欧拉的名字。这些数学家挺伟大的,这些数学理论给我们计算机人提供了指导,虽然有多少能派上用场真的不好说,但是有总比没有好。不过,话说回来,对于计算机人来说,理论当然重要,但是怎么去实现也是非常重要的!以前,我上离散数学,学得懂,但是不会用。拿到实际问题,根本不知道怎么用数学知识去解决。比如,排列与组合就是典型的例子,道理我都懂,怎么写个算法出来呢?以前我一直搞不出来,所以挫败感挺严重的。不过,现在一切都越来越明朗了。我感觉,我的数学知识逐渐能够派上用场了。要能够融会贯通还是需要付出挺多时间的,个人来讲,感觉学数学还是挺有兴趣的,所以,多花点时间不是什么大问题。
上图为柯尼斯堡的七座桥(摘自Wikipedia),要求你走过七座桥恰好一次。这个问题是无解的,因为图中一共有四个度数为奇数的顶点。奇定点意味着,你从这个点出发,如果要用完所有的边,你一定回不来;如果进入这个点,用完所有的边,你一定出不来。所以,要刚好走一遍,只能有两个奇定点。
1. 欧拉通路: 通过图中所有边恰好一次且行遍所有顶点的通路称为欧拉通路。(不需要回到原点)
2. 欧拉回路: 通过图中所有边恰好一次且行遍所有顶点的回路称为欧拉回路。(要能够回到原点)
3. 半欧拉图:具有欧拉通路但不具有欧拉回路的无向图称为半欧拉图。
4. 欧拉图: 具有欧拉回路的无向图称为欧拉图。
5. 连通图
a. 对于无向图G,若从顶点vi到顶点vj有路径相连,则称vi和vj是连通的。如果在无向图G中,任意两点都是互通的,那么无向图G是连通的。如何用代码实现呢?其实从一个点出发,通过广度/深度优先遍历,能够遍历到每一个点,说明该图是连通的。
b. 对于有向图G,若从顶点vi到顶点vj有路径相连(图中所有的边是同向的),则称vi和vj是连通的。如果在有向图G中,任意两点vi和vj,存在一条路径从vi到vj且存在一条路径从vj到vi,则有向图G是强连通的。如果任意两点都是连通的,则称G是连通的。(必须遍历C(n, 2)个定点组合了,我现在能想到的办法。)
性质
1. 对于无向图G
a. G 是欧拉图当且仅当 G 是连通的且没有奇度顶点。(我们首先可以对每个点的度数进行统计,判断是否符合条件。然后,进行深度优先遍历,如果能够遍历完所有的点,那么G是欧拉图)
b. G 是半欧拉图当且仅当 G 是连通的且 G 中恰有 2 个奇度顶点。(首先,对度数进行统计,判断是否符合条件。然后,进行一次深度优先遍历就好了,如果能够遍历完所有的点,那么G是欧拉图)
证明无向图是连通的,只需要进行深度/广度优先遍历,能够遍历所有的点就行。
2. 对于有向图G
a. G是欧拉图当且仅当 G 的所有顶点属于同一个连通分量且每个顶点的入度和出度相同。(首先,对度数进行统计,判断是否符合条件。然后,进行一次深度优先遍历就好了,如果能够遍历完所有的点,那么G是欧拉图。)
b. G 是半欧拉图当且仅当 G 的所有顶点属于同一个连通分量且:1)恰有一个点的出度与入度相差1;2)恰有一个点的入度与出度相差1;3)其他顶点的入度与出度相等。(首先,对度数进行统计,判断是否符合条件。然后,从出度与入度相差1的顶点出发进行一次深度优先遍历就好了,如果能够遍历完所有的点,那么G是欧拉图。)
算法实现 - 针对有向图
1. 生成一张图,List<String> edge是有两个String定点构成的边,List<List<String>> graph是由图中所有的边构成的。
/**
* @Author: zhaoyangyingmu
* @Date: 2020/9/9
* @Description: graph
* @version: 1.0
*/
public class Graph {
static String[] graphStr = new String[] {
"[[\"EZE\",\"AXA\"],[\"TIA\",\"ANU\"],[\"ANU\",\"JFK\"],[\"JFK\",\"ANU\"],[\"ANU\",\"EZE\"],[\"TIA\",\"ANU\"],[\"AXA\",\"TIA\"],[\"TIA\",\"JFK\"],[\"ANU\",\"TIA\"],[\"JFK\",\"TIA\"]]",// 欧拉回路
"[[\"JFK\",\"SFO\"],[\"JFK\",\"ATL\"],[\"SFO\",\"ATL\"],[\"ATL\",\"JFK\"],[\"ATL\",\"SFO\"]]", // 欧拉通路
"[[\"MUC\",\"LHR\"],[\"JFK\",\"MUC\"],[\"SFO\",\"SJC\"],[\"LHR\",\"SFO\"]]",// 欧拉通路
"[[\"MUC\",\"LHR\"],[\"JFK\",\"MUC\"],[\"SFO\",\"SJC\"],[\"LHR\",\"SFO\"], [\"LHJ\",\"LHJ\"]]" // LHJ不可达
};
public static List<List<String>> generateGraph(String str) {
str = str.substring(2, str.length()-2);
String[] edges = str.split("],\\[");
List<List<String>> graph = new LinkedList<>();
for (String edge: edges) {
edge = edge.replaceAll("\"", "");
String[] begin2end = edge.split(",");
List<String> edgeList = new LinkedList<>();
edgeList.add(begin2end[0]);
edgeList.add(begin2end[1]);
graph.add(edgeList);
}
return graph;
}
public static List<List<String>> generateGraph(int idx) {
return generateGraph(graphStr[idx]);
}
}
2. 出入度统计,构造邻接链表,设置所有的边为未访问的;最后深度优先遍历,用完的边要删除掉,返回一笔画的结果。
import java.util.*;
class Solution {
Map<String, List<String>> adjs = new HashMap<>();// 邻接链表
Map<String, int[]> degrees = new HashMap<>(); // 入度+出度
Map<String, Boolean> visited = new HashMap<>(); // 是否访问
List<String> eulerPath = new LinkedList<>();
/**
* 如果是一张欧拉图,返回一笔画的路径(不唯一)
* 否则返回null
* */
public List<String> traverseEulerPath(List<List<String>> graph) {
for (List<String> edge: graph) {
String begin = edge.get(0);
String end = edge.get(1);
if (!adjs.containsKey(begin)) adjs.put(begin, new LinkedList<>());
adjs.get(begin).add(end);
if (!degrees.containsKey(begin)) degrees.put(begin, new int[]{0, 0});
degrees.get(begin)[1] += 1;
if (!degrees.containsKey(end)) degrees.put(end, new int[]{0,0});
degrees.get(end)[0] += 1;
if (!visited.containsKey(begin)) visited.put(begin , false);
if (!visited.containsKey(end)) visited.put(end, false);
}
// 出入度统计
String begin = null;
String end = null;
for (String point: degrees.keySet()) {
int[] degree = degrees.get(point);
if (degree[0] - degree[1] == 1) {// end point
if (end == null) end = point;
else return null;
}
else if (degree[1] - degree[0] == 1) {
if (begin == null) begin = point;
else return null;
}
else if (degree[1] - degree[0] == 0) continue;
else return null; // 出度不等于入度
}
if ((begin == null && end != null)
|| (begin == null && end != null )) return null; // 只有一个点符合条件
// begin与end同时为null,从任意点开始访问
// 从begin开始访问
if (begin == null) begin = degrees.keySet().iterator().next();
dfs(begin);
// 检查是否访问了所有的点
for (String point: visited.keySet()) {
if (!visited.get(point)) {
System.out.println("point: " + point + " not available.");
return null;
}
}
Collections.reverse(eulerPath);
return eulerPath;
}
private void dfs(String point) {
visited.put(point, true);
while (adjs.containsKey(point) && adjs.get(point).size() > 0) {
String tmp = adjs.get(point).get(0);
adjs.get(point).remove(0);
dfs(tmp);
}
eulerPath.add(point);
}
}