01-回溯

回溯

回溯法的基本思想:回溯法在包含問題的所有可能解的解空間樹中,從根結(jié)點(diǎn)出發(fā)虱黄,按照深度優(yōu)先的策略進(jìn)行搜索橱乱,對于解空間樹的某個結(jié)點(diǎn)泳叠,如果該結(jié)點(diǎn)滿足問題的約束條件,則進(jìn)入該子樹繼續(xù)進(jìn)行搜索宗挥,否則將以該結(jié)點(diǎn)為根結(jié)點(diǎn)的子樹進(jìn)行剪枝契耿。

回溯法的算法框架按照問題的解空間一般分為子集樹算法框架與排列樹算法框架螃征。

  1. 當(dāng)給定的問題是從n個元素的集合S中找出滿足某種性質(zhì)的子集時盯滚,相應(yīng)的解空間樹稱為子集樹魄藕。

  2. 當(dāng)給定的問題是確定 n 個元素滿足某種性質(zhì)的排列時背率,對應(yīng)的解空間樹稱為排列樹嫩与;排列樹通常有n!個葉子結(jié)點(diǎn)蕴纳。

回溯法解題的關(guān)鍵要素:

  1. 針對給定的問題个粱,定義問題的解空間
  2. 確定易于搜索的解空間結(jié)構(gòu)
  3. 以深度優(yōu)先方式搜索解空間古毛,并且在搜索過程中用剪枝函數(shù)避免無效搜索

1. 迷宮回溯問題

import java.util.Random;

// 迷宮回溯問題
public class MazeBack {
    public static void main(String[] args) {
        // 先創(chuàng)建一個二維數(shù)組都许,模擬迷宮地圖
        int[][] map = new int[9][9];
        layoutMaze(map, 9); // 給迷宮布局
        // 使用遞歸回溯找通路
        boolean success = setWay(map, 1, 1);
        if (success)
            System.out.println("找到通路:");
        else
            System.out.println("沒有通路:");
        display(map); // 顯示地圖
    }

    /**
     * 從左上角開始出發(fā)稻薇,右下角為終點(diǎn)
     * 0表示沒走過;1表示墻胶征;2表示通路睛低;3表示已經(jīng)走過但走不通
     * 尋路策略:下-->右-->上-->左钱雷,如果走不通拉庵,再回溯
     *
     * @param map 表示地圖
     * @param i   表示起始位置
     * @param j   表示從起始位置
     * @return 如果找到通路钞支,則返回true烁挟,否則返回false
     */
    public static boolean setWay(int[][] map, int i, int j) {
        if (map[map.length - 2][map[0].length - 2] == 2) {
            // 遞歸終止條件撼嗓,通路已經(jīng)找到
            return true;
        } else if (map[i][j] == 0) { // 如果當(dāng)前這個點(diǎn)還沒走過
            map[i][j] = 2; // 假定該點(diǎn)可以走通
            if (setWay(map, i + 1, j)) { // 向下走
                return true;
            } else if (setWay(map, i, j + 1)) { // 向右走
                return true;
            } else if (setWay(map, i - 1, j)) { // 向上走
                return true;
            } else if (setWay(map, i, j - 1)) { // 向左走
                return true;
            } else { // 該點(diǎn)走不通警没,是死路
                map[i][j] = 3;
                return false;
            }
        } else {
            // 1:走不通
            // 2:走過了,不用再走
            // 3:走過了走不通
            // 因此如果map[i][j] != 0亡脸,說明該點(diǎn)不用走大州,直接返回false
            return false;
        }
    }

    // 初始化地圖
    public static void initMap(int[][] map) {
        for (int r = 0; r < map.length; r++) {
            for (int c = 0; c < map[0].length; c++) {
                // 將地圖四周設(shè)為1
                if (r == 0 || r == map.length - 1 || c == 0 || c == map[0].length - 1)
                    map[r][c] = 1;
                else
                    map[r][c] = 0;
            }
        }
    }

    // 給迷宮布局
    public static void layoutMaze(int[][] map, int total) {
        initMap(map); // 先初始化地圖
        for (int i = 0; i < total; i++) {
            Random random = new Random();
            int r = random.nextInt(map.length - 2) + 1;
            int c = random.nextInt(map[0].length - 2) + 1;
            map[r][c] = 1;
        }
    }

    // 顯示地圖
    public static void display(int[][] map) {
        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[0].length; j++) {
                System.out.print(map[i][j] + " ");
            }
            System.out.println();
        }
    }
}

2. 八皇后問題

