leetcode - 動態(tài)規(guī)劃 - Part2

198. 打家劫舍

題目描述

你是一個專業(yè)的小偷蔬蕊,計劃偷竊沿街的房屋岸夯。每間房內(nèi)都藏有一定的現(xiàn)金猜扮,
影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統(tǒng)监婶,
如果兩間相鄰的房屋在同一晚上被小偷闖入,系統(tǒng)會自動報警煮盼。 

給定一個代表每個房屋存放金額的非負整數(shù)數(shù)組僵控,計算你不觸動警報裝置的情況下 鱼冀,
一夜之內(nèi)能夠偷竊到的最高金額。 

示例 1: 
輸入:[1,2,3,1]
輸出:4
解釋:偷竊 1 號房屋 (金額 = 1) 泛烙,然后偷竊 3 號房屋 (金額 = 3)蔽氨。
偷竊到的最高金額 = 1 + 3 = 4 帆疟。 

示例 2: 
輸入:[2,7,9,3,1]
輸出:12
解釋:偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接著偷竊 5 號房屋 (金額 = 1)自赔。
偷竊到的最高金額 = 2 + 9 + 1 = 12 绍妨。

提示: 
0 <= nums.length <= 100 
0 <= nums[i] <= 400 

Related Topics 動態(tài)規(guī)劃

分析求解
因為不能同時偷竊相鄰的兩間房屋,當我們考察第 i 間房屋的時候毙驯,能偷到的總體金額和前面 i-2 間房屋能偷取的金額有關(guān)灾测,因其狀態(tài)轉(zhuǎn)移能從 0,1,\cdots,i-2 任何一個轉(zhuǎn)移而來媳搪。又我們需要求得最大金額,那么可以將狀態(tài)定義為 dp[i] 表示前 i 間房屋能偷到的最大金額序愚,則 0,1,\cdots,i-2 中的最大值為 i-2 的狀態(tài)展运。

對于房間 i精刷,有兩種選擇:偷或者不偷怒允。

  • 選擇偷,則 dp[i] = dp[i-2] + nums[i]
  • 選擇不偷勘畔,則 dp[i] = dp[i-1]
    所以我們的狀態(tài)轉(zhuǎn)移方程為
    dp[i] = max(dp[i-2] + nums[i], dp[i-1])
    相應(yīng)的實現(xiàn)代碼如下:
class Solution:
    def rob(self, nums: List[int]) -> int:
        # dp[i] 表示偷竊到 i 號房屋時的最高金額
        n = len(nums)
        if n == 0:
            return 0
        dp = [0] * n
        for i in range(n):
            if i == 0:
                dp[i] = nums[i]
            elif i == 1:
                dp[i] = max(nums[0], nums[1])
            else:
                dp[i] = max(dp[i-2] + nums[i], dp[i-1])
        return dp[n-1]

時間和空間復(fù)雜度均為 O(n)炫七。

繼續(xù)嘗試對空間復(fù)雜度進行優(yōu)化万哪。由于 dp[i] 只和 dp[i-1]dp[i-2] 有關(guān)抡秆,我們可以定義兩個變量來記錄不同狀態(tài)儒士,這樣能將空間復(fù)雜度降低至 O(1)

class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 0:
            return 0
        if n == 1:
            return nums[0]
        pre, cur = nums[0], max(nums[0], nums[1])
        for i in range(2, n):
            pre, cur = cur, max(pre + nums[i], cur)
        return cur

213. 打家劫舍 II

題目描述

你是一個專業(yè)的小偷诅福,計劃偷竊沿街的房屋,每間房內(nèi)都藏有一定的現(xiàn)金剩檀。
這個地方所有的房屋都圍成一圈旺芽,這意味著第一個房屋和最后一個房屋是緊挨著的采章。
同時壶辜,相鄰的房屋裝有相互連通的防盜系統(tǒng),如果兩間相鄰的房屋在同一晚上被小偷闖入抵怎,
系統(tǒng)會自動報警反惕。 

給定一個代表每個房屋存放金額的非負整數(shù)數(shù)組演侯,計算你在不觸動警報裝置的情況下,
能夠偷竊到的最高金額悬赏。 

