LeetCode 312 Burst Balloons 思路分析總結

LC 312. Burst Balloonsjava

題目描述

Given n balloons, indexed from 0 to n-1. Each balloon is painted with a number on it represented by array nums. You are asked to burst all the balloons. If the you burst balloon i you will get nums[left] * nums[i] * nums[right] coins. Here left and right are adjacent indices of i. After the burst, the left and right then becomes adjacent.算法

Find the maximum coins you can collect by bursting the balloons wisely.數組

Note:bash

You may imagine nums[-1] = nums[n] = 1. They are not real therefore you can not burst them. 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100優化


思路分析與代碼

拿到這道題的時候,能夠從題目描述看出最後要求解的問題是存在子問題的,並且最後的求解是 「最大值」,能夠想到用動態規劃來求解,可是難點在於狀態怎麼定義?遞推方程是什麼?數組裏面的數的數目是在動態變化的,若是直接從動態規劃的思路想,很難看出當前問題和前面的子問題的關係是什麼。所以,首先能夠考慮暴力的深度優先搜索,可是這裏有兩個思路:ui

  • 當前考慮的氣球是最扎爆的氣球
  • 當前考慮的氣球是最扎爆的氣球

其實第一種思路是最好理解的,可是寫代碼的時候你會發現不少問題,例如如何知道當前數的左右相鄰的數是什麼?你能夠經過移除數組中的數來得到,可是這會致使時間的增多,並非一個高效的作法。在看看第二種思路,若是最後打爆的氣球編號是 i,那麼說明 [0,i-1] 和 [i+1,n-1] 兩個區間上面的氣球已經被打爆,這裏的答案就是 1*i*1 + [0,i-1]的解 + [i+1,n-1]的解,這樣一個問題被拆分紅兩個子問題,子問題還能夠繼續往下拆分spa

i
        [0,i-1] [i+1,n-1]
          ...     ...

ans(0,n-1) = max(
    nums[-1]*nums[0]*nums[1]+ans(1,n-1),            // 最後打 0 號氣球
    ans(0,0)+nums[0]*nums[1]*nums[2]+ans(2,n-1),    // 最後打 1 號氣球
    ans(0,1)+nums[1]*nums[2]*nums[3]+ans(3,n-1),    // 最後打 2 號氣球
                      ...
    ans(0,n-3)+nums[n-3]*nums[n-2]*nums[n-1]+ans(n-1,n-1),  // 最後打 n-2 號氣球
    ans(0,n-2)+nums[n-2]*nums[n-1]*nums[n],         //最後打 n-1 號氣球
); where nums[-1] == nums[n] == 1
複製代碼

這裏能夠看出,這樣的思路和分治很像,的確如此,這道題目特殊的地方也在於此,它實際上是動態規劃和分治的結合,咱們稍後再詳細說明,根據上面的思路,咱們能夠轉化爲代碼:code

public int maxCoins(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    
    return helper(nums, 0, nums.length - 1);
}

private int helper(int[] nums, int l, int r) {
    if (l > r) {
        return 0;
    }
    
    int max = nums[l];
    for (int i = l; i <= r; ++i) {
        int cur = helper(nums, l, i - 1)
                    + get(nums, l - 1) * nums[i] * get(nums, r + 1)
                    + helper(nums, i + 1, r);
        
        max = Math.max(max, cur);
    }
    
    return max;
}

private int get(int[] nums, int i) {
    if (i < 0 || i >= nums.length) {
        return 1;
    }
    
    return nums[i];
}
複製代碼

上面的代碼很是的簡潔,核心代碼就是 for 循環裏面的遞歸,可是注意這只是暴力的解法,之因此是暴力是由於它作了不少以前作過、沒有必要的重複操做,你能夠從以前講過的遞推公式能夠看到,或者能夠畫一個遞歸樹狀圖來看。這裏只須要加上一個數組幫助記錄以前作過的事情,就能夠大大提升效率排序

public int maxCoins(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    
    int[][] dp = new int[nums.length][nums.length];
    
    return helper(nums, dp, 0, nums.length - 1);
}

