算法:用不相交集類(並查集)實現隨機迷宮生成算法,並最終得到能顯示迷宮圖的HTML文件

之前我用不相交集類(並查集)輔助實現了克魯斯卡爾(Kruskal)算法求出圖的最小生成樹,今天我就用並查集來再實現一個其經典的應用:隨機迷宮圖的生成

並查集生成迷宮圖的原理如下,也是迷宮圖算法實現的思路:

  1. 根據自定義迷宮的寬高,有一系列 寬×高 的矩陣點,從0開始編號
  2. 剛開始這些矩陣點都是不連通的,對應並查集也是都不相交
  3. 我們隨機找到兩個相鄰的矩陣點,然後通過並查集的find算法,查看相鄰矩陣點是否連通
  4. 如果該相鄰矩陣點對連通,則需要繼續尋找不連通的相鄰點對
  5. 如果該相鄰矩陣點對不連通,則他們之間的牆就拆掉,然後二者在並查集裏面做union操作,表示他們已經連通
  6. 生成一個迷宮圖,需要拆掉矩陣點-1面牆,也就是N個元素,得求N-1次union,才能全部相交
  7. 算法也就是不停地尋找隨機的相鄰兩位置,並且不連通,就記錄下來,拆掉二者中間的牆,求union記錄,算法執行矩陣點-1次後即刻終止
  8. 整理算法的結果,按照HTML格式整理內容,然後IO輸出流寫出HTML文件,用瀏覽器打開即可查看生產的迷宮圖

接下來就是我用Java實現的不相交集類(並查集)實現隨機迷宮生成的算法,算法的思想和精髓都在代碼和其間的詳細註釋中:

import java.io.*;
import java.util.*;

/**
 * @author LiYang
 * @ClassName DrawMaze
 * @Description 迷宮圖生成算法
 * @date 2019/11/29 15:55
 */
public class DrawMaze {

    /**
     * 不相交集工具類,按高度來合併
     * 實現代碼根之前的不相交集類一樣
     * 這裏作爲內部類,輔助實現迷宮生成算法
     */
    static class DisjointSetUnionByHeight {

        //不相交集類(並查集)的元素數組
        private int[] elements;

        /**
         * 不相交集類(並查集)的構造方法,入參元素個數
         * @param elementNum 元素個數
         */
        public DisjointSetUnionByHeight(int elementNum) {
            if (elementNum <= 0) {
                throw new IllegalArgumentException("元素個數要大於零");
            }

            //實例化不相交集類(並查集)的元素數組
            this.elements = new int[elementNum];

            //初始化元素樹的高度都爲-1(如果是根,值就是負數,連通元素組成的樹
            //的高度是多少,則根元素就是負幾)
            for (int i = 0; i < elements.length; i++) {
                this.elements[i] = -1;
            }
        }

        /**
         * 查詢不相交集類(並查集)的元素個數
         * @return 元素個數
         */
        public int size() {
            return elements.length;
        }

        /**
         * 查詢不相交集類(並查集)的某個元素的根元素
         * 輸入的是下標查,如果兩個元素的根元素相同,
         * 則這兩個元素就是等價的。實際中還會有一個
         * 與elements等長的數組,裝的是元素的名字,
         * elements只是相當於代號,記錄連通關係,
         * 二者通過下標,來映射真實元素
         * @param element 待查詢的元素
         * @return 該元素的根元素
         */
        public int find(int element) {
            //如果記錄小於0,那就是根元素
            if (elements[element] < 0) {
                //返回根元素
                return element;

            //如果記錄不小於0,那還不是根,
            //是等價森林中的上一個節點
            } else {
                //遞歸向上繼續尋找根
                return find(elements[element]);
            }
        }

        /**
         * 將不相交集類(並查集)的兩個元素進行連通操作
         * 注意,兩個元素連通,代表這兩個元素所在的子樹
         * 全部變成一個圖中的大的子樹。如果這
         * 兩個元素本來就連通,則不進行連通操作,捨棄該邊。
         * 注意,這裏同樣是入參下標,下標映射真實元素
         * 此實現類,根據樹的高度來決定誰合併到誰上面,
         * 矮的樹的根節點,會作爲大的樹的根節點的子節點
         * @param element1 元素下標1
         * @param element2 元素下標2
         */
        public void union(int element1, int element2) {
            //找到兩個元素的根元素
            int root1 = find(element1);
            int root2 = find(element2);

            //如果兩個元素本就連通
            if (root1 == root2) {
                //不作處理
                return;
            }

            //比高度:如果root1比root2的樹要高
            if (elements[root1] < elements[root2]) {
                //將較矮的root2合併到較高的root1上
                elements[root2] = root1;

            //比高度:如果root2比root1的樹要高
            } else if (elements[root2] < elements[root1]) {
                //將較矮的root1合併到較高的root2上
                elements[root1] = root2;

            //比高度:如果root1和root2一樣高
            } else {
                //將root1合併到root2上
                elements[root1] = root2;

                //root2的高度增加1
                root2 --;
            }
        }
    }

    /**
     * 獲取某位置的前後左右四個位置(可能不到四個)
     * @param position 需要尋找相鄰位置的當前位置
     * @param width 迷宮寬
     * @param height 迷宮高
     * @return 當前位置的相鄰位置集合
     */
    private static List<Integer> getAdjacentPosition(int position, int width, int height) {
        //當前位置上面的位置
        int top = position - width;

        //當前位置下面的位置
        int bottom = position + width;

        //當前位置左邊的位置
        int left = position - 1;

        //當前位置右邊的位置
        int right = position + 1;

        //收集有效的相鄰位置
        List<Integer> adjacentPosition = new ArrayList<>();

        //上面的會更小,因此需要爲非負數
        if (top >= 0) {
            adjacentPosition.add(top);
        }

        //下面的會更大,不能超過最大位置
        if (bottom < width * height) {
            adjacentPosition.add(bottom);
        }

        //左邊的不能越界到上一行
        if ((left + 1) % width != 0) {
            adjacentPosition.add(left);
        }

        //右邊的不能越界到下一行
        if (right % width != 0) {
            adjacentPosition.add(right);
        }

        //返回當前位置所有的有效相鄰位置
        return adjacentPosition;
    }

    /**
     * 對迷宮進行拆牆的操作,返回全部需要拆掉的牆
     * @param width 迷宮寬
     * @param height 迷宮高
     * @return 所有需要拆掉的牆,由兩個相鄰位置決定
     */
    private static List<String> breakWall(int width, int height) {
        //算出總的位置數
        int totalPosition = width * height;

        //創建不相交集(並查集)類,作爲迷宮生成輔助工具類
        DisjointSetUnionByHeight disjointHeight = new DisjointSetUnionByHeight(totalPosition);

        //算出需要破壞的牆的數量(n個獨立元素連通,需要合併n-1次)
        int wallNeedToBreak = width * height - 1;

        //記錄破壞的牆的信息 pos1-pos2,且pos1 < pos2
        List<String> breakWallList = new ArrayList<>();

        //隨機數類
        Random random = new Random();

        //一共需要拆掉位置數-1面牆,使得所有位置連通
        for (int time = 0; time < wallNeedToBreak; time++) {

            //直到遇到一對不連通的相鄰位置,然後拆掉了中間的牆,完成一次拆牆
            while (true) {

                //隨機找到一個位置
                int randomPosition = random.nextInt(totalPosition - 1);

                //獲得隨機位置的所有相鄰位置
                List<Integer> adjacentPosition = getAdjacentPosition(randomPosition, width, height);

                //隨機打亂所有相鄰位置
                Collections.shuffle(adjacentPosition);

                //打亂的相鄰位置中,取第一個相鄰位置
                int anotherPosition = adjacentPosition.get(0);

                //並查集重要操作1:查詢二者是否已連通,就看二者的根元素是否相等
                boolean isConnected = disjointHeight.find(randomPosition) == disjointHeight.find(anotherPosition);

                //如果未連通
                if (!isConnected) {

                    //將二者之間的牆打掉(存的是牆的相鄰兩個位置)
                    breakWallList.add(Math.min(randomPosition, anotherPosition)
                            + "-" + Math.max(randomPosition, anotherPosition));

                    //並查集重要操作2:設置連通狀態,將兩個位置求並
                    disjointHeight.union(randomPosition, anotherPosition);

                    //結束本次打牆操作
                    //當然,如果isConnected爲true,則代表隨機的相鄰兩位置是連通的
                    //此時不需要打牆和求並,得重新找兩個隨機相鄰的不連通的頂點,
                    //直到找到隨機相鄰的不連通兩位置,然後才break掉這個while循環
                    break;
                }
            }
        }

        //返回需要拆的牆,一共需要拆掉位置數-1面牆
        return breakWallList;
    }

    /**
     * 初始化未被破壞掉的牆
     * @param width 迷宮寬
     * @param height 迷宮高
     * @return 未被拆牆的原圖
     */
    private static int[] initWall(int width, int height) {
        //算出矩陣的寬高
        int matrixWidth = width * 2 + 1;
        int matrixHeight = height * 2 + 1;

        //初始化矩陣(本算法,用的是一維數組,HTML的需要)
        int[] matrix = new int[matrixWidth * matrixHeight];

        //將數組全部賦初值1(同樣是後面HTML的需要)
        Arrays.fill(matrix, 1);

        //剛開始的位置
        int initStart = width * 2 + 2;

        //垂直方向的步長
        int verticalStep = (width * 2 + 1) * 2;

        //水平方向的步長
        int horizontalStep = 2;

        //遍歷整個初始圖,先摳出初始位置
        for (int i = 0; i < height; i++) {

            //水平位置的開始位置
            int start = initStart + i * verticalStep;

            //遍歷當前行
            for (int j = 0; j < width; j++) {

                //算出初始空格的位置,1是實體,0是空格
                int blank = start + j * horizontalStep;

                //初始化空格位置
                matrix[blank] = 0;
            }
        }

        //返回已經摳掉初始位置的矩陣
        return matrix;
    }

    /**
     * 根據位置號,找到方格號
     * @param position 位置號
     * @return 方格號,也就是對應matrix的下標
     */
    private static int findMatrixByPosition(int position, int width, int height) {
        //每行matrix的方格數
        int lineNum = width * 2 + 1;

        //返回計算好的方格下標
        return (position / width * 2 + 1) * lineNum + (position % width) * 2 + 1;
    }

    /**
     * 根據相鄰位置對,返回需要拆掉的牆的方格號
     * @param positionPair 相鄰位置對的字符串格式
     * @return 需要拆掉的牆的方格下標
     */
    private static int getBreakWallNo(String positionPair, int width, int height) {
        //較小位置
        int minPosition = Integer.parseInt(positionPair.split("-")[0]);

        //較大位置
        int maxPosition = Integer.parseInt(positionPair.split("-")[1]);

        //較小位置的方格下標
        int minMatrix = findMatrixByPosition(minPosition, width, height);

        //如果兩個位置相差1,則表示是左右相鄰
        if (maxPosition - minPosition == 1) {

            //返回較小位置方格號+1
            return minMatrix + 1;

        //如果兩個位置不相差1,則表示是上下相鄰
        } else {

            //返回較小位置方格號下面的方格下標
            return minMatrix + width * 2 + 1;
        }
    }

    /**
     * 生成迷宮的一維矩陣數組(1是黑方塊,0是白方塊,也就是路)
     * @param width 迷宮寬
     * @param height 迷宮高
     * @return 生成迷宮的一維矩陣數組
     */
    private static String generateMazeArray(int width, int height) {
        //根據寬高,初始化迷宮圖矩陣
        int[] matrix = initWall(width, height);

        //求出隨機生成的所有需要拆掉的牆
        List<String> breakWall = breakWall(width, height);

        //遍歷所有需要拆掉的牆
        for (String posPair : breakWall) {

            //算出需要拆掉的牆的方格號
            int breakIndex = getBreakWallNo(posPair, width, height);

            //將對應的位置的牆拆掉,變成路
            matrix[breakIndex] = 0;
        }

        //生成左上角入口
        matrix[1] = 0;

        //生成右下角出口
        matrix[matrix.length - 2] = 0;

        //返回迷宮一維矩陣數組的字符串(後面作爲HTML文件的內容)
        return Arrays.toString(matrix);
    }

