有關動態規劃

PaperHammer發表於2022-04-24

【以下內容僅為本人在學習中的所感所想,本人水平有限目前尚處學習階段,如有錯誤及不妥之處還請各位大佬指正,請諒解,謝謝!】

引言

通過目前參與的各類比賽和網友的面經,不難發現動態規劃一直是各類競賽和麵試中的高頻和難點,但其最優化的思想在生產生活中的各大領域都具有許多作用。撰寫這篇隨筆的目的既是為了自我學習的總結,也是為了能得到更多的幫助與見解,從而提高自己。在此,我將以自己的想法敘述我解決動規的過程。

動規簡述

動態規劃本質是記憶化搜尋,即記錄之前行為所產生的結果,這也是其比純搜尋演算法更快的原因。

舉個例子:計算斐波那契數列(f[n] = f[n – 1] + f[n – 2]),當我們要計算f[8]時,會使用之前已經算過的f[6]與f[7]直接相加得到答案,而不是再從f[0]開始從頭計算一遍每個值,這樣當資料量很龐大時就可以節省很多時間。

動規特徵

  1.    重疊子問題:每一步,在操作上都具有明顯的相同相似性。

  2.    最優子結構:最終問題的最優解基於每一步的最優解而得出。

解體步驟

  • 大化小;將完整的問題劃分到每一步
  • 分情況定變數,將每一步可做的選擇列出;一般用陣列值代表最終問題的答案,陣列每個索引表示影響到最終答案的變數,類似於自變數與因變數。
  • 推方程;在每一個選擇的基礎上推出當前一步與上一步(或前幾步)間的關係
  • 定邊界;預處理邊界,給定初始值

 

步驟一:大化小

官方稱法為劃分子問題。一般地,我們將每一次操作視為一個子問題,既然滿足重疊子結構,那麼針對每一次操作,處理方式理應是相同或相似的;在最優子結構的性質下,只要得出每一次操作的最優解,就能遞推出最終問題的最優解。

如:

(1)01揹包

題意:有N件物品,一個容量為V的揹包,其中第i件物品價值為w[i],所佔空間為v[i],如何選擇物品使得其不超過容量的同時價值最大。

劃分:最終問題是如何選擇,最終選擇的結果來源於每一次選擇,其關鍵在於每次選擇哪一個物品,那麼就把每次選擇視為一個子問題。並且每次的選擇總是以最小空間換最大價值的最優方式進行,即針對每次選擇,處理方式相同。

 

(2)買賣股票

題意:在每一天,你可以決定是否購買或出售股票。你在任何時候最多隻能持有一股股票。你也可以當天購買,然後在當天出售,求最大利潤。 

劃分:最終問題是如何讓利潤最大,最終利潤來源於每天的利潤,其關鍵在於如何在每一天選擇(買、賣、不買不賣)讓利潤最大,那麼就把每天的操作視為一個子問題。並且每次選擇總以最大利潤為基礎進行操作,每次選擇處理方式相同。 

  來源:力扣(LeetCode)

  連結:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/

 

(3)最長迴文子串

題意:給定一個字串s,找到s中最長的迴文子串

 

劃分:最終問題是如何讓迴文子串最長,最終的迴文串最長基於迴文串的字迴文串最長,問題的關鍵在於如何判斷某子串是不是迴文串,那麼就把每次判斷視為一個子問題。並且每次判斷總是記錄最大回文子串,每次處理方式相同。

  來源:力扣(LeetCode)

  連結:https://leetcode-cn.com/problems/longest-palindromic-substring/

 

(4)編輯距離

題意:給定兩個單詞word1和word2,請返回將word1轉換成word2所使用的最少運算元。可以對一個單詞進行如下三種操作:插入、刪除、替換一個字元。

劃分:最終問題為求最少運算元,最終的運算元基於每次字元的運算元,其關鍵在於兩個字串中當前所選定的兩個字元是否相同,那麼就把每一次判斷視為一個子問題。並且每次判斷總是選擇最少的操作次數進行執行,每次處理方式相同。

  來源:力扣(LeetCode)

  連結:https://leetcode-cn.com/problems/edit-distance

 

步驟二:分情況,定變數

針對劃分出的結果,對每一次操作(選擇或判斷等操作)列出所有可能的情況。

如:

(1)01揹包

