動態規划 所有題型的總結


1 動態規划

1.1 定義

動態規划的核心是狀態和狀態轉移方程。

在記憶化搜索中,可以為正在處理的表項聲明一個引用,簡化對它的讀寫操作;

動態規划解決的是多階段決策問題;

初始狀態→│決策1│→│決策2│→…→│決策n│→結束狀態

和分治法最大的區別在於:適合於用動態規划的問題,經過分解以后得到的子問題往往不是相互獨立的(即下一個子階段的求解是建立在上一個子階段的基礎之上,進行進一步的求解,而不是相互獨立的問題)

動態規划問題一般由難到易分為一維動態規划,二維動態規划,多維動態規划,以及多變量動態規划問題。其中多維動態規划問題又可以進行降維。動態規划問題求解的最重要的一步就是求解出 狀態轉移方程

1.2 特性

  • 最優化原理:如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構,即滿足最優化原理.
  • 無后效性:即某階段狀態一旦確定,就不受這個狀態以后決策的影響。也就是說,某狀態以后的過程不會影響以前的狀態,只與當前狀態有關
  • 有重疊子問題:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被多次使用到。(該性質並不是動態規划適用的必要條件,但是如果沒有這條性質,動態規划算法同其他算法相比就不具備優勢,動態規划可以避免多次計算)

1.3 例子

還是做題最實在!

1.3.1 最長公共子序列

問題描述:給定兩個序列:X[1...m]和Y[1...n],求在兩個序列中同時出現的最長子序列的長度。

如果按照最普通的方法,就是遍歷所有可能的情況(將較短字符串中所有的子串和較長字符串中的子串進行比較),取所有可能的情況中最長的子串;

int DP::LongestCommonSubsequence(string &X, string &Y, int m, int n) {
    if (m == 0 || n == 0) {
        return 0;
    }

    if (X[m-1] == Y[n-1]) {
        return LongestCommonSubsequence(X, Y, m-1, n-1) + 1;
    }
    else {
        return max(LongestCommonSubsequence(X, Y, m-1, n), LongestCommonSubsequence(X, Y, m, n-1));
    }
}

void DP::testLongestCommonSubstring() {
    string x = "abcdefg", y = "efg";
    int result = LongestCommonSubsequence(x, y, (int)x.size(), (int)y.size());
    cout << "result:" << result;
}

很顯然,這花費的時間是指數級的,非常慢;

那么采用動態規划是怎么做的?

思路:我們可以想象成樹,兩個字符串都分別進行發散,對於一個結點來說,左邊是左邊的字符串進行改變,右邊則是右邊的字符串進行改變,直到兩個字符串都相等。

  • 第一步應該是找出遞推公式:

這里的 C[i,j] 代表的意思是字符串 X 中到達下標 i 和字符串 Y 中到達下標 j 的時候的最長子串個數;

  • 第二步是寫出偽代碼
LCS(x,y,i,j)
	if x[i] = y[j]
		then C[i,j] ← LCS(x,y,i-1,j-1)+1
		else C[i,j] ← max{LCS(x,y,i-1,j),LCS(x,y,i,j-1)}
	return C[i,j]
  • 最后是寫出代碼

上面的偽代碼對於每一種情況都會往前重新計算一遍,完全沒有必要,用一個數組保存之前計算的值即可;

int DP::LongestCommonSubsequence(string &X, string &Y, int m, int n) {
    vector<vector<int>> dp(X.size() + 1, vector<int>(Y.size() + 1, 0));
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 因為是從1開始的,所以字符串下標要減去1
            if (X[i-1] == Y[j-1]) {
                dp[i][j] = dp[i-1][j-1] + 1;
            }
            else {
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
    }

    return dp[m][n];
}

void DP::testLongestCommonSubsequence() {
    string x = "abcdefg", y = "efg";
    int result = LongestCommonSubsequence(x, y, (int)x.size(), (int)y.size());
    cout << "result:" << result;
}

看看dp數組:

 0 0 0 0 0 0
 0 1 1 1 1 1
 0 1 2 2 2 2
 0 1 2 3 3 3
 0 1 2 3 3 3
 0 1 2 3 3 3
 0 1 2 3 4 4
 0 1 2 3 4 5

參考文章
動態規划(Dynamic Programming)

1.3.2 最長公共子串

