[LeetCode] 1000. Minimum Cost to Merge Stones 混合石子的最小花費



There are `N` piles of stones arranged in a row.  The `i`-th pile has `stones[i]` stones.

move consists of merging exactly K consecutive piles into one pile, and the cost of this move is equal to the total number of stones in these K piles.

Find the minimum cost to merge all piles of stones into one pile.  If it is impossible, return -1.

Example 1:

Input: stones = [3,2,4,1], K = 2
Output: 20
Explanation:
We start with [3, 2, 4, 1].
We merge [3, 2] for a cost of 5, and we are left with [5, 4, 1].
We merge [4, 1] for a cost of 5, and we are left with [5, 5].
We merge [5, 5] for a cost of 10, and we are left with [10].
The total cost was 20, and this is the minimum possible.

Example 2:

Input: stones = [3,2,4,1], K = 3
Output: -1
Explanation: After any merge operation, there are 2 piles left, and we can't merge anymore.  So the task is impossible.

Example 3:

Input: stones = [3,5,1,2,6], K = 3
Output: 25
Explanation:
We start with [3, 5, 1, 2, 6].
We merge [5, 1, 2] for a cost of 8, and we are left with [3, 8, 6].
We merge [3, 8, 6] for a cost of 17, and we are left with [17].
The total cost was 25, and this is the minimum possible.

Note:

  • 1 <= stones.length <= 30
  • 2 <= K <= 30
  • 1 <= stones[i] <= 100

這道題給了我們N堆石頭,每堆石頭有不同的個數,說每次可以合並K堆石頭,合並堆的花費就是石頭的個數,然后問如何合並,才能使總花費最小。然后給了一些例子,通過觀察例子,可以發現,並不是所有的輸入都能成功合成一堆,比如例子2,無論先和並哪三堆,最終都會剩下兩堆,從而無法進一步合並,因為 K=3,每次至少需要合並三堆。我們當然希望能在開始合並之前就能知道最終是否能成功合並為一堆,而不是算到最后了才發現白忙了一場,所以要來分析一下,什么時候才能最終合並為一堆。再來看看例子2,每次要將三堆合並為一堆,那么就是減少了兩堆,而要使得最終能夠剩下一堆,其他的都要合並調,假設原來共有n堆,只能剩下一堆,就是說 n-1 堆都要減掉,而每次只能減少 k-1 堆,所以只要 n-1 能夠整除 k-1即可,即 (n-1)%(k-1) == 0 成立,這樣就可以提前判斷了。

好,接下來繼續,考慮如何來解題,首先要意識到這道題的情況可能非常多,用暴力搜索的話可能會非常的復雜,而且當前的合並方法完全會影響到之后的合並,所以基本是要放棄 Brute force 的想法的。同樣,這道題也不能用貪婪算法,每次都合並石子個數最少的三堆會收斂到局部峰值,不一定是全局的,所以只能另辟蹊徑。觀察到這題是玩數組的,又是求極值的題目,那么就要祭出神器動態規划 Dynamic Programming 了,先來考慮定義 dp 數組吧,最簡單直接的方法肯定直接用個二維的dp數組了,其中 dp[i][j] 表示合並范圍 [i, j] 內的石頭堆的最小花費,最終 dp[0][n-1] 就是所要求的值。看到了論壇上有人定義了三維的 dp 數組,把每次合並的堆數K也當作一維放入到 dp 數組中了,其實博主覺得不是很有必要,因為像這種必須要對 dp 數組進行升維操作的是當題目中有隱藏信息 Hidden Information,而當前定義的 dp 數組無法重現子問題,即無法找到狀態轉移方程的時候必須要做的,最典型的例子就是之前那道 Remove Boxes,那道題自區間的 dp 值非常依賴於區間左邊相同的數字的個數,而這道題每次合並的堆數K並不是很依賴其他小於K的合並的堆數,所以博主感覺沒有必要加。關於含有隱藏信息的 dp 題目,感覺巔峰就屬於揀櫻桃那題 Cherry Pickup 了吧,現在看還是有點暈,改天還得重新加工一下吧。其實跟這道題最像的當屬於打氣球那題 Burst Balloons,氣球爆了之后,兩邊的氣球就挨到一起了,這里也很類似,石子合並之后,再跟兩邊的石子堆繼續合並,這里的更新方式還是從小區間更新到大區間,跟打氣球那題的思路非常的相似,建議先去看看博主的之前那篇博客 Burst Balloons,會對理解這道題大有裨益。