劃分:最終問題是如何選擇,最終選擇的結果來源於每一次選擇,其關鍵在於每次選擇哪一個物品,那麼就把每次選擇視為一個子問題。並且每次的選擇總是以最小空間換最大價值的最優方式進行,即針對每次選擇,處理方式相同。

 

定變數:我們需要記錄所選擇的物品數,以及當前所用的空間,存在兩個變數故需要一個二維陣列,陣列值表最終問題的答案即最大價值,每個維度的索引代表一個變數,用f[i][v]表示,選擇了前i件物品,且在所佔空間為v(不超過v)的情況下所產生的最大價值。

 

(2)買賣股票

劃分:最終問題是如何讓利潤最大,最終利潤來源於每天的利潤,其關鍵在於如何在每一天選擇(買、賣、不買不賣)讓利潤最大,那麼就把每天的操作視為一個子問題。並且每次選擇總以最大利潤為基礎進行操作,每次選擇處理方式相同。

      

定變數:由於每天最多隻能持有一股,所以我們需要記錄當前持有股票的狀態即持股或不持股,以及當前所過的天數,陣列值代表最終問題的答案即最大利潤,每個維度的索引代表一個變數,用f[i][flag]表示,從0到i天所獲得的最大利潤。由於持股狀態需要繼續分情況,所以會有兩個陣列f[i][0],f[i][1]分別表示從0到i天,當前持股/不持股時,所獲得的最大利潤。

 

(3)最長迴文子串

劃分:最終問題是如何讓迴文子串最長,最終的迴文串最長基於迴文串的字迴文串最長,問題的關鍵在於如何判斷某子串是不是迴文串,那麼就把每次判斷視為一個子問題。並且每次判斷總是記錄最大回文子串,每次處理方式相同。

      

定變數:對於一個子串而言,如果它是迴文串,並且長度大於2,那麼將它首尾的兩個字母去除之後,它仍然是個迴文串。所以兩個變數首字元與尾字元會影響結果。我們已經知道了某字串是否為迴文串,那麼只需要記錄首尾字元是否相同,利用拼接法(將首尾字元拼接到上面提到的某字串上)進行判斷,用f[i][j]表示從i到j的字串是否為迴文串,判斷首位i與末尾j是否相同即可。

 

(4)編輯距離

劃分:最終問題為求最少運算元,最終的運算元基於每次字元的運算元,其關鍵在於兩個字串中當前所選定的兩個字元是否相同,那麼就把每一次判斷視為一個子問題。並且每次判斷總是選擇最少的操作次數進行執行,每次處理方式相同。

      

定變數:判斷當前的操作方式,首先需要判斷當前兩個字元是否相同,相同則不操作,不同則選擇操作次數最少的一種方式。所以兩個變數i與j會影響結果,我們需要記錄從word1的0到i位轉化為從word2的0到j位所需要的最少操作次數,判斷當前兩個位置上的字元是否相同即可。用f[i][j]表示從word1的0到i位變為word2的0到j位所需要額最少操作次數。

 

步驟三:推方程

列出所有情況後,針對每種情況,推匯出相應的狀態轉移方程,即如何得出在本次操作執行完後的當前最優解。

(1)01揹包

劃分:最終問題是如何選擇,最終選擇的結果來源於每一次選擇,其關鍵在於每次選擇哪一個物品,那麼就把每次選擇視為一個子問題。並且每次的選擇總是以最小空間換最大價值的最優方式進行,即針對每次選擇,處理方式相同。

  

(2)買賣股票

劃分:最終問題是如何讓利潤最大,最終利潤來源於每天的利潤,其關鍵在於如何在每一天選擇(買、賣、不買不賣)讓利潤最大,那麼就把每天的操作視為一個子問題。並且每次選擇總以最大利潤為基礎進行操作,每次選擇處理方式相同。

 

(3)最長迴文子串

劃分:最終問題是如何讓迴文子串最長,最終的迴文串最長基於迴文串的字迴文串最長,問題的關鍵在於如何判斷某子串是不是迴文串,那麼就把每次判斷視為一個子問題。並且每次判斷總是記錄最大回文子串,每次處理方式相同。

 

(4)編輯距離

劃分:最終問題為求最少運算元,最終的運算元基於每次字元的運算元,其關鍵在於兩個字串中當前所選定的兩個字元是否相同,那麼就把每一次判斷視為一個子問題。並且每次判斷總是選擇最少的操作次數進行執行,每次處理方式相同。

 

 

