今天我准备用Java来求解图论算法中的欧拉回路(EulerCircuit)问题,这个名字可能对大家有些陌生,如果我说一笔画,大家肯定就都清楚了。原来有一个"七桥问题",不知道的可以百度,这里就不详述。继而引出了一笔画的问题。后来欧拉大神就提出了一笔画的一个原则,那就是一个图形如果可以一笔画,那么把它抽象为图的话,度为奇数的顶点只能是0个或两个。意思也就是,可以一笔画的图形,顶点要么全部都是度为偶数的,要么有且仅有两个度为奇数的顶点,而且一笔画路径的开始和结束,都得是这两个度为奇数的顶点。然后一笔画问题就又叫欧拉回路了
如何用Java编程,来实现图论算法的欧拉回路问题,求解出图的一笔画路径呢?今天我准备以大家都熟知的奥运五环为例,来告诉大家我的实现思路和过程:
以上就是奥运五环,共有八个顶点,每个顶点的度都是偶数。根据欧拉定理,可以从任意顶点出发对奥运五环进行一笔画。我们可以先把奥运五环的顶点进行标记,如下图:
以下是我的实现思路:
- 检查一笔画图形每个顶点的度,如果不是全偶数度顶点或有且仅有两个奇数度顶点,则该图形无法一笔画
- 先将一笔画图形的所有顶点进行编号,如上图
- 然后计算每个顶点之间的连线数,如奥运五环图,A和B之间有3条连线,C和D之间有两条连线,D和E之间有1条连线
- 将一笔画图形抽象为新的图,重新分布顶点,将所有有连线的顶点都加上一条边,边是无向边,且权重为他们之间的连线数
- 从某一点出发(有且仅有两个度为奇数的顶点的图,则必须以其中一个奇数度顶点出发),然后递归用深度优先搜索DFS来探索一笔画路径,每走过一条边,则边的权重减一。如果当前顶点和下一个顶点之间的权重减到了0,或者本来就是0,则两点之间不可达,就换其他可达的顶点,继续递归往深处遍历,直到完成一笔画,或者接下来所有相邻顶点之间的边权重都为0,无路可走
- 如果无路可走但完成了一笔画(所有边的权重都为0了),则可以输出正确结果,完美结束递归。如果未完成一笔画且无路可走,直接结束当前分支递归
- 初始化决定一个开始顶点,求出所有下一个可达的顶点集,遍历下一个顶点集,多批次递归寻路,以求出图形的所有一笔画路径
下面这张图,就是上面步骤4中说的奥运五环抽象的图,顶点间的边的权重,就是在奥运五环中的连线数:
以下是上面的奥运五环抽象图的邻接矩阵:
下面是我用Java实现的欧拉回路的求解算法代码,并以奥运五环为例,用该算法求出 “奥运五环” 的所有一笔画画法路径。画的时候如果从A点到B点有多条路径,随意画一条路径即可,都是等价的。算法的原理和精髓都在下面的代码和其间的注释之中:
import java.util.ArrayList;
import java.util.List;
/**
* @author LiYang
* @ClassName EulerCircuit
* @Description 求解欧拉回路(一笔画)问题
* @date 2019/11/20 16:05
*/
public class EulerCircuit {
/**
* 返回奥运五环图的邻接矩阵(顶点间有几条连线,权重就是几)
* @return 奥运五环图的邻接矩阵
*/
public static int[][] initOlympicRingsMatrix() {
//生成奥运五环图的邻接矩阵
return new int[][] {
{0, 3, 0, 1, 0, 0, 0, 0},
{3, 0, 1, 0, 0, 0, 0, 0},
{0, 1, 0, 2, 0, 1, 0, 0},
{1, 0, 2, 0, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 2, 0, 1},
{0, 0, 1, 0, 2, 0, 1, 0},
{0, 0, 0, 0, 0, 1, 0, 3},
{0, 0, 0, 0, 1, 0, 3, 0}
};
}
/**
* 判断一笔画是否完成,也就是看是否
* 已经形成了欧拉回路
* @param matrix 一笔画图邻接矩阵的中间状态
* @return 是否已经完成
*/
public static boolean isFinished(int[][] matrix) {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix.length; j++) {
//如果还有边没有走过
if (matrix[i][j] > 0) {
//判定为未完成
return false;
}
}
}
//所有边已走过,判定为已完成
return true;
}
/**
* 深度拷贝Set<Integer>对象
* @param source 拷贝源
* @return 深度拷贝的副本
*/
public static List<Integer> copyList(List<Integer> source) {
//拷贝副本
List<Integer> copy = new ArrayList<>();
//加入所有元素
for (Integer item : source) {
copy.add(item);
}
//返回拷贝副本
return copy;
}
/**
* 深度拷贝邻接矩阵二维数组对象
* @param matrix 邻接矩阵拷贝源
* @return 深度拷贝的邻接矩阵副本
*/
public static int[][] copyMatrix(int[][] matrix) {
//拷贝副本
int[][] newMatrix = new int[matrix.length][matrix.length];
//复制所有值
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix.length; j++) {
newMatrix[i][j] = matrix[i][j];
}
}
//返回拷贝副本
return newMatrix;
}
/**
* 求解欧拉回路,探索一笔画路径
* @param matrix 当前的邻接矩阵
* @param nextVertex 下一步的路径
* @param route 当前的路线图
* @param vertexName 顶点名称字符串数组
*/
public static void runEulerCircuit(int[][] matrix, int nextVertex, List<Integer> route, String[] vertexName) {
//先复制一份当前的邻接矩阵
int[][] newMatrix = copyMatrix(matrix);
//当前驻足的顶点
int currentVertex = route.get(route.size() - 1);
//走过nextVertex,更新邻接矩阵
//更新方式为边权重减1,两边通路都要减
newMatrix[currentVertex][nextVertex] --;
newMatrix[nextVertex][currentVertex] --;
//再复制一份路径图
List<Integer> newRoute = copyList(route);
//路径图加入nextVertex
newRoute.add(nextVertex);
//找到nextVertex的所有下步顶点
List<Integer> nextList = new ArrayList<>();
//收集下一步可达的顶点
for (int i = 0; i < newMatrix.length; i++) {
if (newMatrix[nextVertex][i] > 0) {
nextList.add(i);
}
}
//如果有下一个可达的顶点
if (nextList.size() > 0) {
//遍历所有的下一步顶点
for (Integer next : nextList) {
//递归调用本方法,继续往下求解
runEulerCircuit(copyMatrix(newMatrix), next, copyList(newRoute), vertexName);
}
//如果现在已经没有任何可达顶点了
} else {
//如果已经完成了一笔画,求解了欧拉回路
if (isFinished(newMatrix)) {
//字符串路径集合
List<String> solutionRoute = new ArrayList<>();
//根据顶点名称字符串数组,得到字符串的路径
for (Integer vertexIndex : newRoute) {
solutionRoute.add(vertexName[vertexIndex]);
}
//整理路径信息
String solution = solutionRoute.toString()
.replaceAll("\\[", "")
.replaceAll("]", "")
.replaceAll(", ", " -> ");
//打印求得的一笔画路径(欧拉回路)
System.out.println("找到奥运五环一笔画路径:" + solution);
}
}
}
/**
* 求解欧拉回路,探索一笔画路径的驱动程序
* @param matrix 一笔画图的邻接矩阵
* @param startVertex 开始顶点,如果有且仅有两个度为三的顶点,
* 则必须以这两个顶点之一作为起点
* @param vertexName 顶点名称字符串数组
*/
public static void runEulerCircuit(int[][] matrix, int startVertex, String[] vertexName) {
//路径
List<Integer> route = new ArrayList<>();
//从起点出发
route.add(startVertex);
//下一个顶点的集合
List<Integer> nextVertexList = new ArrayList<>();
//找到下一个可达的顶点
for (int i = 0; i < matrix.length; i++) {
if (matrix[startVertex][i] > 0) {
nextVertexList.add(i);
}
}
//遍历所有下一步可达顶点
for (Integer nextVertex : nextVertexList) {
//多分支递归调用求解欧拉回路的算法程序
runEulerCircuit(matrix, nextVertex, route, vertexName);
}
}
/**
* 运行欧拉回路求解算法,解出奥运五环的所有一笔画路径
* @param args
*/
public static void main(String[] args) {
//奥运五环图的初始邻接矩阵
int[][] matrix = initOlympicRingsMatrix();
//奥运五环图的顶点名称字符串数组
String[] vertexName = new String[]{"A", "B", "C", "D", "E", "F", "G", "H"};
//从A点出发,运行算法,解出奥运五环的所有一笔画路径
runEulerCircuit(matrix, 0, vertexName);
}
}
运行上面代码的main方法,控制台输出所有的奥运五环的一笔画画法路径(大家感兴趣可以自己尝试照着画一下),算法测试通过:
找到奥运五环一笔画路径:A -> B -> A -> B -> C -> D -> C -> F -> E -> F -> G -> H -> G -> H -> E -> D -> A
找到奥运五环一笔画路径:A -> B -> A -> B -> C -> D -> C -> F -> E -> H -> G -> H -> G -> F -> E -> D -> A
找到奥运五环一笔画路径:A -> B -> A -> B -> C -> D -> C -> F -> G -> H -> G -> H -> E -> F -> E -> D -> A
找到奥运五环一笔画路径:A -> B -> A -> B -> C -> D -> E -> F -> E -> H -> G -> H -> G -> F -> C -> D -> A
找到奥运五环一笔画路径:A -> B -> A -> B -> C -> D -> E -> F -> G -> H -> G -> H -> E -> F -> C -> D -> A
找到奥运五环一笔画路径:A -> B -> A -> B -> C -> D -> E -> H -> G -> H -> G -> F -> E -> F -> C -> D -> A
找到奥运五环一笔画路径:A -> B -> A -> B -> C -> F -> E -> F -> G -> H -> G -> H -> E -> D -> C -> D -> A
找到奥运五环一笔画路径:A -> B -> A -> B -> C -> F -> E -> H -> G -> H -> G -> F -> E -> D -> C -> D -> A
找到奥运五环一笔画路径:A -> B -> A -> B -> C -> F -> G -> H -> G -> H -> E -> F -> E -> D -> C -> D -> A
找到奥运五环一笔画路径:A -> B -> A -> D -> C -> D -> E -> F -> E -> H -> G -> H -> G -> F -> C -> B -> A
找到奥运五环一笔画路径:A -> B -> A -> D -> C -> D -> E -> F -> G -> H -> G -> H -> E -> F -> C -> B -> A
找到奥运五环一笔画路径:A -> B -> A -> D -> C -> D -> E -> H -> G -> H -> G -> F -> E -> F -> C -> B -> A
找到奥运五环一笔画路径:A -> B -> A -> D -> C -> F -> E -> F -> G -> H -> G -> H -> E -> D -> C -> B -> A
找到奥运五环一笔画路径:A -> B -> A -> D -> C -> F -> E -> H -> G -> H -> G -> F -> E -> D -> C -> B -> A
找到奥运五环一笔画路径:A -> B -> A -> D -> C -> F -> G -> H -> G -> H -> E -> F -> E -> D -> C -> B -> A
找到奥运五环一笔画路径:A -> B -> A -> D -> E -> F -> E -> H -> G -> H -> G -> F -> C -> D -> C -> B -> A
找到奥运五环一笔画路径:A -> B -> A -> D -> E -> F -> G -> H -> G -> H -> E -> F -> C -> D -> C -> B -> A
找到奥运五环一笔画路径:A -> B -> A -> D -> E -> H -> G -> H -> G -> F -> E -> F -> C -> D -> C -> B -> A
找到奥运五环一笔画路径:A -> B -> C -> D -> C -> F -> E -> F -> G -> H -> G -> H -> E -> D -> A -> B -> A
找到奥运五环一笔画路径:A -> B -> C -> D -> C -> F -> E -> H -> G -> H -> G -> F -> E -> D -> A -> B -> A
找到奥运五环一笔画路径:A -> B -> C -> D -> C -> F -> G -> H -> G -> H -> E -> F -> E -> D -> A -> B -> A
找到奥运五环一笔画路径:A -> B -> C -> D -> E -> F -> E -> H -> G -> H -> G -> F -> C -> D -> A -> B -> A
找到奥运五环一笔画路径:A -> B -> C -> D -> E -> F -> G -> H -> G -> H -> E -> F -> C -> D -> A -> B -> A
找到奥运五环一笔画路径:A -> B -> C -> D -> E -> H -> G -> H -> G -> F -> E -> F -> C -> D -> A -> B -> A
找到奥运五环一笔画路径:A -> B -> C -> F -> E -> F -> G -> H -> G -> H -> E -> D -> C -> D -> A -> B -> A
找到奥运五环一笔画路径:A -> B -> C -> F -> E -> H -> G -> H -> G -> F -> E -> D -> C -> D -> A -> B -> A
找到奥运五环一笔画路径:A -> B -> C -> F -> G -> H -> G -> H -> E -> F -> E -> D -> C -> D -> A -> B -> A
找到奥运五环一笔画路径:A -> D -> C -> D -> E -> F -> E -> H -> G -> H -> G -> F -> C -> B -> A -> B -> A
找到奥运五环一笔画路径:A -> D -> C -> D -> E -> F -> G -> H -> G -> H -> E -> F -> C -> B -> A -> B -> A
找到奥运五环一笔画路径:A -> D -> C -> D -> E -> H -> G -> H -> G -> F -> E -> F -> C -> B -> A -> B -> A
找到奥运五环一笔画路径:A -> D -> C -> F -> E -> F -> G -> H -> G -> H -> E -> D -> C -> B -> A -> B -> A
找到奥运五环一笔画路径:A -> D -> C -> F -> E -> H -> G -> H -> G -> F -> E -> D -> C -> B -> A -> B -> A
找到奥运五环一笔画路径:A -> D -> C -> F -> G -> H -> G -> H -> E -> F -> E -> D -> C -> B -> A -> B -> A
找到奥运五环一笔画路径:A -> D -> E -> F -> E -> H -> G -> H -> G -> F -> C -> D -> C -> B -> A -> B -> A
找到奥运五环一笔画路径:A -> D -> E -> F -> G -> H -> G -> H -> E -> F -> C -> D -> C -> B -> A -> B -> A
找到奥运五环一笔画路径:A -> D -> E -> H -> G -> H -> G -> F -> E -> F -> C -> D -> C -> B -> A -> B -> A