根據之前打氣球的經驗,要從小區間開始更新,多小呢,從K開始,因為小於K的區間不用更新,其 dp 值一定為0,因為每次必須合並K堆石子,所以區間的長度 len 從K遍歷到 n。好,區間長度確定了,現在要確定起點了,i從0遍歷到 n-len 即可,有了區間的起點和長度,可以確定區間的終點 j = i+len-1。目標就是要更新區間 [i, j] 的dp值,先初始化為整型最大值。接下來的更新方法,即狀態轉移方程,就是本題最大的難點了,要求區間 [i, j] 的 dp 值,沒法直接得到,但是由於是從小區間開始更新的,所以 suppose 其中的小區間的 dp 值都已經更新好了,就可以將大區間拆成兩個小區間來更新了。一般來講,將一個數組拆成兩個非空子數組的時候,會遍歷其所有情況,比如 [1, 2, 3, 4],會拆成 [1] 和 [2,3,4],[1,2] 和 [3,4], [1,2,3] 和 [4]。但是這道題由於其特殊性,並不需要遍歷所有的拆分情況,因為某些區間是無法通過合並石子堆得到的,就拿上面的例子來說,若 K=3,那么就不需要用 [1,2] 和 [3,4] 來更新整個區間,它們都不到3個,無法合並,所以遍歷的時候每次跳過 K-1 個位置即可,用 t 來分別區間 [i, j],然后每次 t += K-1 即可,用兩個小區間的 dp 值來更新整個區間。這還沒有完,當某個子區間正好可以合並為一堆石子的時候,其 dp 值要加上該區間所有的石子數。舉個最簡單的例子,比如 [1, 2, 3],K=3,那么我們分割的話,只能用 dp[0][0] + dp[1][2] 來更新 dp[0][2],但是 dp[0][0] 和 dp[1][2] 均為0,因為區間長度均小於3,那么我們的 dp[0][2] 值就無法更新成正確的值了,這三個數字是可以合並的,所以要加上區間內所有的數字之和,而為了快速的求得任意區間和,采用提前建立累加和數組 sums 的方式,來提高計算效率,所以整個狀態轉移方程為:

dp[i][j] = min(dp[i][j], dp[i][t] + dp[t + 1][j]); -> (i <= t < j)

dp[i][j] += sums[j + 1] - sums[i]; -> if ((j - i) % (K - 1) == 0)

有了狀態轉移方程,我們就可以寫出代碼如下:


class Solution {
public:
    int mergeStones(vector<int>& stones, int K) {
    	int n = stones.size();
    	if ((n - 1) % (K - 1) != 0) return -1;
        vector<int> sums(n + 1);
        vector<vector<int>> dp(n, vector<int>(n));
        for (int i = 1; i < n + 1; ++i) {
            sums[i] = sums[i - 1] + stones[i - 1];
        }
        for (int len = K; len <= n; ++len) {
            for (int i = 0; i + len <= n; ++i) {
            	int j = i + len - 1;
            	dp[i][j] = INT_MAX;
                for (int t = i; t < j; t += K - 1) {
                    dp[i][j] = min(dp[i][j], dp[i][t] + dp[t + 1][j]);
                }
                if ((j - i) % (K - 1) == 0) {
                	dp[i][j] += sums[j + 1] - sums[i];
                }
            }
        }
        return dp[0][n - 1];
    }
};

Github 同步地址:

https://github.com/grandyang/leetcode/issues/1000


類似題目:

Burst Balloons

Remove Boxes

Cherry Pickup


參考資料:

https://leetcode.com/problems/minimum-cost-to-merge-stones/

https://leetcode.com/problems/minimum-cost-to-merge-stones/discuss/247567/JavaC%2B%2BPython-DP


LeetCode All in One 題目講解匯總(持續更新中...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM