<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          動(dòng)態(tài)規(guī)劃算法幫我通關(guān)了魔塔!

          共 3585字,需瀏覽 8分鐘

           ·

          2021-01-14 18:37

          來(lái)自公眾號(hào):labuladong


          讀完本文,可以去力扣解決如下題目:

          174.地下城游戲(Hard

          「魔塔」是一款經(jīng)典的地牢類游戲,碰怪物要掉血,吃血瓶能加血,你要收集鑰匙,一層一層上樓,最后救出美麗的公主。

          現(xiàn)在手機(jī)上仍然可以玩這個(gè)游戲:

          嗯,相信這款游戲承包了不少人的童年回憶,記得小時(shí)候,一個(gè)人拿著游戲機(jī)玩,兩三個(gè)人圍在左右指手畫(huà)腳,這導(dǎo)致玩游戲的人體驗(yàn)極差,而左右的人異??鞓?lè) ??

          力扣第 174 題是一道類似的題目,我簡(jiǎn)單描述一下:

          輸入一個(gè)存儲(chǔ)著整數(shù)的二維數(shù)組grid,如果grid[i][j] > 0,說(shuō)明這個(gè)格子裝著血瓶,經(jīng)過(guò)它可以增加對(duì)應(yīng)的生命值;如果grid[i][j] == 0,則這是一個(gè)空格子,經(jīng)過(guò)它不會(huì)發(fā)生任何事情;如果grid[i][j] < 0,說(shuō)明這個(gè)格子有怪物,經(jīng)過(guò)它會(huì)損失對(duì)應(yīng)的生命值。

          現(xiàn)在你是一名騎士,將會(huì)出現(xiàn)在最上角,公主被困在最右下角,你只能向右和向下移動(dòng),請(qǐng)問(wèn)騎士的初始生命值至少為多少,才能成功救出公主?

          換句話說(shuō),就是問(wèn)你至少需要多少初始生命值,能夠讓騎士從最左上角移動(dòng)到最右下角,且任何時(shí)候生命值都要大于 0。

          函數(shù)簽名如下:

          int?calculateMinimumHP(int[][]?grid);

          比如題目給我們舉的例子,輸入如下一個(gè)二維數(shù)組grid,用K表示騎士,用P表示公主:

          算法應(yīng)該返回 7,也就是說(shuō)騎士的初始生命值至少為 7 時(shí)才能成功救出公主,行進(jìn)路線如圖中的箭頭所示。

          上篇文章 最小路徑和 寫(xiě)過(guò)類似的問(wèn)題,問(wèn)你從左上角到右下角的最小路徑和是多少。

          我們做算法題一定要嘗試舉一反三,感覺(jué)今天這道題和最小路徑和有點(diǎn)關(guān)系對(duì)吧?

          想要最小化騎士的初始生命值,是不是意味著要最大化騎士行進(jìn)路線上的血瓶?是不是相當(dāng)于求「最大路徑和」?是不是可以直接套用計(jì)算「最小路徑和」的思路?

          但是稍加思考,發(fā)現(xiàn)這個(gè)推論并不成立,吃到最多的血瓶,并不一定就能獲得最小的初始生命值。

          比如如下這種情況,如果想要吃到最多的血瓶獲得「最大路徑和」,應(yīng)該按照下圖箭頭所示的路徑,初始生命值需要 11:

          但也很容易看到,正確的答案應(yīng)該是下圖箭頭所示的路徑,初始生命值只需要 1:

          所以,關(guān)鍵不在于吃最多的血瓶,而是在于如何損失最少的生命值。

          這類求最值的問(wèn)題,肯定要借助動(dòng)態(tài)規(guī)劃技巧,要合理設(shè)計(jì)dp數(shù)組/函數(shù)的定義。類比前文 最小路徑和問(wèn)題,dp函數(shù)簽名肯定長(zhǎng)這樣:

          int?dp(int[][]?grid,?int?i,?int?j);

          但是這道題對(duì)dp函數(shù)的定義比較有意思,按照常理,這個(gè)dp函數(shù)的定義應(yīng)該是:

          從左上角(grid[0][0])走到grid[i][j]至少需要dp(grid, i, j)的生命值。

          這樣定義的話,base case 就是i, j都等于 0 的時(shí)候,我們可以這樣寫(xiě)代碼:

          int?calculateMinimumHP(int[][]?grid)?{
          ????int?m?=?grid.length;
          ????int?n?=?grid[0].length;
          ????//?我們想計(jì)算左上角到右下角所需的最小生命值
          ????return?dp(grid,?m?-?1,?n?-?1);
          }

          int?dp(int[][]?grid,?int?i,?int?j)?{
          ????//?base?case
          ????if?(i?==?0?&&?j?==?0)?{
          ????????//?保證騎士落地不死就行了
          ????????return?gird[i][j]?>?0???1?:?-grid[i][j]?+?1;
          ????}
          ????...
          }

          PS:為了簡(jiǎn)潔,之后dp(grid, i, j)就簡(jiǎn)寫(xiě)為dp(i, j),大家理解就好。

          接下來(lái)我們需要找狀態(tài)轉(zhuǎn)移了,還記得如何找狀態(tài)轉(zhuǎn)移方程嗎?我們這樣定義dp函數(shù)能否正確進(jìn)行狀態(tài)轉(zhuǎn)移呢?

          我們希望dp(i, j)能夠通過(guò)dp(i-1, j)dp(i, j-1)推導(dǎo)出來(lái),這樣就能不斷逼近 base case,也就能夠正確進(jìn)行狀態(tài)轉(zhuǎn)移。

          具體來(lái)說(shuō),「到達(dá)A的最小生命值」應(yīng)該能夠由「到達(dá)B的最小生命值」和「到達(dá)C的最小生命值」推導(dǎo)出來(lái):

          但問(wèn)題是,能推出來(lái)么?實(shí)際上是不能的。

          因?yàn)榘凑?code style="font-size: inherit;line-height: inherit;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">dp函數(shù)的定義,你只知道「能夠從左上角到達(dá)B的最小生命值」,但并不知道「到達(dá)B時(shí)的生命值」。

          「到達(dá)B時(shí)的生命值」是進(jìn)行狀態(tài)轉(zhuǎn)移的必要參考,我給你舉個(gè)例子你就明白了,假設(shè)下圖這種情況:

          你說(shuō)這種情況下,騎士救公主的最優(yōu)路線是什么?

          顯然是按照?qǐng)D中藍(lán)色的線走到B,最后走到A對(duì)吧,這樣初始血量只需要 1 就可以;如果走黃色箭頭這條路,先走到C然后走到A,初始血量至少需要 6。

          為什么會(huì)這樣呢?騎士走到BC的最少初始血量都是 1,為什么最后是從B走到A,而不是從C走到A呢?

          因?yàn)轵T士走到B的時(shí)候生命值為 11,而走到C的時(shí)候生命值依然是 1。

          如果騎士執(zhí)意要通過(guò)C走到A,那么初始血量必須加到 6 點(diǎn)才行;而如果通過(guò)B走到A,初始血量為 1 就夠了,因?yàn)槁飞铣缘窖苛?,生命值足夠?code style="font-size: inherit;line-height: inherit;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">A上面怪物的傷害。

          這下應(yīng)該說(shuō)的很清楚了,再回顧我們對(duì)dp函數(shù)的定義,上圖的情況,算法只知道dp(1, 2) = dp(2, 1) = 1,都是一樣的,怎么做出正確的決策,計(jì)算出dp(2, 2)呢?

          所以說(shuō),我們之前對(duì)dp數(shù)組的定義是錯(cuò)誤的,信息量不足,算法無(wú)法做出正確的狀態(tài)轉(zhuǎn)移。

          正確的做法需要反向思考,依然是如下的dp函數(shù):

          int?dp(int[][]?grid,?int?i,?int?j);

          但是我們要修改dp函數(shù)的定義:

          grid[i][j]到達(dá)終點(diǎn)(右下角)所需的最少生命值是dp(grid, i, j)。

          那么可以這樣寫(xiě)代碼:

          int?calculateMinimumHP(int[][]?grid)?{
          ????//?我們想計(jì)算左上角到右下角所需的最小生命值
          ????return?dp(grid,?0,?0);
          }

          int?dp(int[][]?grid,?int?i,?int?j)?{
          ????int?m?=?grid.length;
          ????int?n?=?grid[0].length;
          ????//?base?case
          ????if?(i?==?m?-?1?&&?j?==?n?-?1)?{
          ????????return?grid[i][j]?>=?0???1?:?-grid[i][j]?+?1;
          ????}
          ????...
          }

          根據(jù)新的dp函數(shù)定義和 base case,我們想求dp(0, 0),那就應(yīng)該試圖通過(guò)dp(i, j+1)dp(i+1, j)推導(dǎo)出dp(i, j),這樣才能不斷逼近 base case,正確進(jìn)行狀態(tài)轉(zhuǎn)移。

          具體來(lái)說(shuō),「從A到達(dá)右下角的最少生命值」應(yīng)該由「從B到達(dá)右下角的最少生命值」和「從C到達(dá)右下角的最少生命值」推導(dǎo)出來(lái):

          能不能推導(dǎo)出來(lái)呢?這次是可以的,假設(shè)dp(0, 1) = 5, dp(1, 0) = 4,那么可以肯定要從A走向C,因?yàn)?4 小于 5 嘛。

          那么怎么推出dp(0, 0)是多少呢?

          假設(shè)A的值為 1,既然知道下一步要往C走,且dp(1, 0) = 4意味著走到grid[1][0]的時(shí)候至少要有 4 點(diǎn)生命值,那么就可以確定騎士出現(xiàn)在A點(diǎn)時(shí)需要 4 - 1 = 3 點(diǎn)初始生命值,對(duì)吧。

          那如果A的值為 10,落地就能撿到一個(gè)大血瓶,超出了后續(xù)需求,4 - 10 = -6 意味著騎士的初始生命值為負(fù)數(shù),這顯然不可以,騎士的生命值小于 1 就掛了,所以這種情況下騎士的初始生命值應(yīng)該是 1。

          綜上,狀態(tài)轉(zhuǎn)移方程已經(jīng)推出來(lái)了:

          int?res?=?min(
          ????dp(i?+?1,?j),
          ????dp(i,?j?+?1)
          )?-?grid[i][j];

          dp(i,?j)?=?res?<=?0???1?:?res;

          根據(jù)這個(gè)核心邏輯,加一個(gè)備忘錄消除重疊子問(wèn)題,就可以直接寫(xiě)出最終的代碼了:

          /*?主函數(shù)?*/
          int?calculateMinimumHP(int[][]?grid)?{
          ????int?m?=?grid.length;
          ????int?n?=?grid[0].length;
          ????//?備忘錄中都初始化為?-1
          ????memo?=?new?int[m][n];
          ????for?(int[]?row?:?memo)?{
          ????????Arrays.fill(row,?-1);
          ????}

          ????return?dp(grid,?0,?0);
          }

          //?備忘錄,消除重疊子問(wèn)題
          int[][]?memo;

          /*?定義:從?(i, j)?到達(dá)右下角,需要的初始血量至少是多少?*/
          int?dp(int[][]?grid,?int?i,?int?j)?{
          ????int?m?=?grid.length;
          ????int?n?=?grid[0].length;
          ????//?base?case
          ????if?(i?==?m?-?1?&&?j?==?n?-?1)?{
          ????????return?grid[i][j]?>=?0???1?:?-grid[i][j]?+?1;
          ????}
          ????if?(i?==?m?||?j?==?n)?{
          ????????return?Integer.MAX_VALUE;
          ????}
          ????//?避免重復(fù)計(jì)算
          ????if?(memo[i][j]?!=?-1)?{
          ????????return?memo[i][j];
          ????}
          ????//?狀態(tài)轉(zhuǎn)移邏輯
          ????int?res?=?Math.min(
          ????????????dp(grid,?i,?j?+?1),
          ????????????dp(grid,?i?+?1,?j)
          ????????)?-?grid[i][j];
          ????//?騎士的生命值至少為?1
          ????memo[i][j]?=?res?<=?0???1?:?res;

          ????return?memo[i][j];
          }

          這就是自頂向下帶備忘錄的動(dòng)態(tài)規(guī)劃解法,參考前文 動(dòng)態(tài)規(guī)劃套路詳解 很容易就可以改寫(xiě)成dp數(shù)組的迭代解法,這里就不寫(xiě)了,讀者可以嘗試自己寫(xiě)一寫(xiě)。

          這道題的核心是定義dp函數(shù),找到正確的狀態(tài)轉(zhuǎn)移方程,從而計(jì)算出正確的答案。

          推薦閱讀:

          完全整理 | 365篇高質(zhì)技術(shù)文章目錄整理

          算法之美 : 棧和隊(duì)列

          主宰這個(gè)世界的10大算法

          徹底理解cookie、session、token

          淺談什么是遞歸算法

          專注服務(wù)器后臺(tái)技術(shù)棧知識(shí)總結(jié)分享

          歡迎關(guān)注交流共同進(jìn)步

          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  蜜臀久久99精品久久久久久宅男 | 国产精品成人熊猫视频成人在线播放 | 天天日,天天干,天天射 | 97在线视频免费 | 在线免费看黄视频 |