示例 1: 
輸入: [2,3,2]
輸出: 3
解釋: 你不能先偷竊 1 號房屋(金額 = 2)娄徊,然后偷竊 3 號房屋(金額 = 2), 因為他們是相鄰的寄锐。

示例 2: 
輸入: [1,2,3,1]
輸出: 4
解釋: 你可以先偷竊 1 號房屋(金額 = 1),然后偷竊 3 號房屋(金額 = 3)中鼠。
偷竊到的最高金額 = 1 + 3 = 4 沿癞。 

Related Topics 動態(tài)規(guī)劃

分析求解
此題和上一題非常相似椎扬,不同點在于房屋是環(huán)狀連接的具温,這也意味著第 0 號房屋和第 n-1 號房屋不能同時被偷铣猩。所以我們可以針對第 0 號房屋是否選擇被偷分別討論求解达皿。因為其他和上一題一致贿肩,所以就不貼代碼了能曾。

337. 打家劫舍 III

題目描述

在上次打劫完一條街道之后和一圈房屋后告私,小偷又發(fā)現(xiàn)了一個新的可行竊的地區(qū)屿良。
這個地區(qū)只有一個入口,我們稱之為“根”餐茵。 除了“根”之外钟病,每棟房子有且只有一
個“父“房子與之相連刚梭。一番偵察之后,聰明的小偷意識到“這個地方的所有房屋的排列
類似于一棵二叉樹”屹徘。 如果兩個直接相連的房子在同一天晚上被打劫衅金,房屋將自動報警。 
計算在不觸動警報的情況下鉴吹,小偷一晚能夠盜取的最高金額惩琉。 

示例 1: 
輸入: [3,2,3,null,3,null,1]
     3
    / \
   2   3
    \   \ 
     3   1
輸出: 7 
解釋: 小偷一晚能夠盜取的最高金額 = 3 + 3 + 1 = 7. 

示例 2: 
輸入: [3,4,5,1,3,null,1]
     3
    / \
   4   5
  / \   \ 
 1   3   1
輸出: 9
解釋: 小偷一晚能夠盜取的最高金額 = 4 + 5 = 9.
 
Related Topics 樹 深度優(yōu)先搜索

分析求解
對于某個節(jié)點 root,我們有兩種選擇 偷 與 不偷技扼。假如我們選擇 偷,則其左右子節(jié)點就不能選擇 偷剿吻;如果我們選擇 不偷丽旅,則左右子節(jié)點我們可以選擇 偷 也可以選擇 不偷造垛。最后的結(jié)果就是取這兩種情況下的最大值。

我們要求能夠盜取的最大金額五辽。根據(jù)上面的思路杆逗,我們細化一下:

  • 假如我們選擇 偷 節(jié)點 root罪郊,此時能盜取的最大金額為 root 節(jié)點的金額 加上其 左右子樹上盜取的金額尚洽。由于我們選擇了 root,所以其左右子節(jié)點不能選擇癣疟,但是其孫子節(jié)點(左右子節(jié)點的子節(jié)點) 可以自由選擇潮酒,即遞歸其孫子節(jié)點(因為它們面臨的情況和節(jié)點 root 是一致的)急黎。
  • 假如我們選擇 不偷 節(jié)點 root,此時能盜取的最大金額為其左右子樹上盜取的金額勃教,即遞歸其左右子節(jié)點(此時它們面臨的情況和節(jié)點 root 是一致的)故源。
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def rob(self, root: TreeNode) -> int:
        if not root:
            return 0
        # 選擇偷節(jié)點root
        selected = root.val
        if root.left is not None:
            selected += self.rob(root.left.left) + self.rob(root.left.right)
        if root.right is not None:
            selected += self.rob(root.right.left) + self.rob(root.right.right)
        # 選擇不偷節(jié)點root
        not_selected = self.rob(root.left) + self.rob(root.right)
        return max(selected, not_selected)

這里的遞歸有很多重復(fù)計算心软,所以可以采用記憶化方法進行優(yōu)化著蛙。

