從迷宮到八皇后問(wèn)題認(rèn)識(shí)遞歸與回溯

從迷宮到八皇后問(wèn)題認(rèn)識(shí)遞歸與回溯

迷宮問(wèn)題

迷宮大家都很熟悉蔫磨,給定一個(gè)起點(diǎn)己肮,一個(gè)終點(diǎn),中間有各種復(fù)雜的通路剪廉,從起點(diǎn)走到終點(diǎn)就算是走出了迷宮娃循。

那么如何使用計(jì)算機(jī)機(jī)算出一條迷宮的走法呢?

首先需要先在計(jì)算機(jī)中模擬出一個(gè)迷宮的樣子斗蒋,之前我們提到過(guò)的二維數(shù)組可以用來(lái)表示一個(gè)平面捌斧,用作迷宮的地圖是非常合適的。

我們可以創(chuàng)建一個(gè)二維數(shù)組泉沾,用0表示路捞蚂,1表示墻,即可表示任意方形的迷宮跷究,下面是我在Java中隨便創(chuàng)建的一個(gè)小迷宮姓迅,接下來(lái)我們將用他來(lái)完成對(duì)迷宮問(wèn)題的解決

    /**
     * 創(chuàng)建迷宮,用二維數(shù)組存放返回
     * @return 迷宮
     */
    public static int[][] createMaze(){
        //創(chuàng)建八行七列的地圖
        int[][] map = new int[8][7];
        //用1表示墻,將四周?chē)饋?lái)
        for (int i = 0; i < 7; i++) {
            map[0][i] = 1;
            map[7][i] = 1;
        }
        for (int i = 0; i < 8; i++) {
            map[i][0] = 1;
            map[i][6] = 1;
        }
        //設(shè)置中間的墻
        map[2][1] = 1;
        map[2][3] = 1;
        map[2][4] = 1;
        map[3][4] = 1;
        map[4][2] = 1;
        map[4][3] = 1;
        map[5][3] = 1;
        map[5][5] = 1;
        map[6][2] = 1;

        return map;
    }
    打印一下迷宮看看效果
    /**
     * 打印迷宮(打印二維數(shù)組)
     * @param map 二維數(shù)組
     */
    public static void printMaze(int[][] map){
        for (int[] row : map) {
            for (int data : row) {
                System.out.print(data+"\t");
            }
            System.out.println();
        }
    }
image.png

1是墻俊马,0是路丁存,沒(méi)有什么問(wèn)題,接下來(lái)開(kāi)始分析如何用Java計(jì)算出一條通路柴我。

我們可以讓計(jì)算機(jī)嘗試按照一種策略去窮舉解寝,也就是把能走的路全部走一遍。

我們發(fā)現(xiàn)每一個(gè)點(diǎn)嘗試?yán)^續(xù)往下走艘儒,可以選擇四個(gè)方向上聋伦、下、左界睁、右

那么就要選定一種嘗試的順序策略觉增,比如規(guī)定按照下、右翻斟、上逾礁、左的順序

也就是說(shuō),走下一步的時(shí)候先嘗試向下走访惜,走不通再?lài)L試向右走敞斋,走不通再?lài)L試向上走,走不通再?lài)L試向左走疾牲,都走不通說(shuō)明只要來(lái)到這個(gè)點(diǎn)就已經(jīng)是死路了植捎,我們要回到上一個(gè)點(diǎn)再去嘗試下一種方式

策略定下了,我們來(lái)分析什么樣的路是走不通的路:

  1. 首先如果那個(gè)位置的數(shù)據(jù)為1也就是墻阳柔,走不通
  2. 如果這個(gè)位置是我們來(lái)時(shí)的位置焰枢,再走就成回頭路了,也應(yīng)該走不通
  3. 如果這個(gè)位置是以前嘗試過(guò)的死路舌剂,也不必再走

綜上我們發(fā)現(xiàn)济锄,需要對(duì)走過(guò)的路也做一個(gè)記錄,我們規(guī)定將走過(guò)的路標(biāo)記為2霍转,走過(guò)已經(jīng)確定是死路的點(diǎn)標(biāo)記為3

