Dynamic Programming

動態規劃

INDEX

  • 基本原理
  • 簡單例題
  • LIS
  • LCS
  • 背包問題

基本原理

基本原理

用表格紀錄programming

dp:用變動的表格來求解

比較像一種思考模式、手段

而不是演算法

基本概念

跟分治有點像

將問題分成子問題

接著合併、解決子問題

最後得到答案

基本概念

1.定義子問題

2.找到問題和子問題間的(遞迴)關係

3.找出計算的順序

並避免用遞迴計算

基本概念

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

首先子問題我們可以稱為狀態

看到題目的時候需要設計適合的狀態

接著要去想狀態間的關係(轉移方程)

ex:

A

B

+10

基本概念

接著依照順序

把問題解決

得到最後的答案

例題1 爬樓梯

有個小朋友要上樓梯

每次可以往上走1階或2階(往上)

有幾種可以到n階的方法

例題1 爬樓梯

首先

來設定狀態

我們設定dp[i]是走到第i格的方法數

因此最後要求的答案

要算出dp[n]的數值

例題1 爬樓梯

找出狀態間的關係

今天要走到第i階之前

要先走到i-1跟i-2階

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

例題1 爬樓梯

用遞迴式表示

\begin{cases} n\qquad \qquad \qquad \qquad \qquad \qquad \qquad if\ n<3\\ f(n)=f(n-1)+f(n-2)\qquad otherwise \end{cases}

發現在用遞迴的時候要相信呼叫完他會幫你算好

如果從小開始算的話?

一般人正常算費式數列應該都是這樣算的吧

其實就是費事數列

用到dp[i]時 dp[i-1],dp[i-2]的值已經算好了

例題1 爬樓梯

一開始要有個狀態

才能一直轉移上去

dp[1]=1;
dp[2]=2;

接著轉移

for(int i=3;i<=n;i++){
	dp[i]=dp[i-1]+dp[i-2];
}

答案

cout<<dp[n]<<'\n';

例題2 走格子

有一個m*n的表格

從左上角走到右下角

每一步只能往右或往下一格

有幾種走法

例題2

先定義子問題

dp[i][j]=走到(i,j)的方法數

接著是轉移式

由於只能往右或往下走

所以當i>1 && j>1的時候

這一格可以從上面或左邊走過來

答案:dp[n][m]

例題2

初始狀態
 

要把所有沒有上面或左邊的先手算完

也就是這兩排

而這兩排都只有一條路

所以都是1

這是轉移的方式

方法數是從上面和左邊走下來的總和

例題2

初始狀態

for(int i=1;i<=n;i++)dp[i][1]=1;
for(int i=1;i<=m;i++)dp[1][i]=1;

轉移式

for(int i=2;i<=n;i++){
	for(int j=2;j<=m;j++){
    	dp[i][j]=dp[i-1][j]+dp[i][j-1];
    }
}

答案

cout<<dp[n][m]<<'\n';

這幾題讓你們稍微感受一點點

dp的感覺

後面的題目會一步步建立起dp的思維模式

簡單例題

例題1 爬樓梯最小成本

跟一開始的費事數列有點像?

看到dp題目要怎麼開始想

狀態!

今天定義dp[i]是走到第i格最小成本

答案:dp[n]

要怎麼轉移?

例題1 爬樓梯最小成本

轉移式!

今天我在第i格

我能從我下面一格或兩格走來

那既然dp[i]是第i格的最小

dp[i-1],dp[i-2]就是i-1,i-2格的最小

那今天我們要接著走的應該要選小的繼續走

所以轉移式就出來了!

例題1 爬樓梯最小成本

轉移式!

dp[i]=min(dp[i-1],dp[i-2])+cost[i];

選小的(取min)、繼續走(+這格成本)

初始狀態

走第1格只有1個成本->cost[1]

走第2格 也能只有1個成本->cost[2]

dp[1]=cost[1];
dp[2]=cost[2];

答案

cout<<dp[n]<<'\n';

例題1 爬樓梯最小成本

每次轉移:O(1)

n次:O(n)

時間複雜度:

O(n)
#include<bits/stdc++.h>
#define maxn 100005
using namespace std;
int dp[maxn],cost[maxn];
int main(){
    int n;cin>>n;
    for(int i=1;i<=n;i++)cin>>cost[i];
    dp[1]=cost[1];
    dp[2]=cost[2];
    for(int i=3;i<=n;i++){
        dp[i]=min(dp[i-1],dp[i-2])+cost[i];
    }
    cout<<dp[n]<<'\n';
}

把上面題目刷完就能高中競程輕鬆一半

這個裸題蠻多的

都寫

dp多刷題就能比較抓得住感覺

基本每次北市賽、TOI、APCS基本都會考

例題2 frog1

每次可以1格或2格

但花費|h[i]-h[j]|

狀態!

dp[i]=跳到第i格最小花費

答案:dp[n]

接著要想轉移式

例題2 frog1

轉移式!

跟上一題想法差不多

要找前兩個中最小

但是要考慮他們的高度

