算法基礎(chǔ)-深度優(yōu)先搜索

深度優(yōu)先搜索(DFS)和廣度優(yōu)先搜BFS)是圖論相關(guān)算法的基礎(chǔ),先學(xué)習(xí)這兩個(gè)思想(工具)為后續(xù)學(xué)習(xí)更多算法做準(zhǔn)備型檀。

迷宮式搜索

學(xué)習(xí)深搜通常用走迷宮這一問(wèn)題來(lái)入門。

一個(gè)機(jī)器人位于一個(gè) m x n 網(wǎng)格的左上角 (起始點(diǎn)在下圖中標(biāo)記為“Start” )。

機(jī)器人每次只能向下或者向右移動(dòng)一步。機(jī)器人試圖達(dá)到網(wǎng)格的右下角(在下圖中標(biāo)記為“Finish”)售睹。

問(wèn)總共有多少條不同的路徑?

image.png

例如可训,上圖是一個(gè)7 x 3 的網(wǎng)格昌妹。有多少可能的路徑生真?

來(lái)源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/unique-paths

首先創(chuàng)建一個(gè)二維數(shù)組,作為地圖int[3][7] map;(int[3][7]或者int[7][3]都行并不影響捺宗,就按照?qǐng)D上定義3行7列)定義i為行數(shù),j為列數(shù)川蒙,初始點(diǎn)為(0,0)蚜厉,終點(diǎn)為(2,6)。

據(jù)DFS的思想畜眨,我們先設(shè)定一個(gè)初始方向→昼牛,即j+1。
模擬下走路過(guò)程
(0,0)->(0,1)->(0,2)->(0,3)->(0,4)->(0,5)->(0,6)
當(dāng)橫向走到頭時(shí)康聂,按照第二方向↓贰健,即i+1繼續(xù)移動(dòng)
(0,6)->(1,6)
繼續(xù)往→不通,向下移動(dòng)
(1,6)->(2,6)
走到了終點(diǎn)處恬汁,計(jì)有一條路徑從(0,0)->...(2,6)

接著是一個(gè)回溯的過(guò)程伶椿,因?yàn)闂l件中只允許往右和往下走,執(zhí)行完兩個(gè)情況后即退出遞歸氓侧,從
(2,6)->(1,6)->(0,6)->(0,5)
繼續(xù)執(zhí)行從(0,5)點(diǎn)往下走到(1,5)->(1,6)->(2,6),又走到了終點(diǎn)脊另。

接下來(lái)看代碼實(shí)現(xiàn)

private static int count;

public static void main(String[] args) {
    count = 0;

    dfs(0, 0, 3, 7);

    System.out.println(count);
}

/**
 * @param x 當(dāng)前所在行
 * @param y 當(dāng)前所在列
 * @param m 總行數(shù)
 * @param n 總列數(shù)
 */
public static void dfs(int x, int y, int m, int n) {
    //在地圖外不能走直接返回
    if (x >= m || y >= n) {
        return;
    }

    //到達(dá)終點(diǎn),記一條路徑
    if (x == m - 1 && y == n - 1) {
        count++;
        System.out.println("到達(dá)終點(diǎn)累計(jì):"+count);
        return;
    }
    //向右走
    System.out.println("(" + x + "," + y + ")向右走到" + "(" + x + "," + (y + 1) + ")");
    dfs(x, y + 1, m, n);
    System.out.println("(" + x + "," + (y + 1) + ")退回" + "(" + x + "," + y + ")");


    //向下走
    System.out.println("(" + x + "," + y + ")-向下走到" + "(" + (x + 1) + "," + y + ")");
    dfs(x + 1, y, m, n);
    System.out.println("(" + (x + 1) + "," + y + ")退回" + "(" + x + 1 + "," + y + ")");

}

通過(guò)打印日志和調(diào)試代碼的方式能夠了解深搜的過(guò)程约巷。(leetcode上這題用深搜決定超時(shí)的偎痛,原題是直接動(dòng)態(tài)規(guī)劃或者排列組合,只是題目比較形象來(lái)演示這個(gè)例子独郎。根據(jù)經(jīng)驗(yàn)?zāi)苁褂蒙钏训念}的數(shù)據(jù)大概60以下踩麦,太深的層數(shù)直接棧溢出了)

63. 不同路徑 II

這題是包含障礙不能走的情況,可以自己試試氓癌。

二叉樹搜索

在數(shù)據(jù)結(jié)構(gòu)的題目中谓谦,經(jīng)常有一些如打印中序遍歷、先序遍歷顽铸、后序遍歷茁计,最深層級(jí)之類的題。這些題都是通過(guò)深搜去查找谓松,去遍歷星压。