和上面最長公共子序列不同的是,子串要求連續,不像子序列只要順序保證是正確的就行了,所以使用一個變量來記錄;

/**************************************
 * // 最長公共子串問題
 ***************************************/
int DP::LongestCommonSubstring(string &X, string &Y, int m, int n) {
    vector<vector<int>> dp(X.size() + 1, vector<int>(Y.size() + 1, 0));
    int max = 0;
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 因為是從1開始的,所以字符串下標要減去1
            if (X[i-1] == Y[j-1]) {
                if (dp[i-1][j-1]) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                }
                else {
                    dp[i][j] = 1;
                }

                if (dp[i][j] > max) {
                    max = dp[i][j];
                }
            }
        }
    }

    for (int i = 0; i <= m; i++) {
        for (int j = 0; j <= n; j++) {
            cout << " " << dp[i][j];
        }
        cout << endl;
    }

    return max;
}

void DP::testLongestCommonSubstring() {
    string x = "abcdefg", y = "abcfg";
    int result = LongestCommonSubstring(x, y, (int)x.size(), (int)y.size());
    cout << "result:" << result << endl;
}

看看dp數組:

 0 0 0 0 0 0
 0 1 0 0 0 0
 0 0 2 0 0 0
 0 0 0 3 0 0
 0 0 0 0 0 0
 0 0 0 0 0 0
 0 0 0 0 1 0
 0 0 0 0 0 2

可以發現,對角線上連續大於0的值則為長度;最長的子串為abc,長度為3,次長為fg,長度為2;

1.3.3 最長遞增子序列

問題描述:給定一個序列:X[1...m],求在這個序列中出現的最長遞增子序列的長度。

/**************************************
 * // 最長遞增子串問題
 ***************************************/
int DP::LongestIncreasingSubstring(string &X, int m) {
    vector<int> dp(X.size(), 0);
    int max = 0;

    for (int i = 0; i < m; i++) { //到達
        dp[i] = 1;
        for (int j = 0; j < i; j++) {
            if (X[i] > X[j]) {
                dp[i] = dp[j] + 1;
            }
            else { //因為是連續的,所以只要不符合就重置
                dp[i] = 1;
            }
        }
    }

    for (int i = 0; i < m; i++) {
        cout << " " << dp[i];
        if (dp[i] > max) {
            max = dp[i];
        }
    }
    cout << endl;

    return max;
}

void DP::testLongestIncreasingSubstring() {
    string x = "babcak";
    int result = LongestIncreasingSubstring(x, (int)x.size());
    cout << "result:" << result << endl;
}

1.3.4 矩陣鏈乘積

題目描述

給定n個矩陣{A1,A2,…,An},其中Ai與Ai+1是可乘的,i=1,2…,n-1。如何確定計算矩陣連乘積的計算次序,使得依此次序計算矩陣連乘積需要的數乘次數最少。

思路

對於矩陣連乘問題,最優解就是找到一種計算順序,使得計算次數最少;

假設 dp[i, j] 表示第 i 個矩陣到達第 j 個矩陣這段的最優解;

將矩陣連乘積 簡記為A[i:j] ,這里i<=j.假設這個最優解在第k處斷開,i<=k<j,則A[i:j]是最優的,那么A[i,k]和A[k+1:j]也是相應矩陣連乘的最優解。把i到j分成兩部分,看哪種分法乘的次數最少,而k就是i到j中斷開的部分,也就是兩個括號中間的部分;

  • 狀態轉移方程
當i=j時,A[i,j]=Ai, m[i,j]=0;(表示只有一個矩陣,如A1,沒有和其他矩陣相乘,故乘的次數為0)

當i<j時,m[i,j]=min{m[i,k]+m[k+1,j] +pi-1*pk*pj} ,其中 i<=k<j

也就是

實現

/**************************************
 * // 矩陣鏈乘積,求最小乘積次數
 ***************************************/