因為跳過來也有花費

dp[i]=min(從前一格跳過來,從前兩格跳過來)

例題2 frog1

轉移式!

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

初始狀態

(dp[1]=0;)

dp[2]=abs(h[2]-h[1]);

答案

cout<<dp[n]<<'\n';

例題2 frog2

最多走k格

這題的想法應該不難

就使原本的2個轉移變成k個

狀態一樣

來想轉移式

轉移式

今天可以從前k個跳過來

那我希望走的就是最小的路徑

取min 然後算跳過來的成本

for(int i=k+1;i<=n;i++){
  for(int j=1;j<=k;j++){
  	dp[i]=min(dp[i],dp[i-j]+abs(h[i]-h[i-j]));
  }
}

每次都取一次min

看從哪一格成本比較低

例題2 frog2

初始狀態!

因為我們會用到前k個

所以要先手算好1~k

那1~k都可以1次跳

dp[i]=從第1格跳過來的成本

for(int i=1;i<=k;i++)dp[i]=abs(h[i]-h[1]);

例題2 frog2

CODE

#include<bits/stdc++.h>
#define maxn 100005
using namespace std;
int dp[maxn],h[maxn];
int main(){
    fill(dp,dp+maxn,1e9);
    int n,k;cin>>n>>k;
    for(int i=1;i<=n;i++)cin>>h[i];
    for(int i=1;i<=k;i++)dp[i]=abs(h[i]-h[1]);
    for(int i=k+1;i<=n;i++){
        for(int j=1;j<=k;j++){
            dp[i]=min(dp[i],dp[i-j]+abs(h[i]-h[i-j]));
        }
    }
    cout<<dp[n]<<'\n';
}

由於每次都取min

如果預設為0

全部都會是0

例題2 frog2

題目

p-6-6

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n,m;
    cin>>n>>m;
    int g[205][205],dp[205][205];
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>g[i][j];
        }
    }
    dp[1][1]=g[1][1];
    for(int i=1;i<=n;i++)dp[i][1]=dp[i-1][1]+g[i][1];
    for(int i=1;i<=m;i++)dp[1][i]=dp[1][i-1]+g[1][i];
    for(int i=2;i<=n;i++){
        for(int j=2;j<=m;j++){
            dp[i][j]=max(dp[i][j-1],dp[i-1][j])+g[i][j];
        }
    }
    cout<<dp[n][m];
}

p-6-2

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n;
    cin>>n;
    int s[200005],dp[200005];
    for(int i=1;i<=n;i++){
        cin>>s[i];
    }
    dp[1]=s[1];
    dp[2]=max(dp[1],s[2]);
    for(int i=3;i<=n;i++){
        dp[i]=max(dp[i-1],dp[i-2]+s[i]);
    }
    cout<<dp[n];
}

p-6-3

#include<bits/stdc++.h>
using namespace std;
#define int long long
int32_t main(){
    int n;
    cin>>n;
    int c[100005],dp[100005];
    for(int i=1;i<=n;i++){
        cin>>c[i];
    }
    dp[1]=c[1];
    dp[2]=c[2];
    dp[3]=c[3]+min(c[1],c[2]);
    for(int i=4;i<=n;i++){
        dp[i]=c[i]+min({dp[i-1],dp[i-2],dp[i-3]});
    }
    if(n!=1)cout<<min(dp[n],dp[n-1]);
    else cout<<c[1];
}

p-6-4

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n,t,dp[2][200005];
    cin>>n>>t;
    vector<pair<int,int>>v;
    v.push_back({0,0});
    for(int i=0;i<n;i++){
        int a,b;
        cin>>a>>b;
        v.push_back({a,b});
    }
    dp[0][1]=(abs(t-v[1].first));
    dp[1][1]=(abs(t-v[1].second));
    for(int i=2;i<=n;i++){
        dp[0][i]=min(dp[0][i-1]+abs(v[i-1].first-v[i].first),dp[1][i-1]+abs(v[i-1].second-v[i].first));
        dp[1][i]=min(dp[0][i-1]+abs(v[i-1].first-v[i].second),dp[1][i-1]+abs(v[i-1].second-v[i].second));
    }
    cout<<min(dp[0][n],dp[1][n])<<'\n';
}

LIS

LIS?

LIS

Longest increasing subsequence

最長遞增子序列

子序列:之前枚舉有講過

取跟不取的那題

要找數列中

最長且符合遞增的子序列

LIS

舉例:

1 3 5 2 9

3 9

遞增子序列

1 5 2 9

不是遞增子序列

要找出最長的(可能不只一個)

LIS

1 3 5 2 9

其中一種最長的:1 3 5 9 

接下來要用dp的想法來解

有兩種做法

O(n^2)\\ O(n\ log\ n)

LIS

設定狀態

dp[i]是以第i項作為結尾的最長子序列長度

答案:dp[1~n]中最長的

接著要來想轉移式

每次都掃過前面的看能不能接

只求長度

LIS

由於前面dp[j]存的都是最長的子序列長度

