【圖解動態規劃】打家劫舍 II(四種解法)

hixiaoxiaoniao發表於2020-11-06

目錄

題目描述

遞迴

遞迴+記憶化

動態規劃

動態規劃+空間壓縮


題目描述

你是一個專業的小偷,計劃偷竊沿街的房屋,每間房內都藏有一定的現金。這個地方所有的房屋都圍成一圈,這意味著第一個房屋和最後一個房屋是緊挨著的。同時,相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。
給定一個代表每個房屋存放金額的非負整數陣列,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。

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

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

遞迴

注意!!這道題的求解過程是完全基於【打家劫舍 I】來的,遞迴的解法、動態規劃解法、以及動態規劃+空間壓縮都是套用了【打家劫舍 I】。
如果沒有看過【打家劫舍 I】,請一定要先看一遍,點選【這裡】檢視。

【打家劫舍 I】本質是基於一維陣列的,而【打家劫舍 II】是一個環形陣列

直接用【打家劫舍 I】的程式碼會有什麼問題呢?

因為是環形陣列,所以第二排的陣列的求解方式就不對了,15因為是首尾相連只能二選一。
而第三排陣列的求解是滿足要求的,只選了1,沒有選5

所以我們可以改成這樣:

我們將原陣列拆成兩部分
1、nums[0],num[1]...num[end-1]
2、nums[1],nums[2]...nums[end]

然後我們分別對這兩個陣列求最大值,這個求解過程就完全套用了【打家劫舍 I】的邏輯。
我們只需要用同樣的邏輯,執行兩邊程式碼,最後求兩次的最大值即可。
遞迴的邏輯就是求兩遍陣列、動態規劃也是求兩遍、動態規劃+壓縮也一樣。
到這裡就很明確了,所謂的【打家劫舍 II】,其實就是執行了兩遍的【打家劫舍 I】而已。

java程式碼:

class Solution {
    public int rob(int[] nums) {
        if(nums==null || nums.length==0) {
            return 0;
        }
        if(nums.length<=2) {
            return (nums.length==2) ? Math.max(nums[0],nums[1]) : nums[0];
        }
        int n = nums.length;
        //將原陣列分拆分成兩個陣列,然後對兩個陣列分別求一次最大值
        int a = dfs(Arrays.copyOfRange(nums,0,n-1),0,0);
        int b = dfs(Arrays.copyOfRange(nums,1,n),0,0);
        return Math.max(a,b);
    }
 
    //直接套用了【打家劫舍 I】的程式碼
    private int dfs(int[] arr,int index,int status) {
        if(index>=arr.length) {
            return 0;
        }
        int a=0,b=0,c=0;
        a = dfs(arr,index+1,status);
        if(status==1) {
            b = dfs(arr,index+1,0);
        }
        else {
            c = dfs(arr,index+1,1)+arr[index];
        }
        return Math.max(a, Math.max(b,c) );
    }
}

python程式碼:

class Solution(object):
    def rob(self, nums): 
        if not nums:
            return 0
        n = len(nums)
        if n<=2:
            return max(nums)
        # 直接套用了【打家劫舍 I】的程式碼
        def dfs(arr,index,status):
            if index>=len(arr):
                return 0
            a,b,c = 0,0,0
            a = dfs(arr,index+1,status)
            if status:
                b = dfs(arr,index+1,0)
            else:
                c = dfs(arr,index+1,1)+arr[index]
            return max(a,b,c)
        # 將原陣列分拆分成兩個陣列,然後對兩個陣列分別求一次最大值
        a = dfs(nums[0:n-1],0,0)
        b = dfs(nums[1:],0,0)
        return max(a,b)

遞迴+記憶化

我們建立了兩套快取,然後分別呼叫了兩次遞迴函式,這兩個遞迴函式的邏輯跟【打家劫舍 I】幾乎是一樣的。
然後將兩次的返回結果求最大值即可。
java程式碼:

class Solution {
    public int rob(int[] nums) {
        if(nums==null || nums.length==0) {
            return 0;
        }
        if(nums.length<=2) {
            return (nums.length==2) ? Math.max(nums[0],nums[1]) : nums[0];
        }
        int n = nums.length;
        int[][] cache1 = new int[n][2];
        int[][] cache2 = new int[n][2];
        for(int i=0;i<n;++i) {
            Arrays.fill(cache1[i],-1);
            Arrays.fill(cache2[i],-1);
        }
        //將原陣列分拆分成兩個陣列,然後對兩個陣列分別求一次最大值
        int a = dfs(cache1,Arrays.copyOfRange(nums,0,n-1),0,0);
        int b = dfs(cache2,Arrays.copyOfRange(nums,1,n),0,0);
        return Math.max(a,b);
    }
 
    //套用了【打家劫舍 I】的程式碼 
    private int dfs(int[][] cache,int[] arr,int index,int status) {
        if(index>=arr.length) {
            return 0;
        }
        if(cache[index][status]>-1) {
            return cache[index][status];
        }
        int a=0,b=0,c=0;
        a = dfs(cache,arr,index+1,status);
        if(status==1) {
            b = dfs(cache,arr,index+1,0);
        }
        else {
            c = dfs(cache,arr,index+1,1)+arr[index];
        }
        cache[index][status] = Math.max(a, Math.max(b,c) );
        return cache[index][status];
    }
}

python程式碼:

