動態規划與貪心、分治的區別
- 貪心算法(Greed alalgorithm) 是一種在每一步選擇中都采取在當前狀態下最好或最優(即最有利)的選擇,從而希望導致全局結果是最好或最優的算法。
- 分治算法(Divide and conquer alalgorithm) 字面上的解釋是“分而治之”,就是把一個復雜的問題分成兩個或更多的相同或相似的子問題,直到最后子問題可以簡單的直接求解,原問題的解即子問題的解的合並。
- 動態規划算法(Dynamic programming,DP) 通過將原問題分解為相對簡單的子問題的方式來求解復雜問題。通常許多子問題非常相似,為此動態規划法試圖僅僅解決每個子問題一次,從而減少計算量:一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次需要同一個子問題解之時直接查表。
貪心法在處理每個子問題時,不能回退,而動態規划可以保存之前的結果,擇優選擇。下面針對Interval Scheduling 問題,分析動態規划在實際問題中的應用。
Interval Scheduling 問題
-
如下圖所示,每個長條方塊代表一個工作,總有若干個工作a、b... h,橫坐標是時間,方塊的起點和終點分別代表這個工作的起始時間和結束時間。
-
當兩個工作的工作時間沒有交叉,即兩個方塊不重疊時,表示這兩個工作是兼容的(compatible)。
-
當給每個工作賦權值都為1時,則稱為 Unweighted Interval Scheduling 問題;當給每個工作賦不同的正權值時,則稱為 Weighted Interval Scheduling 問題。
-
問題最終是要找到一個工作子集,集合內所有工作權值之和最大且集合內每個工作都兼容。
對於 Unweighted Interval Scheduling 問題,使用貪心算法即可求解,具體做法是按照結束時間對所有工作進行排序,然后從結束最晚的工作開始,依次排除掉與前一個不兼容的工作,剩下的工作所組成的集合即為所求。
然而,對於 Weighted Interval Scheduling 問題,貪心法找到的解可能不是最優的了。此時考慮使用動態規划算法解決問題,兼顧權值選擇和兼容關系。
定義P(j)
1、首先依然按照結束時間對所有的工作進行排序;
2、定義p(j)為在工作j之前,且與j兼容的工作的最大標號,通過分析每個工作的起始時間和結束時間,可以很容易計算出p(j);
3、例如下圖所示,p(8)=5,因為工作7和6都與8不兼容,工作1到5都與8兼容,而5是其中索引最大的一個,所以p(8)=5。同理,p(7)=3,p(2)=0。
分析遞歸關系
1、定義opt(j)是j個工作中,所能選擇到的最佳方案,即opt(j)是最大的權值和;
2、對於第j個工作,有兩種情況:
- case 1: 工作j包含在最優解當中,那么往前遞推一步,j之前能選擇到的最優解是opt(p(j)),即
- case 2: 工作j不在最優解中,那么從j個工作中選取解集和從j-1個工作中選取解集是一樣的,即
3、當j=0時,顯示結果為0,這是邊界條件。
后一步的結果取前一步所有可能情況的最大值,因此綜上所述,能得到動態規划的遞歸關系為:
代碼實現
1、遞歸法
遞歸會使得空間復雜度變高,一般不建議使用。
2、自底向上法
從小到大進行計算,這樣每次都可以利用前一步計算好的值來計算后一步的值,算法時間復雜度為O(nlogn),其中排序花費O(nlogn),后面的循環花費O(n)。
Knapsack Problem 問題
背包問題的定義
- 如下圖所示,給定一個背包Knapsack,有若干物品Item
- 每個item有自己的重量weight,對應一個價值value
- 背包的總重量限定為W
- 目標是填充背包,在不超重的情況下,使背包內物品總重量最大。
對於下圖的例子,一種常見的貪心思想是:在背包可以裝得下的情況下,盡可能選擇價值更高的物品。那么當背包容量是W=11時,先選擇item5,再選擇item2,最后只能放下item1,總價值為28+6+1=35。實際上最優解是選擇item3和item4,價值18+22=40。這說明了貪心算法對於背包問題的求解可能不是zuiyou的。下面考慮使用動態規划算法求解,首先要推導遞歸關系式。
推導遞歸關系式
類似於Weighted Interval Scheduling問題,定義opt(i, w)表示在有i個item,且背包剩余容量為w時所能得到的最大價值和。
考慮第i個item,有選和不選兩種情況:
- case 1: 如果選擇第i個item,則
- case 2: 如果不選擇第i個item,則
邊界條件: 當i=0時,顯然opt(i,w)=0。
后一步的結果取前一步所有可能情況的最大值,因此綜上所述,能得到動態規划的遞歸關系為:
自底向上求解
算法迭代過程如下表:
算法運行時間分析
值得注意的是,該算法相對於輸入尺寸來說,不是一個多項式算法,雖然O(nW)看起來很像一個多項式解,背包問題實際上是一個NP完全問題。
為了便於理解,可以寫成這種形式:
W在計算機中只是一個數字,以長度logW的空間存儲,非常小。但是在實際運算中,隨着W的改變,需要計算nW次,這是非常大的(相對於logW來說)。例如,當W為5kg的時候,以kg為基准單位,需要計算O(5n)次,當W為5t時,仍然以kg為單位,需要計算O(5000n)次,而在計算機中W的變化量相對很小。
Sequence Alignment
Define edit distance
給定兩個序列x1,x2...xi和y1,y2,...,yj。要匹配這兩個序列,使相似度足夠大。首先需要定義一個表示代價的量-Edit distance,只有優化使這個量最小,就相當於最大化匹配了這兩個序列。
Edit distance的定義如下所示。
其中,匹配到空,設距離為delta,否則字母p和q匹配的距離記為alpha(p,q),如果p=q,則alpha=0;
那么兩個序列匹配的總代價為:
建立遞推關系
設opt(i,j)是序列x1,x2...xi和y1,y2,...,yj之間匹配所花費的最小代價。當i,j不全為0時,則分別有三種情況,分別是xi-gap,yj-gap,xi-yj,分別計算不同匹配情況所花費的代價,再加上前一步的結果,就可以建立遞推關系式,如下所示。
算法實現
算法復雜度
時間和空間復雜度皆為O(mn)。
下面再分析一個具體的編程問題,使用動態規划算法,但是和上面的DP又有一些區別。
合唱團問題
問題定義
有 n 個學生站成一排,每個學生有一個能力值,牛牛想從這 n 個學生中按照順序選取 k 名學生,要求相鄰兩個學生的位置編號的差不超過 d,使得這 k 個學生的能力值的乘積最大,你能返回最大的乘積嗎?
輸入描述
每個輸入包含 1 個測試用例。每個測試數據的第一行包含一個整數 n (1 <= n <= 50),表示學生的個數,接下來的一行,包含 n 個整數,按順序表示每個學生的能力值 ai(-50 <= ai <= 50)。接下來的一行包含兩個整數,k 和 d (1 <= k <= 10, 1 <= d <= 50)。
輸出描述
輸出一行表示最大的乘積。
問題分析
- 此題的第一個關鍵點是“要求相鄰兩個學生的位置編號的差不超過 d”,如果按照傳統的DP思路,定義opt(i,k)表示在前i個學生中選取k個學生的最大乘積,建立遞推關系:
則無法實現“相鄰兩個學生的位置編號的差不超過 d”的要求。因此,需要定義一個輔助量,來包含對當前學生的定位信息。
- 定義f(i,k)表示在前i個學生中選取k個學生,且第i個學生必選時,所選學生的能力值乘積,這樣就包含對當前學生的定位信息,f的遞推關系可以表示為
其中,j是一個比i小的值,最大為i-1,i、j之差不超過D,f(j,k-1)表示在前j個學生中,選擇k-1個學生,且第j個學生必選。f(i,k)選擇了第i個學生,f(j,k-1)選擇了第j個學生,i、j之差不超過D,這樣就可以滿足題目要求了。
- 輔助量f(i,k)並不是我們最終要得到的結果,最終結果opt(i,k)表示在前i個學生中選取k個學生的最大乘積,因此,可以得到opt(i,k)和f(i,k)的關系為:
- 該問題的第二個關鍵點是學生的能力值在-50到+50之間,每次選擇的學生的能力值有正有負,所以需要兩個f記錄最大和最小值,定義fmax和fmin,在每次迭代f的過程中:
- 當k=K,i=N時,最終所求的:
- 邊界條件k=1時,f(i,k=1)=v(i)
代碼實現
/*********************************************************************
*
* Ran Chen <wychencr@163.com>
*
* Dynamic programming algorithm
*
*********************************************************************/
#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
int main()
{
int N, D, K; // 總共N個學生
vector <int> value;
while (cin >> N)
{
for (int i = 0; i < N; ++i)
{
int v;
cin >> v;
value.push_back(v);
}
break;
}
cin >> K; // 選擇K個學生
cin >> D; // 相鄰被選擇學生的序號差值
// fmax/fmin[i, k]表示在選擇第i個數的情況下的最大/小乘積
vector <vector <long long>> fmax(N+1, vector <long long> (K+1));
vector <vector <long long>> fmin(N+1, vector <long long> (K+1));
// 邊界條件k=1
for (int i = 1; i <= N; ++i)
{
fmax[i][1] = value[i - 1];
fmin[i][1] = value[i - 1];
}
// 自底向上dp, k>=1
for (int k = 2; k <= K; ++k)
{
// i >= k
for (int i = k; i <= N; ++i)
{
// 0 <= j <= i-1 && i - j <= D && j >= k-1
long long *max_j = new long long; *max_j = LLONG_MIN;
long long *min_j = new long long; *min_j = LLONG_MAX;
// f(i, k) = max_j {f(j, k-1) * value(i)}
int j = max(i - D, max(k - 1, 1));
for ( ; j <= i - 1; ++j)
{
*max_j = max(*max_j, max(fmax[j][k - 1] * value[i - 1], fmin[j][k - 1] * value[i - 1]));
*min_j = min(*min_j, min(fmax[j][k - 1] * value[i - 1], fmin[j][k - 1] * value[i - 1]));
}
fmax[i][k] = *max_j;
fmin[i][k] = *min_j;
delete max_j;
delete min_j;
}
}
// opt(N, K) = max_i {f(i, K)}, K <= i <= N
long long *temp = new long long;
*temp = fmax[K][K];
for (int i = K+1; i <= N; ++i)
{
*temp = max(*temp, fmax[i][K]);
}
cout << *temp;
delete temp;
system("pause");
return 0;
}