二叉樹的結(jié)構(gòu)與上面迷宮比較類似,先早左節(jié)點(diǎn)再找右節(jié)點(diǎn)(反之也隨意看題目需要)鬼譬。

private void dfs(TreeNode root) {

        dfs(root.left);

        dfs(root.right);
}

一個(gè)簡(jiǎn)單的二叉樹搜索的框架就搭好了娜膘。
除了向下搜索的過(guò)程,還有必要的返回條件优质】⑻埃總不能一直往死里搜军洼,搜他個(gè)棧溢出吧。

private void dfs(TreeNode root) {

    if (root == null) {
        return;
    }
    dfs(root.left);

    dfs(root.right);
}

有了以上二叉樹搜索的模板演怎,同上面一樣拿個(gè)題做一下匕争。

給定一個(gè)二叉樹,找出其最大深度爷耀。

二叉樹的深度為根節(jié)點(diǎn)到最遠(yuǎn)葉子節(jié)點(diǎn)的最長(zhǎng)路徑上的節(jié)點(diǎn)數(shù)甘桑。

說(shuō)明: 葉子節(jié)點(diǎn)是指沒(méi)有子節(jié)點(diǎn)的節(jié)點(diǎn)。

示例:
給定二叉樹 [3,9,20,null,null,15,7]歹叮,

3

/
9 20
/
15 7
返回它的最大深度 3 跑杭。

來(lái)源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/maximum-depth-of-binary-tree

求的是樹的最大深度,我們需要知道當(dāng)前節(jié)點(diǎn)的深度咆耿,往下搜則深度+1德谅,取左節(jié)點(diǎn)與右節(jié)點(diǎn)中較大的數(shù),即為最終結(jié)果萨螺。

代碼實(shí)現(xiàn)窄做。

/**
 * @param root 當(dāng)前節(jié)點(diǎn)
 * @param cur  當(dāng)前深度
 * @return
 */
private int dfs(TreeNode root, int cur) {
    if (root == null) {
        return cur;
    }

    cur++;
    int lh = dfs(root.left, cur);
    int rh = dfs(root.right, cur);


    return Math.max(lh, rh);
}

抽象型搜索

以上兩個(gè)例子是比較具體化的深搜例子,可以明確搜的方向(步驟)⌒加兀現(xiàn)在來(lái)解決一個(gè)常見的趣味題浸策。
有兩個(gè)容量分別為 x升 和 y升 的水壺以及無(wú)限多的水。請(qǐng)判斷能否通過(guò)使用這兩個(gè)水壺惹盼,從而可以得到恰好 z升 的水庸汗?

如果可以,最后請(qǐng)用以上水壺中的一或兩個(gè)來(lái)盛放取得的 z升 水手报。

你允許:

裝滿任意一個(gè)水壺
清空任意一個(gè)水壺
從一個(gè)水壺向另外一個(gè)水壺倒水蚯舱,直到裝滿或者倒空
示例 1: (From the famous "Die Hard" example)

輸入: x = 3, y = 5, z = 4
輸出: True
示例 2:

輸入: x = 2, y = 6, z = 5
輸出: False

來(lái)源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/water-and-jug-problem

本題就沒(méi)有上面走迷宮和二叉樹那么直觀的具體的搜索路徑,且方法不止深搜掩蛤,但是我們?nèi)钥梢园凑丈钏训乃枷雵L試解決枉昏。

首先,列出所有可能的操作行為

  1. 給x水壺中倒水
  2. 給y水壺中倒水
  3. 將x水壺中的水倒到y(tǒng)水壺
  4. 將y水壺中的水倒到x水壺
  5. 將x水壺中的水倒掉
  6. 將y水壺中的水倒掉

根據(jù)搜索狀態(tài)寫出遞歸代碼

/**
 * @param i 當(dāng)前x水壺中的水量
 * @param j 當(dāng)前y水壺中的水量
 * @param x x水壺的容量
 * @param y y水壺的容量
 * @param z 目標(biāo)水量
 */