class Solution(object):
    def rob(self, nums): 
        if not nums:
            return 0
        n = len(nums)
        if n<=2:
            return max(nums)
        # 套用了【打家劫舍 I】的程式碼 
        def dfs(cache,arr,index,status):
            if index>=len(arr):
                return 0
            if (index,status) in cache:
                return cache[index,status]
            a,b,c = 0,0,0
            a = dfs(cache,arr,index+1,status)
            if status:
                b = dfs(cache,arr,index+1,0)
            else:
                c = dfs(cache,arr,index+1,1)+arr[index]
            cache[index,status] = max(a,b,c)
            return cache[index,status]
        # 將原陣列分拆分成兩個陣列,然後對兩個陣列分別求一次最大值
        a = dfs(dict(),nums[0:n-1],0,0)
        b = dfs(dict(),nums[1:],0,0)
        return max(a,b)

動態規劃

動態規劃也是基於【打家劫舍 I】的。
你會發現這題會有點無聊,每種解法都是執行兩遍【打家劫舍 I】

這裡我們建立兩個dp陣列
dp1計算的是nums[1],nums[2]...nums[end]這個陣列

dp2計算的是nums[0],nums[1]...nums[end-1]這個陣列

dp1是從nums[1]開始的,所以初的前兩個值

  • dp1[0]就等於0

  • dp1[1]等於nums[1]

同理,dp2是從nums[0]開始的,所以初始化的前兩個值

  • dp2[0]就等於nums[0]

  • dp2[1]就等於max(nums[0],nums[1])

java程式碼:

class Solution {
    public int rob(int[] nums) {
        if(nums==null || nums.length==0) {
            return 0;
        }
        if(nums.length<=2) {
            return (nums.length==2) ? Math.max(nums[0],nums[1]) : nums[0];
        }
        int n = nums.length;
        int[] dp1 = new int[n];
        int[] dp2 = new int[n];
        //初始化兩個dp陣列,dp1是計算的是[1,end],dp2計算的是[0,end-1]
        dp1[0] = 0;
        dp1[1] = nums[1];
        dp2[0] = nums[0];
        dp2[1] = Math.max(nums[0],nums[1]);
        //按照【打家劫舍 I】的轉移方式執行兩遍
        for(int i=2;i<n;++i) {
            dp1[i] = Math.max(dp1[i-1],dp1[i-2]+nums[i]);
        }
        for(int i=2;i<n-1;++i) {
            dp2[i] = Math.max(dp2[i-1],dp2[i-2]+nums[i]);
        }
        return Math.max(dp1[n-1],dp2[n-2]);
    }
} 

python程式碼:

class Solution(object):
    def rob(self, nums):
        if not nums:
            return 0
        n = len(nums)
        if n<=2:
            return max(nums)
        dp1 = [0 for _ in xrange(n)]
        dp2 = [0 for _ in xrange(n)]
        # 初始化兩個dp陣列,dp1是計算的是[1,end],dp2計算的是[0,end-1]
        dp1[0] = 0
        dp1[1] = nums[1]
        dp2[0] = nums[0]
        dp2[1] = max(nums[0],nums[1])
        # 按照【打家劫舍 I】的轉移方式執行兩遍
        for i in xrange(2,n):
            dp1[i] = max(dp1[i-1],dp1[i-2]+nums[i])
        for i in xrange(2,n-1):
            dp2[i] = max(dp2[i-1],dp2[i-2]+nums[i])
        return max(dp1[-1],dp2[-2])

動態規劃+空間壓縮

【打家劫舍 I】中我們用兩個變數來優化空間,因為dp[i]的值只需要用到dp[i-2]dp[i-1]
所以我們用兩個變數來代替dp[i-2]dp[i-1]然後不斷滾動更新這兩個變數,這裡就是執行兩遍。

java程式碼:

class Solution {
    public int rob(int[] nums) {
        if(nums==null || nums.length==0) {
            return 0;
        }
        if(nums.length<=2) {
            return (nums.length==2) ? Math.max(nums[0],nums[1]) : nums[0];
        }
        int n = nums.length;
        //執行兩遍,第一次求nums[0,n-1],第二次求nums[1,n],再返回兩次計算的最大值
        int a = dp(nums,0,n-1);
        int b = dp(nums,1,n);
        return Math.max(a,b);
    }
   
    //用滾動陣列的方式優化空間
    private int dp(int[] nums,int begin,int end) {
        int pre = 0;
        int cur = 0;
        for(int i=begin;i<end;++i) {
            int tmp = cur;
            cur = Math.max(cur,pre+nums[i]);
            pre = tmp;
        }
        return cur;
    }
}

python程式碼:

class Solution(object):
    def rob(self, nums):
        if not nums:
            return 0
        n = len(nums)
        if n<=2:
            return max(nums)
        # 用滾動陣列的方式優化空間 
        def dp(begin,end):
            pre,cur = 0,0
            for i in xrange(begin,end):
                cur,pre = max(cur,pre+nums[i]),cur
            return cur
        # 執行兩遍,第一次求nums[0,n-1],第二次求nums[1,n],再返回兩次計算的最大值
        return max( dp(0,n-1),dp(1,n) )

 

推薦閱讀:

打家劫舍 I
打家劫舍 II

歡迎掃描關注公眾號 有更多圖解的演算法面試題等你哦~

 

相關文章