算法:用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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章