private void dfs(int i, int j, int x, int y, int z) {

    // 1.給x水壺中倒水
    dfs(x, j, x, y, z);
    // 2.給y水壺中倒水
    dfs(i, y, x, y, z);

    //3.將x水壺中的水倒到y(tǒng)水壺,這里分倒不滿和倒不下的情況
    if (j + i <= y) {
        //倒不滿y水壺(剛好)
        dfs(0, j + i, x, y, z);
    } else {
        //倒?jié)My水壺剩下的水保留在x水壺中
        dfs(i + j - y, y, x, y, z);
    }

    //4.將y水壺中的水倒到x水壺,同樣分倒不滿和倒不下的情況
    if (j + i < x) {
        dfs(j + i, 0, x, y, z);
    } else {
        dfs(x, i + j - x, x, y, z);
    }
    
    //5.將x水壺中的水倒掉
    dfs(0, j, x, y, z);
    //6.將y水壺中的水倒掉
    dfs(i, 0, x, y, z);
}

接著寫遞歸邊界揍鸟,當(dāng)x或y中的水與目標(biāo)水量z相同時(shí)兄裂,或兩個(gè)水壺中的水加起來(lái)與z相同時(shí),即得到了結(jié)果阳藻。

private boolean dfs(int i, int j, int x, int y, int z) {

    if (i == z || j == z || (i + j == z)) {
        return true;
    }
    ......
}

加上打印當(dāng)年?duì)顟B(tài)方法一運(yùn)行
System.out.println("x水壺水量:" + i + " y水壺水量:" + j);

......
x水壺水量:5 y水壺水量:0
x水壺水量:5 y水壺水量:0
x水壺水量:5 y水壺水量:0
Exception in thread "main" java.lang.StackOverflowError

想到水滿的情況不需要再加水和水空的時(shí)候不需再倒水的情況晰奖,于是加上遞歸條件,優(yōu)化了下代碼腥泥。

private boolean dfs(int i, int j, int x, int y, int z) {

    ......

    // 1.給x水壺中倒水
    if (i != x) {
        if (dfs(x, j, x, y, z)) {
            return true;
        }
    }

    // 2.給y水壺中倒水
    if (j != y) {
        if (dfs(i, y, x, y, z)) {
            return true;
        }
    }
    // y水壺滿了不能倒
    if (j != y) {
        //3.將x水壺中的水倒到y(tǒng)水壺,這里分倒不滿和倒不下的情況
        if (j + i <= y) {
            //倒不滿y水壺(剛好)
            if (dfs(0, j + i, x, y, z)) {
                return true;
            }
        } else {
            //倒?jié)My水壺剩下的水保留在x水壺中
            if (dfs(i + j - y, y, x, y, z)) {
                return true;
            }
        }
    }

    // x水壺滿了不能倒
    if (i != x) {
        //4.將y水壺中的水倒到x水壺,同樣分倒不滿和倒不下的情況
        if (j + i < x) {
            if (dfs(j + i, 0, x, y, z)) {
                return true;
            }
        } else {
            if (dfs(x, i + j - x, x, y, z)) {
                return true;
            }
        }
    }

    //5.將x水壺中的水倒掉
    if (i != 0) {
        if (dfs(0, j, x, y, z)) {
            return true;
        }
    }
    //6.將y水壺中的水倒掉
    if (j != 0) {
        if (dfs(i, 0, x, y, z)) {
            return true;
        }
    }
    return false;

}

仍然匾南。。蛔外。

x水壺水量:0 y水壺水量:3
x水壺水量:5 y水壺水量:3
x水壺水量:0 y水壺水量:3
x水壺水量:5 y水壺水量:3
Exception in thread "main" java.lang.StackOverflowError

實(shí)際問(wèn)題與上面一樣蛆楞,出現(xiàn)了重復(fù)的狀態(tài)溯乒。于是我們尋求一種方法去記錄已經(jīng)出現(xiàn)過(guò)的(i,j)狀態(tài)。這里我采用的是二維數(shù)組記錄的方式豹爹,類似x裆悄、y已經(jīng)走過(guò),就標(biāo)記為1臂聋,下次變成這個(gè)狀態(tài)的時(shí)候就直接返回不往下遞歸灯帮。

private boolean[][] map;

public boolean canMeasureWater(int x, int y, int z) {

    map = new boolean[x + 1][y + 1];
    return dfs(0, 0, x, y, z);
}

private boolean dfs(int i, int j, int x, int y, int z) {

    //已經(jīng)走過(guò)該狀態(tài),直接返回
    if (map[i][j]) {
        return false;
    }
    //標(biāo)位已走
    map[i][j] = true;
    ......
}

提交代碼完事關(guān)機(jī)逻住。 結(jié)果。迎献。瞎访。。
20 / 34 個(gè)通過(guò)測(cè)試用例
狀態(tài):超出內(nèi)存限制
最后執(zhí)行的輸入:
22003
31237
1