八皇后問題遞歸思路:

  1. 第一個皇后先放第一列第一行
  2. 第二個皇后放第二行第一列滥朱,然后判斷是否OK徙邻,如果不OK缰犁,繼續(xù)放第二列帅容、第三列...依次把所有列放完夯到,找到一個合適的耍贾。
  3. 繼續(xù)放第三個皇后荐开,還是第一列晃听、第二列...直到第8個皇后也放在一個不沖突的位置,算找到了一個正確解初斑。
  4. 當(dāng)?shù)玫揭粋€正確解時见秤,在椇醭危回退到上一個棧時置济,就開始回溯舟肉,即得到第一個皇后放在第一列的所有正確解路媚。
  5. 然后回頭繼續(xù)第一個皇后放第二列,后面繼續(xù)循環(huán)執(zhí)行1裤园,2拧揽,3,4步驟铡羡。
  • 理論上應(yīng)該創(chuàng)建一個二維數(shù)組表示棋盤,但實(shí)際上通過算法读慎,用一個一維數(shù)組即可(下標(biāo)為行,值為列)闰靴。

八皇后問題實(shí)現(xiàn):

import java.util.Arrays;

// 八皇后問題遞歸實(shí)現(xiàn)
public class NQueens {
    static int max = 8; // 一共有幾個皇后
    static int[] arr = new int[max]; // 保存一個結(jié)果
    static int total = 0;
    static int judgeCount = 0;

    public static void main(String[] args) {
        check(0);
        System.out.println(max + "個皇后解法:" + total); // 92
        System.out.println("判斷次數(shù):" + judgeCount); // 15720
    }

    // 放置第n個皇后
    public static void check(int n) {
        if (n == max) {
            // max個皇后已經(jīng)放好,顯示結(jié)果
            total++;
            System.out.println(Arrays.toString(arr));
            return;
        }
        // 依次放入皇后,并判斷是否沖突
        for (int i = 0; i < max; i++) {
            // 先把當(dāng)前皇后n放到該行第一列
            arr[n] = i;
            // 判斷第n個皇后在i位置是否沖突
            if (Conflict(n) == false) { // 不沖突
                // 接著放n+1個皇后淑翼,即開始遞歸
                check(n + 1);
            }
            // 如果沖突,繼續(xù)循環(huán)遭京,將第n個皇后后移一列
        }
    }

    // 檢測第n個皇后是否與已擺放的皇后沖突
    public static boolean Conflict(int n) {
        judgeCount++;
        for (int i = 0; i < n; i++) {
            // 檢測是否在同一列或同一斜線(正方形長寬相等)
            if (arr[i] == arr[n] || Math.abs(n - i) == Math.abs(arr[n] - arr[i]))
                return true;
        }
        return false;
    }
}

3. 馬踏棋盤問題

馬踏棋盤問題也稱騎士周游問題,是旅行商問題(TSP)或哈密頓回路問題(HCP)的一個特例。在8×8的國際象棋棋盤上堡僻,用一個馬跟隨馬步跳遍整個棋盤,要求每個格子都只跳到一次,最后回到出發(fā)點(diǎn)咨油。這是一個NP問題,通常采用回溯法或啟發(fā)式搜索類算法轉(zhuǎn)化棉胀。

馬踏棋盤問題解決思路:

  1. 將當(dāng)前位置設(shè)置為已經(jīng)訪問,然后根據(jù)當(dāng)前位置霎挟,計算還能走哪些位置赐纱,并將這些位置放入到一個集合中;最多有8個位置起胰,每走一步就讓step增1
  2. 遍歷集合中存放的所有位置,看哪個可以走通;如果走通瓜客,就繼續(xù),走不通,就回溯
  3. 使用step和應(yīng)該走的步數(shù)比較,來判斷應(yīng)該走的步數(shù)是否走完了砂吞;若不相等,則將整個棋盤置0
  • 不同的走法(策略)呼巷,得到的結(jié)果可能不同,效率也會有影響(可優(yōu)化)

馬在當(dāng)前位置可以踏的位置:

import java.awt.*;
import java.util.ArrayList;
import java.util.List;

public class Backtracking {
    static int x = 8; // 棋盤行
    static int y = 8; // 棋盤列
    static int[][] chessboard = new int[x][y]; // 棋盤
    static boolean[] visited = new boolean[x * y]; // 標(biāo)記棋盤的各個位置是否訪問過
    static boolean finished = false; // 應(yīng)該走的步數(shù)是否走完了

