動態規劃算法學習總結


動態規劃與貪心、分治的區別

  • 貪心算法(Greed alalgorithm) 是一種在每一步選擇中都採起在當前狀態下最好或最優(即最有利)的選擇,從而但願致使全局結果是最好或最優的算法。
  • 分治算法(Divide and conquer alalgorithm) 字面上的解釋是「分而治之」,就是把一個複雜的問題分紅兩個或更多的相同或類似的子問題,直到最後子問題能夠簡單的直接求解,原問題的解即子問題的解的合併。
  • 動態規劃算法(Dynamic programming,DP) 經過將原問題分解爲相對簡單的子問題的方式來求解複雜問題。一般許多子問題很是類似,爲此動態規劃法試圖僅僅解決每一個子問題一次,從而減小計算量:一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次須要同一個子問題解之時直接查表。

貪心法在處理每一個子問題時,不能回退,而動態規劃能夠保存以前的結果,擇優選擇。下面針對Interval Scheduling 問題,分析動態規劃在實際問題中的應用。ios


Interval Scheduling 問題

  • 以下圖所示,每一個長條方塊表明一個工做,總有若干個工做a、b... h,橫座標是時間,方塊的起點和終點分別表明這個工做的起始時間和結束時間。算法

  • 當兩個工做的工做時間沒有交叉,即兩個方塊不重疊時,表示這兩個工做是兼容的(compatible)。編程

  • 當給每一個工做賦權值都爲1時,則稱爲 Unweighted Interval Scheduling 問題;當給每一個工做賦不一樣的正權值時,則稱爲 Weighted Interval Scheduling 問題。ide

  • 問題最終是要找到一個工做子集,集合內全部工做權值之和最大集合內每一個工做都兼容測試

對於 Unweighted Interval Scheduling 問題,使用貪心算法便可求解,具體作法是按照結束時間對全部工做進行排序,而後從結束最晚的工做開始,依次排除掉與前一個不兼容的工做,剩下的工做所組成的集合即爲所求。優化

然而,對於 Weighted Interval Scheduling 問題,貪心法找到的解可能不是最優的了。此時考慮使用動態規劃算法解決問題,兼顧權值選擇和兼容關係。ui

定義P(j)

一、首先依然按照結束時間對全部的工做進行排序;spa

二、定義p(j)爲在工做j以前,且與j兼容的工做的最大標號,經過分析每一個工做的起始時間和結束時間,能夠很容易計算出p(j);3d

三、例以下圖所示,p(8)=5,由於工做7和6都與8不兼容,工做1到5都與8兼容,而5是其中索引最大的一個,因此p(8)=5。同理,p(7)=3,p(2)=0。code

分析遞歸關係

一、定義opt(j)是j個工做中,所能選擇到的最佳方案,即opt(j)是最大的權值和;

二、對於第j個工做,有兩種狀況:

  • case 1: 工做j包含在最優解當中,那麼往前遞推一步,j以前能選擇到的最優解是opt(p(j)),即

  • case 2: 工做j不在最優解中,那麼從j個工做中選取解集和從j-1個工做中選取解集是同樣的,即

三、當j=0時,顯示結果爲0,這是邊界條件。

後一步的結果取前一步全部可能狀況的最大值,所以綜上所述,能獲得動態規劃的遞歸關係爲:

代碼實現

一、遞歸法

遞歸會使得空間複雜度變高,通常不建議使用。

二、自底向上法

從小到大進行計算,這樣每次均可以利用前一步計算好的值來計算後一步的值,算法時間複雜度爲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;
}
相關文章
相關標籤/搜索