定義
動態規劃簡稱DP
問題,是分治思想的延伸,通俗一點來說就是大事化小,小事化無的藝術。
在將大問題化解爲小問題的分治過程中,保存對這些小問題已經處理好的結果,並供後面處理更大規模的問題時直接使用這些結果。
特點
動態規劃具備了以下三個特點:
- 把原來的問題分解成了幾個相似的子問題。
- 所有的子問題都只需要解決一次。
- 儲存子問題的解。
本質
動態規劃的本質:是對問題狀態的定義和狀態轉移方程的定義。
(狀態轉移方程:狀態之間的遞推關係)
動態規劃問題一般從以下四個角度考慮:
- 狀態定義
- 狀態間的轉移方程定義
- 狀態的初始化
- 返回結果
- 狀態定義的要求:定義的狀態一定要形成遞推關係。
- 【概括】:三特點四要素兩本質
- 適用場景:最大值/最小值, 是否可行, 是不是,方案個數等。
樣例
Fibonacci
斐波那契數列(Fibonacci sequence
),又稱黃金分割數列、因數學家列昂納多·斐波那契以兔子繁殖爲例子而引入,故又稱爲“兔子數列
”,指的是這樣一個數列:1、1、2、3、5、8、13、21、34、……
我們知道他的遞推公式是:F(n)=F(n-1)+F(n-2)
(n>=2,n∈N*),其中F(1)=1
,F(2)=1
。所以很容易用遞歸實現:
int Fibonacci(int n){
// 初始值
if (n <= 0){
return 0;
}
if (n == 1 || n == 2) {
return 1;
}
//遞推公式:F(n)=F(n-1)+F(n-2)
return Fibonacci(n - 2) + Fibonacci(n - 1);
}
可以想到斐波那契數列遞歸求解最大的缺點就是:大量的重複計算。
它的方法時間複雜度爲O(2^n)
,隨着n
的增大呈現指數增長,效率不盡人意。而當輸入比較大時,還可能導致棧溢出。
如何優化?使用動態規劃思想!
【動態規劃】解法:
狀態:F(n)
狀態遞推:F(n) = F(n-1) + F(n-2)
初始值:F(1) = F(2) = 1
返回結果:F(N)
代碼實現:
int Fibonacci(int n){
// 初始值
if (n <= 0){
return 0;
}
if (n == 1 || n == 2) {
return 1;
}
// 申請一個數組,保存子問題的解,題目要求從第0項開始
int* record = new int[n + 1];
record[0] = 0;
record[1] = 1;
for (int i = 2; i <= n; i++){
// F(n)=F(n-1)+F(n-2)
record[i] = record[i - 1] + record[i - 2];
}
//返回結果
return record[n];
delete[] record;
}
上述解法的時間、空間複雜度均爲O(n)
,對於遞歸的時間複雜度O(2^n)
有了明顯優化。
但其實F(n)
只與它相鄰的前兩項有關,所以沒有必要保存所有子問題的解,只需要保存兩個子問題的解就可以。
以下方法將空間複雜度降爲O(1)
int Fibonacci(int n){
// 初始值
if (n <= 0){
return 0;
}
if (n == 1 || n == 2) {
return 1;
}
int fn1 = 1;
int fn2 = 1;
int result = 0;
for (int i = 3; i <= n; i++){
// F(n)=F(n-1)+F(n-2)
result = fn2 + fn1;
// 更新值
fn1 = fn2;
fn2 = result;
}
return result;
}
變態青蛙跳臺階(Climbing Stairs)
- 例題描述:
一隻青蛙一次可以跳上1
級臺階,也可以跳上2
級……它也可以跳上n
級。求該青蛙跳上一個n
級的臺階總共有多少種跳法。
狀態:
子狀態:跳上1級,2級,3級,...,n級臺階的跳法數
f(n):還剩n個臺階的跳法數
狀態遞推:
n級臺階,第一步有n種跳法:跳1級、跳2級、到跳n級
跳1級,剩下n-1級,則剩下跳法是f(n-1)
跳2級,剩下n-2級,則剩下跳法是f(n-2)
f(n) = f(n-1)+f(n-2)+...+f(n-n)
f(n) = f(n-1)+f(n-2)+...+f(0)
f(n-1) = f(n-2)+...+f(0)
錯位相減得:
f(n) = 2*f(n-1)
初始值:
f(1) = 1
f(2) = 2*f(1) = 2
f(3) = 2*f(2) = 4
f(4) = 2*f(3) = 8
所以它是一個等比數列
f(n) = 2^(n-1)
返回結果:
f(n)
代碼實現:
int jumpFloorII(int number) {
if(number <= 0)
return 0;
int total = 1;
for(int i = 1;i < number;i++)
total *= 2;
return total;
}
優化:降低時間複雜度。
上述實現的時間複雜度:O(N)
,優化爲O(1)
的實現:使用移位操作
int jumpFloorII(int number) {
if(number <= 0)
return 0;
return 1 << (number-1);
}
連續子數組最大和
- 例題描述:
HZ偶爾會拿些專業問題來忽悠那些非計算機專業的同學。今天測試組開完會後,他又發話了:在古老的一維模式識別中,常常需要計算連續子向量的最大和,當向量全爲正數的時候,問題很好解決。但是,如果向量中包含負數,是否應該包含某個負數,並期望旁邊的正數會彌補它呢?例如{6,-3,-2,7,-15,1,2,2}
,連續子向量的最大和爲8
(從第0
個開始,到第3
個爲止)。給一個數組,返回它的最大連續子序列的和,你會不會被他忽悠住?(子向量的長度至少是1
)
狀態:
子狀態:長度爲1,2,3,...,n的子數組和的最大值
F(i):長度爲i的子數組和的最大值,這種定義不能形成遞推關係,捨棄
F(i):以array[i]爲末尾元素的子數組和的最大值
狀態遞推:
F(i) = max(F(i-1) + array[i],array[i])
F(i) = (F(i-1) > 0)? F(i-1) + array[i] : array[i]
初始值:F(0) = array[0]
返回結果:
maxsum:所有F(i)中的最大值
maxsum = max(maxsum,F(i))
代碼實現:
int FindGreatestSumOfSubArray(vector<int> array){
if (array.empty()){
return -1;
}
// F(i)初始化
int sum = array[0];
// maxsum初始化
int maxsum = array[0];
for (int i = 1; i < array.size(); i++){
// F(i) = max(F(i-1) + array[i],array[i])
sum = (sum > 0) ? sum + array[i] : array[i];
// maxsum = max( maxsum,F(i))
maxsum = (sum < maxsum) ? maxsum : sum;
}
return maxsum;
}
字符串分割(Word Break)
- 例題描述:
給定一個字符串s
和一組單詞dict
,判斷s
是否可以用空格分割成一個單詞序列,使得單詞序列中所有的單詞都是dict
中的單詞(序列可以包含一個或多個單詞)。
例如:
給定s
=“leetcode”
; dict=["leet", "code"].
返回true
,因爲"leetcode"
可以被分割成"leet code"
.
狀態:
子狀態:前1,2,3,...,n個字符能否根據詞典中的詞被成功分詞
F(i): 前i個字符能否根據詞典中的詞被成功分詞
狀態遞推:
F(i): true{j < i && F(j) && substr[j+1,i]能在詞典中找到} OR false
在j小於i中,只要能找到一個F(j)爲true,並且從j+1到i之間的字符能在詞典中找到,則F(i)爲true
初始值:
對於初始值無法確定的,可以引入一個不代表實際意義的空狀態,作爲狀態的起始
空狀態的值需要保證狀態遞推可以正確且順利的進行
F(0) = true
返回結果:F(n)
代碼實現:
bool wordBreak(string s, unordered_set<string> &dict){
if (s.empty()){
return false;
}
if (dict.empty()){
return false;
}
// 獲取詞典中的單詞的最大長度
int max_length = 0;
unordered_set<string>::iterator dict_iter= dict.begin();
for (; dict_iter != dict.end(); dict_iter++){
if ((*dict_iter).size() > max_length){
max_length = (*dict_iter).size();
}
}
vector<bool> can_break(s.size() + 1, false);
// 初始化 F(0) = true
can_break[0] = true;
for (int i = 1; i <= s.size(); i++){
for (int j = i - 1; j >= 0; j--){
// 如果最小子串長度大於max_length,跳過
if ((i - j) > max_length) break;
// F(i): true{j <i && F(j) && substr[j+1,i]能在詞典中找到} OR false
// 第j+1個字符的索引爲j
if (can_break[j] && dict.find(s.substr(j, i - j)) != dict.end()){
can_break[i] = true;
break;
}
}
}
return can_break[s.size()];
}