    // 回溯法解決馬踏棋盤問題
    public static void knightTour(int row, int column, int step) {
        // 第step步訪問該位置
        chessboard[row][column] = step;
        // 將該位置標(biāo)記為已訪問
        visited[row * x + column] = true;
        // 根據(jù)當(dāng)前位置,計算還能走哪些位置
        List<Point> next = next(new Point(column, row));
        /**
         * 對next按照可走步數(shù)進(jìn)行升序排序,減少回溯的次數(shù):
         * 不同的走法(策略),得到的結(jié)果可能不同
         * 貪心選擇策略:選擇可走步數(shù)最少的位置
         */
        next.sort(((o1, o2) -> next(o1).size() - next(o2).size()));
        // 遍歷每一個還能走哪些位置
        for (Point point : next) {
            // 如果該位置還沒有被訪問,則訪問這個位置
            if (!visited[point.y * x + point.x])
                knightTour(point.y, point.x, step + 1);
        }
        // 判斷棋盤是否踏完傻工,如果沒踏完,將整個棋盤置0
        // step < x * y - 1有兩種情況:1. 棋盤還沒踏完    2. 棋盤正在回溯
        if (step < x * y - 1 && !finished) {
            chessboard[row][column] = 0;
            visited[row * x + column] = false;
        } else {
            finished = true;
        }
    }

    // 根據(jù)當(dāng)前位置(Point為Java的內(nèi)置對象),計算還能走哪些位置,最多有8個位置
    // 返回還能走的位置的坐標(biāo)集合
    private static List<Point> next(Point current) {
        List<Point> points = new ArrayList<>();
        Point point = new Point();
        // 可以走0這個位置
        if ((point.x = current.x + 2) < x && (point.y = current.y - 1) >= 0)
            points.add(new Point(point));
        // 可以走1這個位置
        if ((point.x = current.x + 2) < x && (point.y = current.y + 1) < y)
            points.add(new Point(point));
        // 可以走2這個位置
        if ((point.x = current.x + 1) < x && (point.y = current.y + 2) < y)
            points.add(new Point(point));
        // 可以走3這個位置
        if ((point.x = current.x - 1) >= 0 && (point.y = current.y + 2) < y)
            points.add(new Point(point));
        // 可以走4這個位置
        if ((point.x = current.x - 2) >= 0 && (point.y = current.y + 1) < y)
            points.add(new Point(point));
        // 可以走5這個位置
        if ((point.x = current.x - 2) >= 0 && (point.y = current.y - 1) >= 0)
            points.add(new Point(point));
        // 可以走6這個位置
        if ((point.x = current.x - 1) >= 0 && (point.y = current.y - 2) >= 0)
            points.add(new Point(point));
        // 可以走7這個位置
        if ((point.x = current.x + 1) < x && (point.y = current.y - 2) >= 0)
            points.add(new Point(point));
        return points;
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        knightTour(0, 0, 0);
        long end = System.currentTimeMillis();
        System.out.println("馬踏棋盤耗時" + (end - start) + "毫秒,結(jié)果為:");
        for (int r = 0; r < chessboard.length; r++) {
            for (int c = 0; c < chessboard[r].length; c++) {
                System.out.printf("%4d", chessboard[r][c]);
            }
            System.out.println();
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌划乖,老刑警劉巖仰美,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異止邮,居然都是意外死亡埃唯,警方通過查閱死者的電腦和手機(jī)止毕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進(jìn)店門漠趁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人闯传,你說我怎么就攤上這事谨朝。” “怎么了?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵则披,是天一觀的道長洗出。 經(jīng)常有香客問我士复,道長,這世上最難降的妖魔是什么翩活? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任阱洪,我火速辦了婚禮,結(jié)果婚禮上冗荸,老公的妹妹穿的比我還像新娘辟犀。我一直安慰自己俏竞,他們只是感情好堂竟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布出嘹。 她就那樣靜靜地躺著,像睡著了一般税稼。 火紅的嫁衣襯著肌膚如雪垮斯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天兜蠕,我揣著相機(jī)與錄音,去河邊找鬼曙旭。 笑死晶府,一個胖子當(dāng)著我的面吹牛桂躏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播剂习,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼土至!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起猾昆,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤垂蜗,失蹤者是張志新(化名)和其女友劉穎楷扬,沒想到半個月后贴见,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镣衡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年档悠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惰说。...
    茶點(diǎn)故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡缘回,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出啦吧,到底是詐尸還是另有隱情,我是刑警寧澤授滓,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布倒庵,位于F島的核電站,受9級特大地震影響擎宝,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜噩咪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望涨享。 院中可真熱鬧仆百,春花似錦厕隧、人聲如沸俄周。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽波势。三九已至,卻和暖如春尺铣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背迄埃。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工侄非, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留流译,地道東北人。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓福澡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親除秀。 傳聞我的和親對象是個殘疾皇子册踩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評論 2 354

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