這時(shí)候荐绝,重點(diǎn)來(lái)了,體會(huì)用遞歸的方式來(lái)完成這種回溯避消,我們?cè)O(shè)計(jì)一個(gè)方法低滩,讓這個(gè)方法的功能是從當(dāng)前點(diǎn)位開(kāi)始召夹,嘗試找到一條通向重點(diǎn)的路,那么思路應(yīng)該是先嘗試向下走恕沫,走到下一個(gè)點(diǎn)监憎,從下一個(gè)點(diǎn)再?lài)L試走到再下一個(gè)點(diǎn)直到走通(到達(dá)終點(diǎn))返回true,或者走不通(將走不通的點(diǎn)標(biāo)記為3)返回false

可以這樣想婶溯,我們用一個(gè)固定的走迷宮的方式(按順序嘗試)鲸阔,從一個(gè)點(diǎn)移動(dòng)到了下一個(gè)點(diǎn),還要再向下一個(gè)點(diǎn)移動(dòng)迄委,那么我們先不管我們是從哪里走過(guò)來(lái)的褐筛,就將當(dāng)前所在這個(gè)點(diǎn)看成是一個(gè)新的起點(diǎn)再去按固定的走迷宮方式嘗試下去。

解釋起來(lái)有點(diǎn)抽象叙身,看到這里應(yīng)該是有點(diǎn)云里霧里渔扎,不知道如何去做,此時(shí)應(yīng)該上代碼了曲梗,結(jié)合上面的話已經(jīng)代碼注釋來(lái)讀一下代碼赞警,應(yīng)該就能夠理解了

我們就使用剛剛創(chuàng)建的那個(gè)迷宮,將[1,1]點(diǎn)作為總起點(diǎn)虏两,[6,5]點(diǎn)作為終點(diǎn)來(lái)實(shí)現(xiàn)

    /**
     * 走迷宮
     * 結(jié)束點(diǎn)寫(xiě)死愧旦,map[6][5]
     * 約定,當(dāng)?shù)貓D[i][j]為0表示路沒(méi)有走過(guò)定罢,為1時(shí)表示墻笤虫,為2時(shí)表示通路可以走,3表示該位置已經(jīng)走過(guò)但是不通
     * 策略——>走迷宮時(shí)祖凫,下-右-上-左琼蚯,走不通再回溯
     *
     * @param map 迷宮地圖
     * @param i 從哪里開(kāi)始(起點(diǎn)的行索引)
     * @param j 從哪里開(kāi)始(起點(diǎn)的列索引)
     * @return 是否能夠找到通路
     */
    public static boolean setWay(int[][] map,int i,int j){
        //進(jìn)入方法先判斷當(dāng)前點(diǎn)是不是終點(diǎn),如果當(dāng)前點(diǎn)就是終點(diǎn)惠况,說(shuō)明已經(jīng)走通了遭庶,返回true
        if (i == 6 && j == 5){
            //已經(jīng)是終點(diǎn)了,設(shè)置這個(gè)點(diǎn)走過(guò)了稠屠,返回true
            map[i][j] = 2;
            return true;
        }
        //沒(méi)返回說(shuō)明當(dāng)前點(diǎn)不是終點(diǎn)峦睡,需要繼續(xù)走
        //再判斷,當(dāng)前點(diǎn)能不能走权埠,如果當(dāng)前點(diǎn)為0(1是墻榨了,2是回頭路,3是死路)攘蔽,表示這個(gè)點(diǎn)可以走
        if (map[i][j] == 0) {
            //路能走龙屉,就先將當(dāng)前點(diǎn)設(shè)置為走過(guò)
            map[i][j] = 2;
            //然后按策略走一遍迷宮——>下->右->上->左
            //先嘗試向下走
            // 把向下走的那個(gè)點(diǎn)再作為起點(diǎn),調(diào)用自己這個(gè)走迷宮的方法
            // 如果走通了就返回true(一層一層返回true最終返回到最外層)
            // 如果走不通返回了false满俗,會(huì)回到這里转捕,接著去嘗試向右走
            // (false也是下面的很多個(gè)點(diǎn)一層一層返回到這里)
            if (setWay2(map,i+1,j)){
                return true;
            }
            //直到向下走不了作岖,改向右走
            else if (setWay2(map,i, j+1)){
                return true;
            }
            //向右也走不通,改向上走
            else if (setWay2(map,i-1,j)){
                return true;
            }
            //向上也走不通瓜富,改向左走
            else if (setWay2(map,i,j-1)){
                return true;
            }else {
                //全都走不通鳍咱,是死路降盹,將當(dāng)前點(diǎn)標(biāo)記為3
                map[i][j] = 3;
                return false;
            }
        }
        //如果當(dāng)前點(diǎn)不為0与柑,返回false
        else {
            return false;
        }

    }
    由于傳入方法的二維數(shù)組是引用類(lèi)型,傳來(lái)的是地址蓄坏,所以方法中對(duì)這個(gè)數(shù)組的操作都會(huì)實(shí)際作用在數(shù)組上价捧,方法結(jié)束后打印數(shù)據(jù)看看效果
    public static void main(String[] args) {
        //創(chuàng)建迷宮
        int[][] maze = createMaze();
        //使用遞歸回溯走迷宮
        setWay(maze,1,1);
        //輸出嘗試走過(guò)的地圖
        printMaze(maze);
    }
