動態規劃是一種非常重要的算法思想,畢竟是一種思想,所以在邏輯層面的體現並沒有一個固定的形式,但是處理問題的方式卻都是本着一個原則:將一個問題遞歸分解爲它的子問題進行求解,也許有人會問:這不是分治的思想嘛?因爲問題分解的過程中往往會產生很多重複的子問題,這個時候動態規劃和分支的區別就會體現出來,動態規劃會將需要重複解決的子問題的結果在第一次計算之後就保存起來,之後碰到重複的子問題都可以直接得到結果,這樣大大減少了遞歸過程中的算法時間複雜度。
動態規劃能夠解決問題的前提是該問題具有最優子結構的性質,即當問題的最優解包含了其子問題的最優解,那麼這種問題就可以通過動態規劃的思想求得最優解,這幾篇有關動態規劃的博客就拿leetcode上最簡單的幾道動態規劃的題作爲例子來講。
leetcode 53
最大子序和
給定一個整數數組 nums
,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大和。
示例:
輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,爲 6。
進階:
如果你已經實現複雜度爲 O(n) 的解法,嘗試使用更爲精妙的分治法求解。
方法一:通過遞歸遍歷所有的子序列和的情況,時間複雜度O(n^2)
一開始做這道題的時候,我想到的一個比較普通的方法,那就是通過遞歸求出所有子序列的和,通過遍歷nums,每次都算出以nums[i]結尾的子序列之和,遞歸的過程中也有用到動態規劃的思想,只是這種遍歷所有子序列的方式的時間複雜度爲O(n^2),給出代碼:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int last_sum[nums.size()];
memset(last_sum,0,sizeof(last_sum));
last_sum[0]=nums[0];
int max=last_sum[0];
for(int i=1;i<nums.size();i++){
for(int j=0;j<i;j++){
last_sum[j]=last_sum[j]+nums[i];
if(last_sum[j]>max)
max=last_sum[j];
}
last_sum[i]=nums[i];
if(last_sum[i]>max)
max=last_sum[i];
}
return max;
}
};
int數組last_sum是用來保存每次計算以nums[i]結尾的所有子序列的和,一開始我使用的是一個n*n的二維數組,但是提交叫代碼之後提示超出內存限制,後來考慮到每次迭代之後只需要保存最大的子序和就夠了,沒有必要把所有的子序和都保存下來,於是把n*n的二位數組改成一個n長度的一維數組。
但是既然題目都提示,用動態規劃的思想是可以達到O(n)的時間複雜度的,那麼肯定會有更加簡單的方法。
方法二:動態規劃遞歸求最大子序列和,時間複雜度O(n)
爲了更好地理解這種方法,我們可以先理清一下思路:一個序列[a0,a1,a2,...,an-1,an]可以看作是它的子序列[a0,a1,a2,...,an-1]的基礎上添加一個元素an得到的序列,如果每次我們都記錄下來以ai爲結尾的所有子序和中最大的那個,那麼我們添加的這一個元素an給序列[a0,a1,a2,...,an-1]以an-1結尾的所有子序列和中最大的那個帶來的影響就是:因爲子序列求和要求其中的各個元素是連續的,因此只有兩種可能:1.最大子序列和爲以an-1結尾的最大子序列和加上an;2.最大子序列和爲an本身。經過n次迭代,找出所有ai結尾的子序列的最大和,返回這個最大和即可。代碼如下:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int>::iterator it = nums.begin();
int maxSum = *it;
int theSum = *it;
for(it = it+1 ; it != nums.end(); it++){
theSum = max(theSum + *it, *it);
if(theSum > maxSum)
maxSum = theSum;
}
return maxSum;
}
};
第二種方法其實和第一種的意圖非常相似,都是找出以ai結尾的所有子序列中的最大子序列之和,但是方法二更爲簡潔巧妙一些,時間複雜度也更小。
歡迎關注接下來幾篇動態規劃相關博客~