算法:用Java实现图论算法中欧拉回路的求解(一笔画问题),并求出 "奥运五环" 的所有一笔画画法

今天我准备用Java来求解图论算法中的欧拉回路(EulerCircuit)问题,这个名字可能对大家有些陌生,如果我说一笔画,大家肯定就都清楚了。原来有一个"七桥问题",不知道的可以百度,这里就不详述。继而引出了一笔画的问题。后来欧拉大神就提出了一笔画的一个原则,那就是一个图形如果可以一笔画,那么把它抽象为图的话,度为奇数的顶点只能是0个或两个。意思也就是,可以一笔画的图形,顶点要么全部都是度为偶数的,要么有且仅有两个度为奇数的顶点,而且一笔画路径的开始和结束,都得是这两个度为奇数的顶点。然后一笔画问题就又叫欧拉回路了

如何用Java编程,来实现图论算法的欧拉回路问题,求解出图的一笔画路径呢?今天我准备以大家都熟知的奥运五环为例,来告诉大家我的实现思路和过程:

在这里插入图片描述
以上就是奥运五环,共有八个顶点,每个顶点的度都是偶数。根据欧拉定理,可以从任意顶点出发对奥运五环进行一笔画。我们可以先把奥运五环的顶点进行标记,如下图:

在这里插入图片描述
以下是我的实现思路:

  1. 检查一笔画图形每个顶点的度,如果不是全偶数度顶点或有且仅有两个奇数度顶点,则该图形无法一笔画
  2. 先将一笔画图形的所有顶点进行编号,如上图
  3. 然后计算每个顶点之间的连线数,如奥运五环图,A和B之间有3条连线,C和D之间有两条连线,D和E之间有1条连线
  4. 将一笔画图形抽象为新的图,重新分布顶点,将所有有连线的顶点都加上一条边,边是无向边,且权重为他们之间的连线数
  5. 从某一点出发(有且仅有两个度为奇数的顶点的图,则必须以其中一个奇数度顶点出发),然后递归用深度优先搜索DFS来探索一笔画路径,每走过一条边,则边的权重减一。如果当前顶点和下一个顶点之间的权重减到了0,或者本来就是0,则两点之间不可达,就换其他可达的顶点,继续递归往深处遍历,直到完成一笔画,或者接下来所有相邻顶点之间的边权重都为0,无路可走
  6. 如果无路可走但完成了一笔画(所有边的权重都为0了),则可以输出正确结果,完美结束递归。如果未完成一笔画且无路可走,直接结束当前分支递归
  7. 初始化决定一个开始顶点,求出所有下一个可达的顶点集,遍历下一个顶点集,多批次递归寻路,以求出图形的所有一笔画路径

下面这张图,就是上面步骤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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章