image.png

我們看到算法是正確的,找到了一條標(biāo)記為2的通路涡戳,從[1,1]走到[6,5]结蟋,還有一些標(biāo)記為3的也都確實(shí)是走不通的死路。

八皇后問(wèn)題

八皇后問(wèn)題表述為:在8×8格的國(guó)際象棋上擺放8個(gè)皇后渔彰,使其不能互相攻擊嵌屎,即任意兩個(gè)皇后都不能處于同一行、同一列或同一斜線上恍涂,問(wèn)有多少種擺法宝惰。

高斯認(rèn)為有76種方案。1854年在柏林的象棋雜志上不同的作者發(fā)表了40種不同的解再沧,后來(lái)有人用圖論的方法解出92種結(jié)果尼夺。如果經(jīng)過(guò)±90度、±180度旋轉(zhuǎn)炒瘸,和對(duì)角線對(duì)稱(chēng)變換的擺法看成一類(lèi)淤堵,共有42類(lèi)。

但我們這里暫時(shí)先不討論旋轉(zhuǎn)和對(duì)稱(chēng)變換顷扩。

如圖所示就是一種八皇后的擺放方式

image.png

如何使用計(jì)算機(jī)算出一共有多少擺放方式拐邪,并且將所有擺放方式列舉出來(lái)呢?

這里依然是采用一種遞歸+回溯的思路隘截,先分析一下:

  1. 首先分析可以按行考慮或按列考慮扎阶,比如按行考慮,就是說(shuō)第一行放了皇后技俐,下一個(gè)皇后就只能放第二行乘陪,再下一個(gè)就只能放第三行,每一行都去嘗試皇后可以放在那一列來(lái)進(jìn)行遞歸

  2. 一個(gè)棋盤(pán)按說(shuō)可以存進(jìn)一個(gè)二維數(shù)組雕擂,但是皇后的擺放有每一行只能有一個(gè)的限制啡邑,所以可以用一個(gè)一位數(shù)組即可,數(shù)組下標(biāo)表示第幾行井赌,具體數(shù)據(jù)記錄在那一列

  3. 那么就可以先將第一個(gè)皇后放在第一行第一列谤逼,然后去考慮第二行皇后可以放在哪里贵扰、第三行...一直到第八行

  4. 我們的方法可以設(shè)計(jì)為當(dāng)前正在放第幾個(gè)皇后,但是要記錄好之前的皇后都怎么擺的

  5. 假設(shè)我們正在放第n個(gè)皇后

首先需要一個(gè)數(shù)組arr[]來(lái)保存八個(gè)皇后的擺放位置流部,一個(gè)變量count記錄一共有多少種擺法戚绕。