步驟四:定邊界

陣列索引從0開始,在上述遞推式中發現如果索引變數從0開始,會出現負索引的情況,所以我們需要對無法計算到的值進行預處理,直接賦上相應的結果。

如:

(1)01揹包

注意到迴圈遍歷從1開始,則需要對索引為0時的結果進行預處理f[0][v] = 0;

  (2)  買賣股票

同理:f[0][0] = 0,f[0][1] = -prices[0];

  (3)  最長迴文子串

某些特殊情況下可能涉及到初始化一個序列,本題中單個字元一定是迴文串,所以將所有的單個字元的結果全部初始化f[i][i] =  true,f[j][j]= true;

(4)編輯長度

本題中當索引為0時,無法進行訪問計算,所以需要對索引為0的所有情況進行初始化f[i][0] = i,f[0][j] = j;

一般地,初始化邊界時可能只需要對某幾個量進行初始化賦值,也可能對某一序列進行初始化賦值,視情況而定。

 

總結

個人認為,動態規劃的關鍵在於對每一步進行情況劃分(即步驟二)。有些時候我們覺得動態規劃難,並不是因為推導狀態轉移方程難,更多的是我們沒能將每一種情況劃分清楚,不知道每一步有多少種可能的選擇,從而導致了我們無法準確推匯出方程。動態規劃確實比較難掌握,需要我們一步一步去積累,學習是艱苦的也是快樂的,腳踏實地相信我們終能成為理性中的我們,讓我們一起努力!

【感謝您可以抽出時間閱讀到這裡,第一篇部落格可能會有許多不妥之處;受限於水平,許多地方可能存在錯誤,還請各位大佬留言指正,請見諒,謝謝!】

 

#附文中所提到的4個題目的程式碼

(1)01揹包

#include<bits/stdc++.h>
using namespace std; 
int f[1024][1024], v[1024],w[1024],n,k; 
int main()
{
    cin>>n>>k;
    for(int i=1;i<=n;i++)
        cin>>v[i]>>w[i]; 
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=k;j++)
        {
            f[i][j]=f[i-1][j];
            if(j>=v[i])
                f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
    }
    int ans=-1;
    for(int i=0;i<=k;i++)
        ans=max(ans,f[n][i]);
    cout<<ans;
    return 0;
}

 

(2)買賣股票

public class Solution {
    public int MaxProfit(int[] prices) {
        int[,] profit = new int[prices.Length, 2];
        profit[0, 1] = -prices[0];
        profit[0, 0] = 0;
        for(int i = 1; i < prices.Length; i++) {
            profit[i, 1] = Math.Max(profit[i - 1, 1], profit[i - 1, 0] - prices[i]);
            profit[i, 0] = Math.Max(profit[i - 1, 0], profit[i - 1, 1] + prices[i]);
        }
        return profit[prices.Length - 1, 0];
    }
}

 

(3)最長迴文子串

public class Solution {
    public string LongestPalindrome(string s) {
        int len = s.Length, begin = 0, maxLen = 1;
        if(len < 2) return s;
        bool[,] dp = new bool[len, len];//[i, j]是否為迴文串
        for(int i = 0; i < len; i++) dp[i, i] = true;
        for(int L = 2; L <= len; L++)
        {
            for(int i = 0; i < len; i++)//L = j - i + 1;
            {
                int j = L + i - 1;
                if(j >= len) break;
                if(s[i] != s[j]) dp[i, j] = false;
                else
                {
                    if(j - i < 3) dp[i, j] = true;
                    else dp[i, j] = dp[i + 1, j - 1];
                }
                if(dp[i, j] && j - i + 1 > maxLen)
                {
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.Substring(begin, maxLen);
    }
}

 

(4)編輯距離

public class Solution {
    public int MinDistance(string word1, string word2) {
        int n = word1.Length, m = word2.Length;
        int[,] cost = new int[n + 1, m + 1];
        for(int i = 0; i <= n; i++) cost[i, 0] = i;
        for(int j = 0; j <= m; j++) cost[0, j] = j;
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= m; j++) {
                if(word1[i - 1] == word2[j - 1]) cost[i, j] = cost[i - 1, j - 1];
                else cost[i, j] = Math.Min(cost[i - 1, j - 1], Math.Min(cost[i, j - 1], cost[i - 1, j])) + 1;
            }
        return cost[n, m];
    }
}

 

相關文章