其實(shí)到這里吁恍,深搜部分要講的已經(jīng)講完了扒秸,但是題目還是要A掉。(map這種方式超內(nèi)存冀瓦。用String hash = String.format("%09d%09d", i, j)拼接成hash值極其費(fèi)時(shí)伴奥。用遞歸深搜的方式又java.lang.StackOverflowError,最后勉強(qiáng)卡掉)

總結(jié)

通過(guò)上面幾個(gè)例題翼闽,可以總結(jié)出寫深搜題的一些套路拾徙。

  1. 狀態(tài)如何改變,通過(guò)遞歸去遍歷(dfs(x,x,x))感局;
  2. 設(shè)置邊界條件尼啡,搜到頭了需要回溯,不可能無(wú)邊界的遞歸下去(return)询微;
  3. 通過(guò)一些條件的判斷進(jìn)行剪枝崖瞭,減少遞歸的層級(jí)和次數(shù)。(if(xxx))

搜索的題目還是蠻有意思的撑毛,一個(gè)題目如果問(wèn)有沒(méi)有結(jié)果书聚,能不能到達(dá),或者有幾種方式(基本超時(shí))藻雌,都能夠用深搜的方式去解決雌续,不一定能解決題目,但可能可以從答案中反推規(guī)律蹦疑。
比較經(jīng)典的還有 八皇后 Sticks等西雀,有興趣的可以自行嘗試。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末歉摧,一起剝皮案震驚了整個(gè)濱河市艇肴,隨后出現(xiàn)的幾起案子腔呜,更是在濱河造成了極大的恐慌,老刑警劉巖再悼,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件核畴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡冲九,警方通過(guò)查閱死者的電腦和手機(jī)谤草,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)莺奸,“玉大人丑孩,你說(shuō)我怎么就攤上這事∶鸫” “怎么了温学?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)甚疟。 經(jīng)常有香客問(wèn)我仗岖,道長(zhǎng),這世上最難降的妖魔是什么览妖? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任轧拄,我火速辦了婚禮,結(jié)果婚禮上讽膏,老公的妹妹穿的比我還像新娘檩电。我一直安慰自己,他們只是感情好府树,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布是嗜。 她就那樣靜靜地躺著,像睡著了一般挺尾。 火紅的嫁衣襯著肌膚如雪鹅搪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天遭铺,我揣著相機(jī)與錄音丽柿,去河邊找鬼。 笑死魂挂,一個(gè)胖子當(dāng)著我的面吹牛甫题,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播涂召,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼坠非,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了果正?” 一聲冷哼從身側(cè)響起炎码,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤盟迟,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后潦闲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體攒菠,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年歉闰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了辖众。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡和敬,死狀恐怖凹炸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情昼弟,我是刑警寧澤还惠,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站私杜,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏救欧。R本人自食惡果不足惜衰粹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望笆怠。 院中可真熱鬧铝耻,春花似錦、人聲如沸蹬刷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)办成。三九已至泡态,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間迂卢,已是汗流浹背某弦。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留而克,地道東北人靶壮。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像员萍,于是被迫代替她去往敵國(guó)和親腾降。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

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

  • 目錄 0.樹0.1 一般樹的定義0.2 二叉樹的定義 1.查找樹ADT 2.查找樹的實(shí)現(xiàn)2.1 二叉查找樹2.2 ...
    王偵閱讀 7,148評(píng)論 0 3
  • 更多干貨就在我的個(gè)人博客 BlackBlog.tech 歡迎關(guān)注碎绎!也可以關(guān)注我的csdn博客:黑哥的博客謝謝大家螃壤!...
    BlackBlog__閱讀 4,191評(píng)論 0 3
  • 樹形動(dòng)態(tài)規(guī)劃抗果,顧名思義就是樹+DP,先分別回顧一下基本內(nèi)容吧:動(dòng)態(tài)規(guī)劃:?jiǎn)栴}可以分解成若干相互聯(lián)系的階段映穗,在每一個(gè)...
    Mr_chong閱讀 1,475評(píng)論 0 2
  • 2019 iOS面試題大全---全方面剖析面試2018 iOS面試題---算法相關(guān)1、七種常見的數(shù)組排序算法整理(...
    Theendisthebegi閱讀 10,350評(píng)論 0 16
  • 課堂安排也使得其更容易將學(xué)生分成小組討論問(wèn)題辕录,解決練習(xí)睦霎。小班還有可移動(dòng)的課桌和桌子,沒(méi)有問(wèn)題走诞。甚至在大講堂里副女,學(xué)生...
    201吳曉歡閱讀 104評(píng)論 0 0