Dynamic programming

動態規劃

基礎觀念

把一個問題分成很多子問題

解決子問題

最後再由子問題的答案 得到原本問題的答案

把一個問題分成很多子問題

解決子問題

最後再由子問題的答案 得到原本問題的答案

跟分治有點像

如何從子問題的答案得到原本問題的答案?

如何從子問題的答案得到原本問題的答案?

首先要先依據題目,設計適合的狀態

如何從子問題的答案得到原本問題的答案?

首先要先依據題目,設計適合的狀態

以及狀態與狀態間的關係

如何從子問題的答案得到原本問題的答案?

首先要先依據題目,設計適合的狀態

以及狀態與狀態間的關係

A

B

+10

ex:

再藉由我們推得的狀態間關係

(稱為轉移方程)

依適合的順序一一算出不同狀態的答案

再藉由我們推得的狀態間關係

(稱為轉移方程)

依適合的順序一一算出不同狀態的答案

最後解決原本的問題

爬樓梯

有一隻青蛙一開始在地上

要跳到第n個階梯

他一次可以跳一階or兩階(往上)

請問他有幾種跳到n階的方法

首先要給這個問題設定狀態

首先要給這個問題設定狀態

定義 dp[i] = 青蛙跳到第 i 階的方法數

首先要給這個問題設定狀態

定義 dp[i] = 青蛙跳到第 i 階的方法數

因此答案就等於dp[n]

我們要找出dp[n]的數值

接著尋找狀態與狀態的關係

如果青蛙要跳到第i階,那他必須要先跳到 i-1 or i-2

因此 dp[i] = dp[i-1]+dp[i-2]

我們知道轉移方程了 !

dp[i] = dp[i-1] + dp[i-2]

依據轉移方程

可以看出適合的方向是從小到大算

我們知道轉移方程了 !

dp[i] = dp[i-1] + dp[i-2]

依據轉移方程

可以看出適合的方向是從小到大算

因為這樣當我在算 dp[i] 時

需要的dp[i-1] 、 dp[i-2]都已經算好了

最後就是我們要先找出轉移的起頭

不然只有轉移式是沒有用的

最後就是我們要先找出轉移的起頭

不然只有轉移式是沒有用的

Text

因此我們可以先手動算出較簡單的子問題(狀態)

最後就是我們要先找出轉移的起頭

不然只有轉移式是沒有用的

Text

因此我們可以先手動算出較簡單的子問題(狀態)

dp[1] = 1

dp[2] = 2

接著就可以快樂的一路用轉移方程求出答案了 !

別忘了答案就是dp[n]

#include<bits/stdc++.h>
using namespace std;
#define maxn 100005

int dp[maxn],n;

main(){
    cin>>n;
    dp[1] = 1;
    dp[2] = 2;
    for(int i=3;i<=n;++i){
        dp[i] = dp[i-1] + dp[i-2];
    }
    cout<<dp[n]<<endl;
}

有沒有發現其實這個問題就是費事數列 !

Dp 的複雜度?

Dp 的複雜度?

狀態數量 X 每次轉移

Dp 的複雜度?

狀態數量 X 每次轉移

剛剛的題目為例

狀態數量 : O(n)

每次轉移 : O(1)

O(n) * O(1) = O(n)

各種例題

NNN 顆石頭,每顆石頭的高度為 hih_ihi

青蛙從第 1 顆石頭出發,要跳到第 NNN 顆石頭

如果青蛙目前在第 iii 顆石頭,可以跳到第 i+1i+1i+1 或第 i+2i+2i+2 顆石頭

跳到某顆石頭會產生花費,花費為 ∣hi−hj∣|h_i - h_j|hihj

請計算青蛙到達第 NNN 顆石頭的最小總花費

NNN 顆石頭,每顆石頭的高度為 hih_ihi

青蛙從第 1 顆石頭出發,要跳到第 NNN 顆石頭

如果青蛙目前在第 iii 顆石頭,可以跳到第 i+1i+1i+1 或第 i+2i+2i+2 顆石頭

跳到某顆石頭會產生花費,花費為 ∣hi−hj∣|h_i - h_j|hihj

請計算青蛙到達第 NNN 顆石頭的最小總花費

ex : 

4
10 30 40 20

as : 30

code

#include<bits/stdc++.h>
using namespace std;

int n,h[100005],dp[100005];