private int helper(int[] nums, int[][] dp, int l, int r) {
    if (l > r) {
        return 0;
    }
    
    if (dp[l][r] != 0) {
        return dp[l][r];
    }
    
    int max = nums[l];
    for (int i = l; i <= r; ++i) {
        int cur = helper(nums, dp, l, i - 1)
                    + get(nums, l - 1) * nums[i] * get(nums, r + 1)
                    + helper(nums, dp, i + 1, r);
        
        max = Math.max(max, cur);
    }
    
    dp[l][r] = max;
    
    return max;
}

private int get(int[] nums, int i) {
    if (i < 0 || i >= nums.length) {
        return 1;
    }
    
    return nums[i];
}
複製代碼

其實上面的代碼實現已經用到了動態規劃了,可是是使用了遞歸的實現方式,這時候咱們再回過頭去看看動態規劃裏面的 「狀態」 和 「遞推公式」 就一目瞭然:遞歸

dp[i][j] -> 輸入數組 [i,j] 區間上的最大值

dp[0,n-1] = max(
    nums[-1]*nums[0]*nums[1]+dp[1,n-1],         
    dp[0,0]+nums[0]*nums[1]*nums[2]+dp[2,n-1],
    dp[0,1]+nums[1]*nums[2]*nums[3]+dp[3,n-1],
                      ...
    dp[0,n-3]+nums[n-3]*nums[n-2]*nums[n-1]+dp[n-1,n-1],
    dp[0,n-2]+nums[n-2]*nums[n-1]*nums[n],
);
複製代碼

有了狀態的定義和遞推公式,咱們就能夠直接上手動態規劃了,可是注意邊界條件:

public int maxCoins(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    
    int[] newNums = new int[nums.length + 2];
    
    newNums[0] = newNums[nums.length + 1] = 1;
    for (int i = 0; i < nums.length; ++i) {
        newNums[i + 1] = nums[i];
    }
    
    int n = newNums.length;
    
    int[][] dp = new int[n][n];

    for (int i = 2; i < n; ++i) {
        for (int l = 0; l < n - i; ++l) {
            int r = l + i;
            for (int j = l + 1; j < r; ++j) {
                dp[l][r] = Math.max(dp[l][r], 
                             newNums[l] * newNums[j] * newNums[r] 
                                    + dp[l][j] + dp[j][r]);
            }
        }
    }
    
    return dp[0][n - 1];
}
複製代碼

這是一個二維的動態規劃,若是在紙上畫表格來看 DP 數組的記錄軌跡的話,你會發現這裏記錄只用到二維矩陣的上三角,也就是以對角線爲軸的一半;記錄順序也並非一行一行下來的,而是以對角線進行的。


總結

這裏能夠看到咱們解這道題的一個過程是

  1. 思考並實現暴力求解
  2. 畫樹狀圖或者思考是否有重複的子問題
  3. 在暴力求解的基礎上,看能不能增長記憶化,記錄以前解過的子問題的解
  4. 經過狀態和遞推公式,試着用動態規劃求解

每每遇到不能一會兒獲得最優算法,或者沒有什麼思路的題,均可以按這個步驟試試。每每動態規劃可以解決的問題,暴力搜索均可以解,可是反過來就不必定了。只是說暴力搜索它並非咱們的終點,但它卻能夠爲咱們提供一個不錯的突破口,咱們在此基礎之上再來思考如何進一步地優化,獲得咱們最終想要看到的算法。不斷地去熟練這麼一個過程,相信思惟能力和直覺能力會不禁自主地提升。

另外就是這道題的一個很是巧妙的地方就是把分治的思想加了進來,分治算法與動態規劃算法不一樣的地方,或者說是截然相反的地方是,分治是不存在重複子問題的,不理解的話能夠想一想快速排序,一個區間被一分爲二,被分開的兩個區間不存在任何交集,它們各自在各自的空間內作事情;正是由於這一點,在思考暴力求解的時候,按照分治的思想,選擇的方向則是從結果導向,倒着去想,先分再合,分到不能分爲止,再去合併,對於這道題來講,合併是很是簡單的,就是相加;可是若是咱們按照通常動態規劃的思路順着去想,那麼打爆一個氣球后,這個氣球的左右區間將會合在一塊兒,這沒法將一個問題化成更小的問題去解決。雖然這樣的思路打破了咱們一般的思惟方式,可是仍是那句話,多積累,如今不會之後會,見得多了就不怕了。

相關文章
相關標籤/搜索