每次往前找到如果比現在位置的值小就接看看

看怎麼接長度最長

for(int i=2;i<=n;i++){
  for(int j=1;j<i;j++){
    if(arr[i]>arr[j]){
      dp[i]=max(dp[i],dp[j]+1);
    }
  }
}

j每次往前掃

遇到可以接的 看看接上去會不會比原本大

CODE

#include<bits/stdc++.h>
#define maxn 200005
using namespace std;
int arr[maxn],dp[maxn];
int main(){
    int n;cin>>n;
    for(int i=1;i<=n;i++)cin>>arr[i];
    fill(dp,dp+maxn,1);
    for(int i=2;i<=n;i++){
        for(int j=1;j<i;j++){
            if(arr[i]>arr[j]){
                dp[i]=max(dp[i],dp[j]+1);
            }
        }
    }
    int ans=1;
    for(int i=1;i<=n;i++)ans=max(ans,dp[i]);
    cout<<ans<<'\n';
}

LIS

複雜度:

O(n^2)

很遺憾

這個方式過不了這一題的

看一下測資

2e5->n log n

接著用一點特殊的算法

LIS

Robinson–Schensted–Knuth algorithm

用的是貪心想法

直接來看怎麼做

利用binary search加速

如果聽不懂可以看這個

LIS

今天我們要來做一個LIS

如果a比陣列所有值大

那我們就可以放在最後

長度+1

要放入一個數字a

否則找一個它應該在的位置

將原有值替換掉

LIS

要放入一個數字2

ex

1 3 5 2 9

1 3 5

假如目前LIS做到第3

它沒比5大

找到適合它的位置

->把3換掉

LIS:1 2 5

LIS

要放入一個數字9

ex

1 3 5 2 9

1 2 5

接著LIS做到第4

它比5大

推到最後

LIS:1 2 5 9

長度=4

LIS

由於是遞增序列

是不是就有單調性

所以在找要怎麼替換的時候

可以快速找到

二分搜!

問題瞬間變簡單很多

LIS

CODE

#include<bits/stdc++.h>
#define maxn 200005
using namespace std;
int arr[maxn],dp[maxn];
vector<int>v;
int main(){
    int n;cin>>n;
    for(int i=1;i<=n;i++){
        cin>>arr[i];
    }
    for(int i=1;i<=n;i++){
        if(lower_bound(v.begin(),v.end(),arr[i])==v.end())v.push_back(arr[i]);
        else{
            auto it=lower_bound(v.begin(),v.end(),arr[i]);
            *it=arr[i];
        }
    }
    cout<<v.size();
}

利用二分搜

要注意 題目是嚴格遞增

複雜度:O(n log n)

LIS

最後vector裡面的並不是真正的LIS

要利用pos[]陣列來存

再用特殊的找法

詳情上面的連結有

題目

LCS

LCS

求最長共同子序列長度

並找一種輸出

ex

s1 : 5 7 9 3 1 2

s2 : 3 5 3 2 8

LCS=5 3 2

先來講長度

LCS

狀態

dp[i][j]=s1前i個跟s2前j個的LCS長度

答案=dp[n][m]

接著要來想轉移式

LCS

考慮兩種情況

1. s1[i]==s2[j]

2. s1[i]!=s2[j]

LCS長度是上面跟左邊格的mx

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

則LCS長度+1

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

LCS

長度求完了 要怎麼找子序列

要來找怎麼轉移過來的

如果dp[i][j]是由dp[i-1][j-1]轉移過來的

代表s[i]==s[j]

用反推的

然後記錄怎麼來的

LCS

用一個pre陣列存起來

0 1 2分別是三種轉過來的路徑左上 左 上

最後用遞迴走回去並輸出

直接看code

CODE

#include<bits/stdc++.h>
#define maxn 1005
using namespace std;
int dp[maxn][maxn],pre[maxn][maxn];
vector<int>s1,s2;
void print(int i,int j){
    if(i==0 || j==0)return;
    if(pre[i][j]==1){
        print(i-1,j-1);
        cout<<s1[i]<<' ';
    }
    else if(!pre[i][j]){
        print(i,j-1);
    }
    else print(i-1,j);
    return;
}
int main(){
    int n,m;cin>>n>>m;
    s1.push_back(0);
    s2.push_back(0);
    for(int i=0;i<n;i++){
        int a;cin>>a;
        s1.push_back(a);
    }
    for(int i=0;i<m;i++){
        int a;cin>>a;
        s2.push_back(a);
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(s1[i]==s2[j]){
                    dp[i][j]=dp[i-1][j-1]+1;
                    pre[i][j]=1;
            }
            else{
                if(dp[i][j-1]>dp[i-1][j]){
                    dp[i][j]=dp[i][j-1];
                    pre[i][j]=0;
                }
                else{
                    dp[i][j]=dp[i-1][j];
                    pre[i][j]=2;
                }
            }
        }
    }
    cout<<dp[n][m]<<'\n';
    print(n,m);
}

Dynamic Programming

By wuchanghualeo

Dynamic Programming

  • 43