有序號爲1~n這n項工做,每項工做在Si時間開始,在Ti時間結束。對於每項工做均可以選擇參加與否。若是選擇了參與,那麼自始至終都必須全程參與。此外,參與不一樣工做的時間段不能重疊。目標是參與儘量多的工做,問最多能參與多少項工做?數組
這個問題乍一看有點棘手,因爲每項工做間有時間段的重疊問題,而致使可能選了某個工做後接下去的幾個選不了了。因此並非簡單地從起始時間開始,每次在可選的工做中選最先趕上的會達到最優。code
事實上,不從遍歷時間,而從遍歷工做的角度上會更容易想到,每項工做其實都只有選和不選的區別。劃分子問題就是這類題目最主要的思想。對於選和不選兩種狀況,能夠分解爲僅由余下工做所構成的一樣的子問題。blog
假設用OPT(i)來表示在這1~i個工做中最多能參與多少項。那麼對於當前的第i個工做:遞歸
而後在選與不選的兩種狀況中取最優。ip
int OPT(int i) { if(i == 0) return 0; return max(OPT(i-1), 1+OPT(pre[i])); }
但這裏會有個問題,這個遞歸可能會反覆的計算某個值(好比下圖中的OPT(5)),浪費了不少時間。這也就是重疊子問題(overlap sub-problem),解決方案是用數組存下每次計算的答案,下次若是再要計算時直接用先前保留好的值就行(稱爲記憶化搜索)。element
int OPT(int i) { if(i == 0) return 0; if(dp[i] != 0) return dp[i]; return dp[i] = max(OPT(i-1), 1+OPT(pre[i])); }
因爲明顯能夠看出來這是一個從前日後更新的狀態,每一個OPT[i]都取決於它以前的值,因此也能夠直接用一個for遍歷更新,這樣作更簡潔。leetcode
dp[0] = 0; for(int i = 1; i <= n; i++) dp[i] = max(dp[i-1],1+dp[pre[i]);
這種一步步按順序求出問題的解的方法稱做動態規劃,不熟練DP的時候能夠先從記憶化搜索出發推導出遞推式。get
其實這道題還能夠用貪心的思路解決,正確的貪法是每次選取結束時間最先的工做。證實大概是,這個方案在選取了相同數量的更早開始的工做時,最終結束時間不會比其餘方案的更晚,因此不存在選取更多工做的方案。(嚴格意義的證實須要概括法和反證法)it
但我的感受貪心老是很玄學,能把動規想清楚就仍是穩妥的動規吧。io
給出一組正整數,問能不能取出任意個求和剛好爲S。若是能的話返回Ture,不能返回False。
一樣的從「取和不取」的角度來考慮。若是用OPT(i,S)來表示從1~i這前i個數中去湊S的話:
最終返回的是OPT(i-1,S-arr[i]) || OPT(i-1,S)
這裏出口的判斷值得注意:
若是S爲0,說明已經湊好了不須要再取了,直接返回true。
若是S不爲0且i爲0,這時已經沒有辦法湊了,直接返回false。
而且還有一個難想到的點,若是arr[i]>S,那麼只能走不選的分支。
bool OPT(int i, int S) { if(S == 0) return true; if(i == 0) return false; if(arr[i] > S) return OPT(i-1,S); return OPT(i-1,S-arr[i]) || OPT(i-1,S); }
一樣的能夠記憶化搜索開個dp數組存一下提升效率。
或者這裏用另外一種方法,直接非遞歸的遍歷更新。
for(int s = 0; s <= S; s++) dp[0][s] = false; for(int i = 0; i <= n; i++) dp[i][0] = true; for(int i = 1; i <= n; i++) for(int s = 1; s <= S; s++) { if(arr[i] > s) dp[i][s] = dp[i-1][s]; else dp[i][s] = dp[i-1][s-arr[i]] || dp[i-1][s]; }
有n個重量和價值分別爲w[i]、v[i]的物品,從這些物品中挑選出總重量不超過W的物品,問全部挑選方案中價值總和的最大值。
用dp[i][j]來表示對於1~i這前i個物品,裝入容量爲j的揹包中的最大價值。則有:
初始化時對於i=0及j=0時,dp值都爲0。
for(int i = 1; i <=n; i++) for(int j = 1; j <= W; j++) { if(j < w[i]) dp[i][j] = dp[i-1][j]; else dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]); } return dp[n][W];
實際上,dp[i][j] 的值只依賴於第 i−1 行的 dp[i−1][0...j] 這前 j+1 個元素,與 dp[i−1][j+1...W] 的值無關。因此其實只存1行就能完成整個dp過程。
用 dp[0...W] 存儲當前行,更新 dp[0...W] 的時候,按照 j=W...0 的遞減順序計算 dp[j],這樣能夠保證計算 dp[j] 時用到的 dp[j] 和 dp[j−w[i]] 的值和本來的二維數組中的第 i−1 行的值是相等的。更新完 dp[j] 的值後,對 dp[0...j−1] 的值不會產生影響。而且只須要更新到 j=w[i] 就能夠中止,由於再以前的與第 i−1 行沒有變化。
for(int i = 1; i <= n; i++) for(int j = W; j >= w[i]; j--) dp[j] = max(dp[j], dp[j-w[i]] + v[i]); return dp[W];
有n種重量和價值分別爲w[i]、v[i]的物品,從這些物品中挑選出總重量不超過W的物品,問全部挑選方案中價值總和的最大值。每種物品能夠挑選任意多件。
與01揹包惟一的不一樣就是從「選和不選」變成了「選幾件」。
用dp[i][j]來表示對於1~i這前i個物品,裝入容量爲j的揹包中的最大價值。對於第i件物品,至多能夠選k件,其中k知足k*w[i] <= j
。
因而把上面的代碼改一改能夠寫成三重循環:
for(int i = 1; i <=n; i++) for(int j = 1; j <= W; j++) for(int k = 0; k*w[i] <= j; k++) dp[i][j] = max(dp[i][j], dp[i-1][j-k*w[i]] + k*v[i]);
但事實上,對於dp[i][j]中選k個的狀況,和dp[i][j-w[i]]選k-1個是同樣的。也就是說,仍然能當作選與不選兩種狀況,只是若是選了,i的位置沒有改變(此位置的物品是無盡的)。
則有:
for(int i = 1; i <=n; i++) for(int j = 1; j <= W; j++) { if(j < w[i]) dp[i][j] = dp[i-1][j]; else dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i]); } return dp[n][W];
一樣的,能夠簡化爲只用一維數組,在這裏須要用以前第 i−1 行的值只有當前這一個dp[i-1][j],而須要用到更新後的第i行的值倒是dp[i][0...j],因此遍歷時得從前日後更新。一樣的,只需從 j=w[i] 開始,由於與以前沒有變化。
for(int i = 1; i <= n; i++) for(int j = w[i]; j <= W; j--) dp[j] = max(dp[j], dp[j-w[i]] + v[i]); return dp[W];
You are given a list of non-negative integers, a1, a2, ..., an, and a target, S. Now you have 2 symbols + and -. For each integer, you should choose one from + and - as its new symbol.
Find out how many ways to assign symbols to make sum of integers equal to target S.
Example 1:
Input: nums is [1, 1, 1, 1, 1], S is 3.
Output: 5
Explanation:-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3There are 5 ways to assign symbols to make the sum of nums be target 3.
Note:
The length of the given array is positive and will not exceed 20. The sum of elements in the given array will not exceed 1000. Your output answer is guaranteed to be fitted in a 32-bit integer.
若是暴力搜索的複雜度是O(2^n),然而巧妙的化簡一下就能變成DP。
關鍵就在於這三步推導:
sum(P) - sum(N) = target sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N) 2 * sum(P) = target + sum(nums)
從而題目就變爲了找一個子序列P使得 sum(P) = (target + sum(nums)) / 2
成立,也由此得知target + sum(nums)必須爲偶數。
想起有另外一道相似題Partition Equal Subset Sum,題意是找出兩個和相等的子序列。也即可否找到一個子序列,使得和爲整個數組和的一半,而後就是0-1揹包問題。
bool canPartition(vector<int>& nums) { int sum = 0; for(auto e : nums) sum += e; if(sum%2 != 0) return false; sum/=2; bool dp[20001] = {0}; dp[0] = true; for(int i = 0; i < nums.size(); i++) for(int j = sum; j >= nums[i]; j--) dp[j] = dp[j] || dp[j-nums[i]]; return dp[sum]; }
因而本題的代碼就很容易寫了,把上面代碼改一改,dp存的再也不是「是否能裝下」,而是「能裝下的方案數」,表達式改成 dp[j] += dp[j-nums[i]]
int subsetSum(vector<int>& nums, int sum) { int dp[20001] = {0}; dp[0] = 1; for(int i = 0; i < nums.size(); i++) for(int j = sum; j >= nums[i]; j--) dp[j] += dp[j-nums[i]]; return dp[sum]; } int findTargetSumWays(vector<int>& nums, int S) { int sum = 0; for(int i = 0; i < nums.size(); i++) sum += nums[i]; if((S+sum)%2 != 0 || sum < S) return 0; return subsetSum(nums,(S+sum)/2); }