然后進(jìn)行如下步驟:

  1. 在第n行循環(huán)將皇后嘗試擺放在第1到第8列
  2. 嘗試第i列時(shí),將皇后放進(jìn)去(arr[n]=i),再循環(huán)去分析和前n-1行的皇后是否有沖突
  3. 如果沒(méi)有沖突說(shuō)明前n行擺的是可以的枝冀,接著去調(diào)用我們自己舞丛,擺第n+1個(gè)皇后,無(wú)論后面的皇后擺的怎么樣果漾,到方法執(zhí)行完球切,還是要接著去嘗試第i+1列
  4. 如果沖突直接繼續(xù)去嘗試第i+1列
  5. 直到八列都嘗試完,說(shuō)明第n個(gè)皇后后續(xù)所有情況已經(jīng)窮盡了绒障,也就是對(duì)我們的上一層來(lái)說(shuō)吨凑,步驟c擺放第n+1個(gè)皇后方法執(zhí)行完畢了,如果我們就是最外層户辱,說(shuō)明所有情況都窮盡了
  6. 而擺第n個(gè)皇后的時(shí)候鸵钝,要進(jìn)行邊界判斷,如果n變成8了庐镐,說(shuō)明arr[0]到arr[7]八個(gè)皇后都放好了恩商,此時(shí)數(shù)組arr中完整的保存了一種皇后的擺法,將它打印或記錄下來(lái)焚鹊,讓解法總數(shù)+1
  7. 還有一個(gè)重點(diǎn)痕届,就是如何判斷沖突,行沖突不用判斷因?yàn)榉椒ň褪菑男谐霭l(fā)末患,每行一定只有一個(gè)跛锌,還需判斷列沖突和對(duì)角線沖突亿傅,列沖突非常簡(jiǎn)單演怎,判斷數(shù)組中第n個(gè)和前n-1個(gè)有沒(méi)有相同即可十厢,判斷對(duì)角看似非常復(fù)雜,但只要借助一個(gè)叫做斜率的概念即可簡(jiǎn)單解決探橱,觀察可發(fā)現(xiàn)申屹,如果是在一個(gè)對(duì)角線,這個(gè)對(duì)角線一定屬于一個(gè)正方形隧膏,正方形對(duì)角線斜率為1哗讥,也就是第n個(gè)皇后和前面第i個(gè)皇后,如果在一個(gè)對(duì)角線胞枕,那么n-i == arr[n]-arr[i]杆煞。

八皇后比起剛剛迷宮只找一條通路應(yīng)該是要難理解一些的,這種遞歸+回溯的方式,確實(shí)需要一定的想象力决乎,下面在結(jié)合注釋閱讀一下代碼队询,慢慢體會(huì)這種算法

package com.zwx.recursion;

import java.util.Arrays;

/**
 * 八皇后問(wèn)題(遞歸回溯)
 *
 * @author coderZWX
 * @date 2022-05-31 15:41
 */
public class EightQueens {