main(){
    cin>>n;
    for(int i=1;i<=n;++i) cin>>h[i];
    dp[1] = 0;
    dp[2] = abs(h[1]-h[2]);

    for(int i=3;i<=n;++i){
        dp[i] = min(dp[i-1]+abs(h[i]-h[i-1]),dp[i-2]+abs(h[i]-h[i-2]));
    }
    cout<<dp[n]<<endl;
}

code

#include<bits/stdc++.h>
using namespace std;
#define maxn 100005
#define inf 1e18
#define int long long
int n,k,h[maxn],dp[maxn];

main(){
    cin>>n>>k;
    for(int i=1;i<=n;++i) cin>>h[i];
    for(int i=1;i<=n;++i) dp[i] = inf;

    dp[1] = 0;

    for(int i=2;i<=n;++i){
        for(int j=1;j<=k;++j){
            if(i-j <= 0) continue;
            dp[i] = min(dp[i],dp[i-j]+abs(h[i-j]-h[i]));
        }
    }
    cout<<dp[n]<<endl;
}

code

#include<bits/stdc++.h>
using namespace std;
#define maxn 100005
#define inf 1e18
#define int long long

int n,a[maxn],b[maxn],c[maxn],dp[maxn][3];
main(){
    cin>>n;
    for(int i=1;i<=n;++i) cin>>a[i]>>b[i]>>c[i];

    dp[1][0] = a[1];
    dp[1][1] = b[1];
    dp[1][2] = c[1];
    for(int i=2;i<=n;++i){
        dp[i][0] = a[i] + max(dp[i-1][1],dp[i-1][2]);
        dp[i][1] = b[i] + max(dp[i-1][0],dp[i-1][2]);
        dp[i][2] = c[i] + max(dp[i-1][1],dp[i-1][0]);
    }
    cout<<max({dp[n][0],dp[n][1],dp[n][2]})<<endl;
}

最大連續和 ( O(n) )

code

#include<bits/stdc++.h>
using namespace std;
#define maxn 100005
#define inf 1e18
#define int long long

int n,a[maxn],dp[maxn];

main(){
    cin>>n;
    for(int i=1;i<=n;++i) cin>>a[i];
    dp[1] = a[1];
    int as = dp[1];
    for(int i=2;i<=n;++i){
        dp[i] = max(a[i],dp[i-1]+a[i]);
        as = max(as,dp[i]);
    }
    cout<<as<<endl;
}

各種背包問題

什麼是背包問題?

有很多物品,依據題目規則,可能有不同的數值,例如重量、價值,目的就是要將物品放入一個有重量限制的背包,最後讓背包中裝的物品價值最大

什麼是背包問題?

有很多物品,依據題目規則,可能有不同的數值,例如重量、價值,目的就是要將物品放入一個有重量限制的背包,最後讓背包中裝的物品價值最大

背包問題有非常多變形,

對經典的 0-1背包、無限背包、有限背包

回朔解等

0-1背包

有n個物品,每個物品重量為mi,價值為vi,

背包重量限制為w

求在符合重量限制時,最大價值是多少

0-1背包

有n個物品,每個物品重量為mi,價值為vi,

背包重量限制為w

求在符合重量限制時,最大價值是多少

ex:

三個物品,背包線種為8

三種物品 m 、 v分別如下
1 : 3 30
2 : 4 50
3 : 5 60

0-1背包

有n個物品,每個物品重量為mi,價值為vi,

背包重量限制為w

求在符合重量限制時,最大價值是多少

ex:

三個物品,背包線種為8

三種物品 m 、 v分別如下
1 : 3 30
2 : 4 50
3 : 5 60

ans : 90

設定狀態

在這個問題,前面的一維狀態已經無法表示此問題的狀態

需要加入更多的訊息

設定狀態

在這個問題,前面的一維狀態已經無法表示此問題的狀態

需要加入更多的訊息

定義

dp[i][j] = 考慮前 i 個物品,背包重量限制為 j 的最大價值

轉移方程

求dp[i][j]可以有兩種情況

轉移方程

求dp[i][j]可以有兩種情況

case 1 : 我沒拿第 i 個物品到背包中  ->  dp[i][j] = dp[i-1][j]

轉移方程

求dp[i][j]可以有兩種情況

case 1 : 我沒拿第 i 個物品到背包中  ->  dp[i][j] = dp[i-1][j]

case 2 : 我拿了第 i 個物品到背包中  ->  dp[i][j] = dp[i-1][j-mi]+vi

轉移方程

求dp[i][j]可以有兩種情況

case 1 : 我沒拿第 i 個物品到背包中  ->  dp[i][j] = dp[i-1][j]

case 2 : 我拿了第 i 個物品到背包中  ->  dp[i][j] = dp[i-1][j-mi]+vi

我要選擇價值大的case:

dp[i][j] = max( dp[i-1][j] , dp[i-1][j-mi]+vi )

轉移順序

dp[i][j] = max( dp[i-1][j] , dp[i-1][j-mi]+vi )

觀察到計算dp[i][j]時,需要用到的是

dp[i-1]的結果

 

因此轉移順序為

                         for i = 1~n :

                             for j = 1~w :

                                 (轉移)

using namespace std;
#define int long long
#define inf 1e18
#define maxn 1000006

int n,w,dp[105][100005];
pair<int,int> p[105];

main(){
    cin>>n>>w;
    for(int i=1;i<=n;++i){
        cin>>p[i].first>>p[i].second;
    }
    for(int i=1;i<=n;++i){
        for(int j=1;j<=w;++j){
            if(j-p[i].first >= 0){
                dp[i][j] = max(dp[i-1][j],dp[i-1][j-p[i].first]+p[i].second);
            }else{
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    cout<<dp[n][w];
}

目前空間複雜度是 O(nw)

還可以用更少嗎?

1 : 滾動dp

對於一些只取到  i-1 資訊的轉移,i-2以前的資訊不會再被用到

1 : 滾動dp

對於一些只取到  i-1 資訊的轉移,i-2以前的資訊不會再被用到

dp[i][j] = max(dp[i-1][j],dp[i-1][j-mi]+vi)

1 : 滾動dp

對於一些只取到  i-1 資訊的轉移,i-2以前的資訊不會再被用到

dp[i][j] = max(dp[i-1][j],dp[i-1][j-mi]+vi)

觀察到轉移式中 dp[i] ,都只有用到 dp[i-1] 的資訊

因此我可以只記錄 dp[0][j] 、 dp[1][j]

交替使用

滾動dp 0-1背包

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define inf 1e18
#define maxn 1000006

int n,w,dp[2][100005];
pair<int,int> p[105];

main(){
    cin>>n>>w;
    for(int i=1;i<=n;++i){
        cin>>p[i].first>>p[i].second;
    }
    for(int i=1;i<=n;++i){
        for(int j=1;j<=w;++j){
            if(j-p[i].first >= 0){
                dp[i%2][j] = max(dp[(i+1)%2][j],dp[(i+1)%2][j-p[i].first]+p[i].second);
            }else{
                dp[i%2][j] = dp[(i+1)%2][j];
            }
        }
    }
    cout<<dp[n%2][w];
}

2 : 依照轉移順序壓狀態

2 : 依照轉移順序壓狀態

dp[i][j] = max(dp[i-1][j],dp[i-1][j-mi]+vi)

每次計算dp[i][j] 

只會用到 dp[i-1][k]  (k<j)

dp[j] = max(dp[j],dp[j-mi]+vi)

因此可以變成一維

dp[j] : 重量限制為 j 考慮前 i 項物品的答案 

但是沒有狀態 i

這裡的dp[j]、dp[j-mi]

是上一次的dp[j] 、dp[j-mi] (考慮 1~i-1項物品的)

因為dp[i]、dp[i-1]現在是共用同一個陣列

因此枚舉 j (重量限制) 的方向要相反

才可以確保每次取到的資訊都是dp[i-1]的

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define inf 1e18
#define maxn 1000006
int n,w,dp[100005];
pair<int,int> p[105];
main(){
    cin>>n>>w;
    for(int i=1;i<=n;++i){
        cin>>p[i].first>>p[i].second;
    }
    for(int i=1;i<=n;++i){
        for(int j=w;j>=p[i].first;--j){
            dp[j] = max(dp[j],dp[j-p[i].first]+p[i].second);
        }
    }
    cout<<dp[w];
}

無限背包

有n種物品,每種物品可以拿任意個,每個物品重量為mi,價值為vi,

背包重量限制為w

求在符合重量限制時,最大價值是多少

ex:

三個物品,背包線種為8

三種物品 m 、 v分別如下
1 : 3 30
2 : 4 50
3 : 5 60

as :  100

設定狀態

定義

dp[i][j] = 考慮前 i 個物品,背包重量限制為 j 的最大價值

(狀態與0-1背包一樣)

轉移

求dp[i][j]可以有兩種情況

case 1 : 我沒拿第 i 個物品到背包中  ->  dp[i][j] = dp[i-1][j]

case 2 : 我再拿了一個第 i 個物品到背包中  ->

 dp[i][j] = dp[i][j-mi]+vi

綜合兩種情況 dp[i][j] = max(dp[i-1][j],dp[i][j-mi]+vi)

其實整題的解法與0-1背包就只有差別在

我是拿了第i個物品

還是我再多拿一個第i個物品

與0-1背包比較

其實整題的解法與0-1背包就只有差別在

我是拿了第i個物品

還是我再多拿一個第i個物品

與0-1背包比較

轉移式也只差了 i-1 -> i

dp[i][j] = max(dp[i-1][j],dp[i][j-mi]+vi)

dp[i][j] = max(dp[i-1][j],dp[i-1][j-mi]+vi)

好好思考兩者的差異

與轉移式修改的意義

LCS

Longest common subsequence

有兩個字串,長度分別為n,m,

要求兩個字串的最長共同子序列

的長度,並印出任何一個LCS

axyb
abyxb

ans : 長度 : 3, axb

 

Longest common subsequence

有兩個字串,長度分別為n,m,以下稱S, T

要求S、T的最長共同子序列

的長度,並印出任何一個LCS

axyb
abyxb

ans : 長度 : 3, axb

 

先解決第一個問題

LCS多長 ?

定義狀態

定義 dp[i][j] =

考慮 S 的前 i 個字元

考慮 T 的前 j 個字元

的LCS長度

轉移方程 dp[i][j] = ?

考慮兩種情況 :

case1 : S[i] == T[i] : 

dp[i][j] = dp[i-1][j-1] + 1

case2 : S[i] != T[i] :

dp[i][j] = max(dp[i-1][j] , dp[i][j-1])

最後答案即為

dp[n][m]

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define maxn 3005
int n,m,dp[maxn][maxn];
string S,T;
main(){
    cin>>S>>T;
    n = S.size();
    m = T.size();
    S = ' '+S;
    T = ' '+T;
    for(int i=1;i<=n;++i){
        for(int j=1;j<=m;++j){
            if(S[i] == T[j]) dp[i][j] = dp[i-1][j-1] + 1;
            else dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
        }
    }
    cout<<dp[n][m]<<endl;
}

接下來要來找LCS實際長怎樣

我們紀錄每個dp[i][j]是從哪裡轉移過來的

 

若 S[i] == T[j]  :  從  dp[i-1][j-1]

 

若 S[i] != T[j] 且 dp[i-1][j] > dp[i][j-1]  : 從 dp[i-1][j]

 

若 S[i] != T[j] 且 dp[i-1][j] < dp[i][j-1]  : 從 dp[i][j-1]

記錄好轉移來源後,我可以從dp[n][m]

一路依照轉移來源往回走

每次遇到轉移來源為 dp[i-1][j-1]

就代表我將 S[i] 與 T[j] 配對再一起

記錄好轉移來源後,我可以從dp[n][m]

一路依照轉移來源往回走

每次遇到轉移來源為 dp[i-1][j-1]

就代表我將 S[i] 與 T[j] 配對再一起

因此把答案加入 S[i] or T[j]

最後將答案反轉再印出就可以了

(因為找答案時是從後面往前找)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define maxn 3005

string s,t,as;
int ls,lt,dp[maxn][maxn],d[maxn][maxn];
main(){
    cin>>s>>t;
    ls = s.size(), lt = t.size();
    s = ' '+s, t = ' '+t;
    for(int i=1;i<=ls;++i){
        for(int j=1;j<=lt;++j){
            if(s[i] == t[j]) dp[i][j] = dp[i-1][j-1] + 1, d[i][j] = 1;
            else dp[i][j] = max(dp[i-1][j],dp[i][j-1]), d[i][j] = (dp[i-1][j] > dp[i][j-1])?2:3;
        }
    }
    int x = ls,y = lt;
    while(x >= 1 && y >= 1){
        if(d[x][y] == 1) as += s[x], x--, y--;
        else if(d[x][y] == 2) x--;
        else y--;
    }
    reverse(as.begin(),as.end());
    cout<<as<<endl;
}

deck

By maxbrucelen