題目:
這裏我直接引用別人的題解,講動態規劃講的很好
動態規劃的一般思考方向
1、狀態定義;
2、狀態轉移方程;
3、初始化;
4、輸出;
5、思考狀態壓縮。
這 5 個部分是本題解的結構。其它類似的動態規劃問題也可以按照這樣的方向去思考、解釋和理解。
這是一個典型的“動態規劃”問題,並且它的“原形”是“0-1 揹包問題”。使用“動態規劃”解決問題的思路是“以空間換時間”,“規劃”這個詞在英文中就是“填表格”的意思,代碼執行的過程,也可以稱之爲“填表格”。
“動態規劃”的方法可以認爲是爲我們提供了一個思考問題的方向,我們不是直接面對問題求解,而是去找原始問題(或者說和原始問題相關的問題)的最開始的樣子,通過“狀態轉移方程”(這裏沒法再解釋了,可以結合下文理解)記錄下每一步求解的結果,直到最終問題解決。
而直接面對問題求解,就是我們熟悉的“遞歸”方法,由於有大量重複子問題,我們就需要加緩存,這叫“記憶化遞歸”,這裏就不給參考代碼了,感興趣的朋友可以自己寫一下,比較一下它們兩種思考方式的不同之處和優缺點。
做這道題需要做這樣一個等價轉換:是否可以從這個數組中挑選出一些正整數,使得這些數的和等於整個數組元素的和的一半。前提條件是:數組的和一定得是偶數,即數組的和一定得被 22 整除,這一點是特判。
本題與 0-1 揹包問題有一個很大的不同,即:
- 0-1 揹包問題選取的物品的容積總量不能超過規定的總量;
- 本題選取的數字之和需要恰恰好等於規定的和的一半。
這一點區別,決定了在初始化的時候,所有的值應該初始化爲 false。 (《揹包九講》的作者在介紹 0-1 揹包問題的時候,有強調過這點區別,我在這裏也只是再重複一下。)
作爲“0-1 揹包問題”,它的特點是:“每個數只能用一次”。思路是:物品一個一個選,容量也一點一點放大考慮(這一點是“動態規劃”的思想,特別重要)。
如果在實際生活中,其實我們也是這樣做的,一個一個嘗試把候選物品放入“揹包”,看什麼時候能容納的價值最大。
具體做法是:畫一個 len 行,target + 1 列的表格。這裏 len 是物品的個數,target 是揹包的容量。len 行表示一個一個物品考慮,target + 1多出來的那 1 列,表示揹包容量從 0 開始,很多時候,我們需要考慮這個容量爲 0 的數值。
狀態定義:dp[i][j]表示從數組的 [0, i] 這個子區間內挑選一些正整數,每個數只能用一次,使得這些數的和恰好等於 j。
狀態轉移方程:很多時候,狀態轉移方程思考的角度是“分類討論”,對於“0-1 揹包問題”而言就是“當前考慮到的數字選與不選”。
1、不選擇 nums[i],如果在 [0, i - 1] 這個子區間內已經有一部分元素,使得它們的和爲 j ,那麼 dp[i][j] = true;
2、選擇 nums[i],如果在 [0, i - 1] 這個子區間內就得找到一部分元素,使得它們的和爲 j - nums[i]。
狀態轉移方程是:
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]
一般寫出狀態轉移方程以後,就需要考慮邊界條件(一般而言也是初始化條件)。
1、j - nums[i] 作爲數組的下標,一定得保證大於等於 0 ,因此 nums[i] <= j;
2、注意到一種非常特殊的情況:j 恰好等於 nums[i],即單獨 nums[j] 這個數恰好等於此時“揹包的容積” j,這也是符合題意的。
因此完整的狀態轉移方程是:
初始化:dp[0][0] = false,因爲是正整數,當然湊不出和爲 0。
輸出:dp[len - 1][target],這裏 len 表示數組的長度,target 是數組的元素之和(必須是偶數)的一半。
代碼:
class Solution {
public boolean canPartition(int[] nums) {
if(nums == null || nums.length == 0){
return false;
}
int sum = 0;
for (int num : nums) {
sum += num;
}
//如果是奇數,直接返回false即可
if (sum%2 != 0) {
return false;
}
int target = sum / 2;
//行:nums範圍,列:求解目標,目標也包括0
boolean[][] dp = new boolean[nums.length][target + 1];
// 第 1 個數只能恰是目標值纔有解
if (nums[0] <= target) {
dp[0][nums[0]] = true;
}
// 再求解之後的情況
for (int i = 1; i < len; i++) {
for (int j = 0; j <= target; j++) {
if (nums[i] == j) {
dp[i][j] = true;
continue;
}
if (nums[i] < j) {
//如果nums[i] < j,那麼要麼前i-1範圍內能湊成target j,要麼前i-1範圍能湊成target j - nums[i]
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
return dp[len - 1][target];
}
}
本文分析部分轉自鏈接:https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/0-1-bei-bao-wen-ti-xiang-jie-zhen-dui-ben-ti-de-yo/