    /**
     * 生成迷宮圖的HTML文件,用瀏覽器打開則可以看到迷宮
     * @param width 迷宮寬度
     * @param height 迷宮高度
     * @param filePath 生成的迷宮HTML文件的路徑
     * @throws IOException
     */
    private static void generateMazeHTML(int width, int height, String filePath) throws IOException {
        //HTML文件的內容的StringBuffer
        StringBuffer sbuf = new StringBuffer();

        //加入必要的HTML內容
        sbuf.append("<!doctype html>\n" +
                "<html lang=\"en\">\n" +
                "<head>\n" +
                "    <meta charset=\"UTF-8\">\n" +
                "    <title>LeeCode生成迷宮圖</title>\n" +
                "</head>\n" +
                "<body>\n" +
                "    <canvas id=\"canvas1\" width=\"2000\" height=\"2000\"></canvas>\n" +
                "    <script>\n" +
                "        var map={\n" +
                "\t\t\t\"data\":");

        //這裏加入生成的迷宮圖數組數據
        sbuf.append(generateMazeArray(width, height)).append("\n");

        //這裏加入迷宮矩陣的寬高
        sbuf.append(",\"width\":").append(width * 2 + 1).append(",\n");
        sbuf.append("\"height\":").append(height * 2 + 1).append("}\n");

        //繼續加入必要的HTML內容
        sbuf.append("    var canvas = document.getElementById(\"canvas1\");\n" +
                "        var ctx = canvas.getContext(\"2d\");\n" +
                "        var W = 10;var H = 10;var l = 0;var t = 0;\n" +
                "        for (var i=0; i<map.data.length; i++){    \n" +
                "            l = i%map.width*W;\n" +
                "            if (i%map.width==0&&i!=0){\n" +
                "                t+=H;\n" +
                "            }\n" +
                "            if (map.data[i]>0){\n" +
                "                ctx.fillRect(l, t, W, H);\n" +
                "            }    \n" +
                "        }\n" +
                "    </script>\n" +
                "</body>\n" +
                "</html>");

        //HTML文件類
        File file = new File(filePath);

        //如果HTML文件不存在,則創建新文件
        if (!file.exists()) {
            file.createNewFile();
        }

        //生成迷宮圖HTML文件的IO輸出流類
        FileWriter fileWriter = new FileWriter(file);
        BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

        //將上面StringBuffer的HTML內容寫入HTML文件
        bufferedWriter.write(sbuf.toString());

        //衝完緩衝區
        bufferedWriter.flush();

        //關閉輸出流
        bufferedWriter.close();
        fileWriter.close();

        //給出迷宮已生成的提示信息
        System.out.println("迷宮已生成!");
    }

    /**
     * 運行迷宮隨機生成算法,在桌面生成自定義寬高的HTML迷宮文件
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        //自定義迷宮寬
        int width = 24;

        //自定義迷宮高
        int height = 24;

        //最終生成的迷宮文件的路徑
        String filePath = "C:\\Users\\liyang\\Desktop\\maze_ " + System.currentTimeMillis() + ".html";

        //運行迷宮隨機生成算法,在桌面生成HTML迷宮文件
        generateMazeHTML(width, height, filePath);
    }

}

大家可以在main方法中自定義迷宮的寬高和HTML文件的輸出路徑。我這裏是迷宮寬高都爲24,然後輸出路徑是桌面,文件名是 “maze_” 再加上當前時間戳。運行main方法後,控制檯輸出 “迷宮已生成”,代表迷宮生成代碼運行成功。在桌面找到迷宮的HTML文件,在瀏覽器裏面打開後,看到了生成的迷宮圖,如下所示:
在這裏插入圖片描述
大家有興趣可以走一下這個迷宮!這個難度有點低,我們把width改爲60,height改爲40,再運行main方法,再生成一個更大更難的迷宮:
在這裏插入圖片描述
怎麼樣,是不是有一種眼花繚亂的感覺?是不是很好玩?趕緊複製上面的隨機迷宮生成的算法代碼,到你自己的IDE裏面粘貼改參數運行吧!這個週末我們就走迷宮玩吧!O(∩_∩)O哈哈~

發佈了67 篇原創文章 · 獲贊 14 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章