class Solution:
    def __init__(self):
        self.memory = {}

    def rob(self, root: TreeNode) -> int:
        if root in self.memory:
            return self.memory[root]
        if not root:
            return 0
        # 選擇偷節(jié)點root
        selected = root.val
        if root.left is not None:
            selected += self.rob(root.left.left) + self.rob(root.left.right)
        if root.right is not None:
            selected += self.rob(root.right.left) + self.rob(root.right.right)
        # 選擇不偷節(jié)點root
        not_selected = self.rob(root.left) + self.rob(root.right)
        self.memory[root] = max(selected, not_selected)
        return self.memory[root]

既然有記憶化解法踏堡,那是否有動態(tài)規(guī)劃的解法呢顷蟆?
對于每個節(jié)點 root腐魂,它對應(yīng)著兩個狀態(tài):被選擇 或者 未被選擇。我們進行狀態(tài)轉(zhuǎn)移的時候削樊,需要同時考慮到這兩個狀態(tài)兔毒,因此都需要記錄。我們可以這樣設(shè)計狀態(tài) dp[root] = [not_selected, selected]迅脐。dp 是一個哈希表谴蔑,其鍵為節(jié)點,值為一個數(shù)組隐锭,表示該節(jié)點是否被選擇的兩種情況下盜取的最大金額成榜。

class Solution:
    def rob(self, root: TreeNode) -> int:

        def dfs(root):
            if not root:
                return [0, 0]
            left = dfs(root.left)
            right = dfs(root.right)
            selected = root.val + left[0] + right[0]
            not_selected = max(left[0], left[1]) + max(right[0], right[1])
            # 記錄狀態(tài)
            dp[root] = [not_selected, selected]
            # 返回狀態(tài)蹦玫,供父節(jié)點參考
            return [not_selected, selected]
        
        dp = {}
        res = dfs(root)
        return max(res)

仔細分析以上代碼,當前節(jié)點 root 的狀態(tài)受到其 左右子樹的狀態(tài)影響挣输,而與其他節(jié)點無關(guān)撩嚼,因此我們考慮對空間復(fù)雜度進行優(yōu)化。利用兩個變量記錄左右子樹的狀態(tài)完丽,返回供父節(jié)點調(diào)用即可。

class Solution:
    def rob(self, root: TreeNode) -> int:

        def dfs(root):
            if not root:
                return [0, 0]
            left = dfs(root.left)
            right = dfs(root.right)
            selected = root.val + left[0] + right[0]
            not_selected = max(left[0], left[1]) + max(right[0], right[1])
            # 返回狀態(tài)蜻底,供父節(jié)點參考
            return [not_selected, selected]
        
        res = dfs(root)
        return max(res)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末薄辅,一起剝皮案震驚了整個濱河市站楚,隨后出現(xiàn)的幾起案子搏嗡,更是在濱河造成了極大的恐慌,老刑警劉巖彻况,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異抽碌,居然都是意外死亡货徙,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門赏迟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锌杀,“玉大人泻仙,你說我怎么就攤上這事⊥幌耄” “怎么了猾担?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵绑嘹,是天一觀的道長。 經(jīng)常有香客問我蛤克,道長夷蚊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任筋现,我火速辦了婚禮矾飞,結(jié)果婚禮上呀邢,老公的妹妹穿的比我還像新娘洒沦。我一直安慰自己申眼,他們只是感情好括尸,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布病毡。 她就那樣靜靜地躺著,像睡著了一般有送。 火紅的嫁衣襯著肌膚如雪娶眷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天啸臀,我揣著相機與錄音届宠,去河邊找鬼烁落。 笑死,一個胖子當著我的面吹牛豌注,可吹牛的內(nèi)容都是我干的伤塌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼轧铁,長吁一口氣:“原來是場噩夢啊……” “哼每聪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起齿风,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤药薯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后救斑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體童本,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡穷娱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年携添,在試婚紗的時候發(fā)現(xiàn)自己被綠了亡资。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡奇唤,死狀恐怖咬扇,靈堂內(nèi)的尸體忽然破棺而出经窖,到底是詐尸還是另有隱情画侣,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站忿檩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏兽掰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坎拐,春花似錦、人聲如沸积担。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至杨帽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間老客,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工哲嘲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留眠副,地道東北人丧枪。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓钝计,卻偏偏與公主長得像债沮,于是被迫代替她去往敵國和親硅蹦。 傳聞我的和親對象是個殘疾皇子童芹,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354