Dynamic Programming -- 動態規劃


定義

動態規劃簡稱DP問題,是分治思想的延伸,通俗一點來說就是大事化小,小事化無的藝術。

在將大問題化解爲小問題的分治過程中,保存對這些小問題已經處理好的結果,並供後面處理更大規模的問題時直接使用這些結果。

特點

動態規劃具備了以下三個特點:

  1. 把原來的問題分解成了幾個相似的子問題
  2. 所有的子問題都只需要解決一次
  3. 儲存子問題的解。

本質

動態規劃的本質:是對問題狀態的定義和狀態轉移方程的定義。
(狀態轉移方程:狀態之間的遞推關係)

動態規劃問題一般從以下四個角度考慮:

  1. 狀態定義
  2. 狀態間的轉移方程定義
  3. 狀態的初始化
  4. 返回結果
  • 狀態定義的要求:定義的狀態一定要形成遞推關係
  • 【概括】:三特點四要素兩本質
  • 適用場景:最大值/最小值, 是否可行, 是不是,方案個數等

樣例

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)=1F(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)
狀態:
  子狀態:長度爲123...,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".

狀態:
  子狀態:前123...,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()];
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章