void DP::MatrixChainMultiplication(vector<int> data, int n,vector<vector<int>>& m_dp, vector<vector<int>>& s_dp) {
    //矩陣段長度為1,則 dp[][] 中對角線的值為0,表示只有一個矩陣,沒有相乘的.
    for(int i = 1;i<=n;i++)
        m_dp[i][i] = 0;

    // 從第二個開始(第一個也是0),當前的乘次數取決於下一個的乘次數
    // 對角線循環,r表示矩陣的長度(2,3…逐漸變長)
    for(int r = 2;r<=n;r++) {
        // 行循環
        for(int i = 1; i<=n-r+1; i++) {
            // 列的控制,當前矩陣段(Ai~Aj)的起始為Ai,尾為Aj
            int j = r+i-1;

            //例如對(A2~A4),則i=2,j=4,下面一行的m[2][4] = m[3][4]+p[1]*p[2]*p[4],即A2(A3A4)
            m_dp[i][j] = m_dp[i+1][j] + data[i-1]*data[i]*data[j]; //計算次數

            s_dp[i][j] = i;//記錄斷開點的索引

            //循環求出(Ai~Aj)中的最小數乘次數,遍歷所有可能的情況
            for(int k = i+1 ; k<j;k++) {
                //將矩陣段(Ai~Aj)分成左右2部分(左m[i][k],右m[k+1][j])
                //再加上左右2部分最后相乘的次數(p[i-1] *p[k]*p[j])
                int t = m_dp[i][k] + m_dp[k+1][j] + data[i-1] *data[k]*data[j];

                if(t < m_dp[i][j]) {
                    m_dp[i][j] = t;
                    s_dp[i][j] = k;  //保存最小的,即最優的結果
                }
            }
        }
    }
}

void DP::testMatrixChainMultiplication() {
    vector<int> data = {30,35,15,5,10,20,25};//記錄6個矩陣的行和列,注意相鄰矩陣的行和列是相同的
    vector<vector<int>> m_dp(7, vector<int>(7, 0));//存儲第i個矩陣到第j個矩陣的計算代價(以乘法次數來表示)
    vector<vector<int>> s_dp(7, vector<int>(7, 0));//存儲第i個矩陣到第j個矩陣的最小代價時的分為兩部分的位置
    int n=6;//矩陣個數
    MatrixChainMultiplication(data, n , m_dp, s_dp);

    cout << "---計算代價---" << endl;
    for (int i = 0; i < m_dp.size(); i++) {
        for (int j = 0; j < m_dp[0].size(); j++) {
            cout << " " << m_dp[i][j];
        }
        cout << endl;
    }

    cout << "---最小代價時分為兩邊的位置" << endl;
    for (int i = 0; i < s_dp.size(); i++) {
        for (int j = 0; j < s_dp[0].size(); j++) {
            cout << " " << s_dp[i][j];
        }
        cout << endl;
    }
}

運行結果為:

---計算代價---
 0 0 0 0 0 0 0
 0 0 15750 7875 9375 11875 15125
 0 0 0 2625 4375 7125 10500
 0 0 0 0 750 2500 5375
 0 0 0 0 0 1000 3500
 0 0 0 0 0 0 5000
 0 0 0 0 0 0 0
---最小代價時分為兩邊的位置
 0 0 0 0 0 0 0
 0 0 1 1 3 3 3
 0 0 0 2 3 3 3
 0 0 0 0 3 3 3
 0 0 0 0 0 4 5
 0 0 0 0 0 0 5
 0 0 0 0 0 0 0

表示第i個矩陣到第j個矩陣的計算代價矩陣m[i][j]和表示第i個矩陣到第j個矩陣的最小代價時的分為兩部分的位置矩陣s[i][j]的結果如下圖:

從上面左圖的m矩陣可以看出任意第i個到第j個矩陣連乘的乘法次數。最終的加括號形式為:(A1(A2A3))((A4A5)A6)

參考文章
【算法導論】
動態規划之矩陣鏈乘法

0010算法筆記——【動態規划】矩陣連乘問題

1.3.5 數塔問題

問題描述:

數塔第i層有i個結點,要求從頂層走到底層,若每一步只能走到相鄰的結點,則經過的結點的數字之和最大是多少?

用二維數組則為:

9
12	15
10	6	8
2    18	9	5
19	7	10	4	16

假設9首先被輸入,是第 0 層,越往下層數不斷遞增;

思路

為了保證整條路徑的和是最大的,下一層的走向取決於再下一層上的最大值是否已經求出才能決定,所以要做一個自頂向下的分析,自底向上的計算;

  • 狀態轉移方程
dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + data[i][j]

dp[i+1][j] 為左結點,dp[i+1][j+1] 為右結點;

實現

/**************************************
 * // 數塔問題
 ***************************************/