    //定義一個(gè)max表示共有多少個(gè)皇后
    private int maxQueensCount = 8;
    //定義一個(gè)數(shù)組,保存皇后放置位置的結(jié)果构诚,比如arr={0,4,7,5,2,6,1,3}
    private int[] array = new int[maxQueensCount];
    //解法總數(shù)
    private int count;

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        EightQueens queens = new EightQueens();
        queens.check(0);
        System.out.println(queens.count);
        System.out.println("用時(shí)(毫秒):"+(System.currentTimeMillis()-startTime));
    }

    /**
     * 放置第n個(gè)皇后
     * @param n n表示數(shù)組下標(biāo)蚌斩,第一個(gè)皇后其實(shí)n為0
     */
    private void check(int n){
        //判斷n是否越界,如果越界到第九個(gè)范嘱,說(shuō)明已經(jīng)八個(gè)皇后已經(jīng)放好了
        if (n == maxQueensCount){
            //打印一個(gè)結(jié)果
            System.out.println(Arrays.toString(array));
            //記錄結(jié)果數(shù)量加一
            count++;
            return;
        }
        //依次放入皇后并判斷是否沖突
        for (int i = 0; i < maxQueensCount; i++) {
            //先把當(dāng)前皇后n送膳,放到該行的第i列
            array[n] = i;
            //判斷是否沖突
            // 不論是否沖突,都會(huì)最終i++接著執(zhí)行下一次循環(huán)
            // 如果沖突就直接進(jìn)下一次循環(huán)接著嘗試下一列
            // 不沖突在放完后面皇后之后也會(huì)進(jìn)下一次循環(huán)嘗試下一列
            if (judge(n)){
                //不沖突接著放n+1個(gè)皇后
                check(n+1);
            }
        }
        //如果出了這個(gè)大循環(huán)
        // 說(shuō)明第n個(gè)皇后在第n行放哪里都不行彤侍,也就是前面的擺法都是錯(cuò)的
        // 方法到這里就結(jié)束了肠缨,如果是內(nèi)層逆趋,它的外層還會(huì)在外層的for接著循環(huán)
    }

    /**
     * 查看當(dāng)放置第n個(gè)皇后時(shí)盏阶,是否沖突
     *
     * @param n 第n個(gè)皇后
     * @return 是否沖突
     */
    private boolean judge(int n){
        for (int i = 0; i < n; i++) {
            //1.array[i] == array[n] 同一列沖突
            //2.Math.abs(n-i) == Math.abs(array[n]-array[i]) 同一斜線沖突(相當(dāng)于判斷了兩點(diǎn)間斜率為1)
            if (array[i] == array[n] || Math.abs(n-i) == Math.abs(array[n]-array[i])){
                return false;
            }
        }
        return true;
    }
    
}

根據(jù)上面代碼,最終打印出了所有情況闻书,count也就是92名斟,沒(méi)有問(wèn)題

image.png

小練習(xí)——迷宮問(wèn)題的最短路徑

我們的迷宮問(wèn)題,為了循序漸進(jìn)魄眉,在前面的算法中只是找到了其中一條通路就結(jié)束了砰盐,所以理解起來(lái)比八皇后這個(gè)簡(jiǎn)單了不少,相當(dāng)于八皇后問(wèn)題只找到一種擺放方式坑律。

那么現(xiàn)在經(jīng)過(guò)了這兩個(gè)算法的學(xué)習(xí)岩梳,結(jié)合迷宮問(wèn)題找一條通路的方法、以及八皇后問(wèn)題的思路晃择,是否可以自行嘗試寫(xiě)一個(gè)算法冀值,窮舉一下迷宮的所有通路,記錄通路數(shù)量宫屠,并找到其中的最短路徑

可評(píng)論留言獲得小練習(xí)的實(shí)現(xiàn)(附詳細(xì)的注釋講解)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末列疗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子浪蹂,更是在濱河造成了極大的恐慌抵栈,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坤次,死亡現(xiàn)場(chǎng)離奇詭異古劲,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)缰猴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)产艾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事胰舆∩叮” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵缚窿,是天一觀的道長(zhǎng)棘幸。 經(jīng)常有香客問(wèn)我,道長(zhǎng)倦零,這世上最難降的妖魔是什么误续? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮扫茅,結(jié)果婚禮上蹋嵌,老公的妹妹穿的比我還像新娘。我一直安慰自己葫隙,他們只是感情好栽烂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著恋脚,像睡著了一般腺办。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上糟描,一...
    開(kāi)封第一講書(shū)人閱讀 51,462評(píng)論 1 302
  • 那天怀喉,我揣著相機(jī)與錄音,去河邊找鬼船响。 笑死躬拢,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的见间。 我是一名探鬼主播聊闯,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼缤剧!你這毒婦竟也來(lái)了馅袁?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤荒辕,失蹤者是張志新(化名)和其女友劉穎汗销,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體抵窒,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弛针,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了李皇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片削茁。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡宙枷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出茧跋,到底是詐尸還是另有隱情慰丛,我是刑警寧澤,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布瘾杭,位于F島的核電站诅病,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏粥烁。R本人自食惡果不足惜贤笆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望讨阻。 院中可真熱鬧芥永,春花似錦、人聲如沸钝吮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)搀绣。三九已至飞袋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間链患,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工瓶您, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留麻捻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓呀袱,卻偏偏與公主長(zhǎng)得像贸毕,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子夜赵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容