int DP::MaxSumTower(vector<vector<int>> nums, int m, int n) {
    vector<vector<int>> dp(m, vector<int>(n, 0));

    // 用底層的值來初始化dp,m為行,n為列
    for (int i = 0; i < n; i++) {
        dp[m-1][i] = nums[m-1][i];
    }

    // 自底向上,父結點的最大值取決於左結點或者右結點的最大值
    for (int i = m-2; i >= 0; i--) {
        for (int j = 0; j < n; j++) {
            if (i >= j) { //過濾多余的
                dp[i][j] = std::max(dp[i+1][j], dp[i+1][j+1]) + nums[i][j];
            }
        }
    }

    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            cout << " " << dp[i][j];
        }
        cout << endl;
    }

    return dp[0][0];
}

1.3.6 01背包問題

題目描述

有編號分別為a,b,c,d,e的五件物品,它們的重量分別是2,2,6,5,4,它們的價值分別是6,3,5,4,6,現在給你個承重為10的背包,如何讓背包里裝入的物品具有最大的價值總和?

思路

dp[x][y] 表示體積不超過 y 且可選前 x 種物品的情況下的最大總價值

遞歸關系:

  • mp[0][y] = 0
  • mp[x][0] = 0
  • 當 v[x] > y 時,mp[x][y] = mp[x-1][y]
  • 當 v[x] <= y 時,mp[x][y] = max{ mp[x-1][y], p[x] + mp[x-1][y-v[x]] }

解釋如下:

  • 表示體積不超過 y 且可選前 0 種物品的情況下的最大總價值,沒有物品可選,所以總價值為 0
  • 表示體積不超過 0 且可選前 x 種物品的情況下的最大總價值,沒有物品可選,所以總價值為 0
  • 因為 x 這件物品的體積已經超過所能允許的最大體積了,所以肯定不能放這件物品, 那么只能在前 x-1 件物品里選了
  • x 這件物品可能放入背包也可能不放入背包,所以取前兩者的最大值就好了, 這樣就將前兩種情況都包括進來了

實現

/**************************************
 * // 01背包問題
 ***************************************/
int DP::ZeroOneBackpack(vector<Goods> goods, int limit_weight) {
    int m = goods.size();
    int n = limit_weight;
    vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));

    // 沒有選擇物品的時候價值為0
    for (int i = 1; i < n; i++) {
        dp[0][i] = 0;
    }

    // 重量為0的時候什么物品都選不了,價值自然也為0
    for (int i = 1; i < m; i++) {
        dp[i][0] = 0;
    }

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (goods[i-1].weight > j) { //超出限制的重量
                dp[i][j] = dp[i-1][j];
            }
            else { //可能放入背包也可能不放入背包,取兩者情況的最大值
                dp[i][j] = std::max(dp[i-1][j], dp[i-1][j - goods[i-1].weight] + goods[i-1].value);
            }
        }
    }

    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            cout << " " << dp[i][j];
        }
        cout << endl;
    }

    return dp[m][n];
}

void DP::testZeroOneBackpack() {
    vector<Goods> goods;
    goods.emplace_back(0, 0);
    goods.emplace_back(60, 10);
    goods.emplace_back(100, 20);
    goods.emplace_back(120, 30);
    int result = ZeroOneBackpack(goods, 50);
    cout << "result:" << result << endl;
}

1.3.7 最大連續子序列之和

問題描述:給定一個序列:X[1...m],求在這個序列中出現的最大的連續子序列之和。

  • 狀態轉移方程
dp[i] = std::max(dp[i-1] + nums[i], nums[i]);
  • 實現
/**************************************
 * // 最大連續子序列和
 ***************************************/
int DP::MaxContinusSubsequenceSum(int* nums, int m) {
    vector<int> dp(m, 0);
    int max = INT_MIN;

    dp[0] = nums[0];
    for (int j = 1; j < m; j++) {
        dp[j] = std::max(dp[j-1] + nums[j], nums[j]);
    }

    for (int i = 0; i < m; i++) {
        cout << " " << dp[i];
        if (dp[i] > max) {
            max = dp[i];
        }
    }
    cout << endl;

    return max;
}

void DP::testMaxContinusSubsequenceSum() {
    int data[] = {
            1,-2,3,-1,7
    };

    int result = MaxContinusSubsequenceSum(data, 5);
    cout << "result:" << result << endl;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM