Basic
Dynamic Programming

日月卦長

先備知識

  • High Dimensional Array
  • Recursion
  • Bitwise operator
  • Math

Fibonacci number

Fn=Fn1+Fn2F_n = F_{n-1}+F_{n-2}
F0=0F1=1F_0=0\\F_1=1

迴圈版

遞迴版

long long F[100+5];
// ...
F[0] = 0;
F[1] = 1;
for(int i=2; i<=100; ++i){
    F[i] = F[i-1] + F[i-2];
}
// ...
cout << F[100] << '\n';
long long F(int i){
    if(i == 0) return 0;
    if(i == 1) return 1;
    return F(i-1) + F(i-2);
}
// ...
cout << F(100) << '\n';

遞迴實驗

#include <bits/stdc++.h>
using namespace std;
long long F(int i){
    if(i==0 || i==1) return i;
    return F(i-1) + F(i-2);
}
int main(){
    for(int i = 0; i < 50; ++i){
        cout << i << ": " << F(i) << '\n';
    }
    return 0;
}

遞迴實驗2

#include <bits/stdc++.h>
using namespace std;
long long DP[1000];
long long F(int i){
    if(i==0 || i==1) return i;
    if(DP[i]!=0) return DP[i];
    return DP[i] = F(i-1) + F(i-2);
}
int main(){
    for(int i = 0; i < 50; ++i){
        cout << i << ": " << F(i) << '\n';
    }
    return 0;
}

遞迴實驗2

#include <bits/stdc++.h>
using namespace std;
long long DP[1000];
long long F(int i){
    if(i==0 || i==1) return i;
    if(DP[i]!=0) return DP[i];
    return DP[i] = F(i-1) + F(i-2);
}
int main(){
    for(int i = 0; i < 50; ++i){
        cout << i << ": " << F(i) << '\n';
    }
    return 0;
}

動態規劃充要條件

  • 重覆子問題 Overlapping subproblems
  • 最佳子結構 Optimal substructure

其實就是滿足一些條件的遞迴公式

重覆子問題

FnF_n

Fn1F_{n-1}

Fn2F_{n-2}

Fn2F_{n-2}

Fn3F_{n-3}

  • 該問題在計算時會出現很多重覆子問題
  • 我們稱每個子問題為:狀態

最佳子結構

  • 該問題的最佳解可以從子問題的最佳解中求得
  • 可以寫成公式
  • 稱為:狀態轉移式

F0=0F1=1F_0=0\\F_1=1

Fn=Fn1+Fn2F_n = F_{n-1}+F_{n-2}

時間複雜度計算

  • 狀態數量 * 狀態轉移時間

F0=0F1=1F_0=0\\F_1=1

Fn=Fn1+Fn2F_n = F_{n-1}+F_{n-2}

  • 狀態數量: nn
  • 狀態轉移時間: O(1)O(1)
  • 總時間: n×O(1)=O(n)n\times O(1)=O(n)

Bottom Up

Top Down

long long F[100+5];
// ...
F[0] = 0;
F[1] = 1;
for(int i=2; i<=100; ++i){
  F[i] = F[i-1] + F[i-2];
}
// ...
cout << F[100] << '\n';
long long DP[100+5];
long long F(int i){
  if(i == 0) return 0;
  if(i == 1) return 1;
  if(DP[i]!=0) return DP[i];
  return DP[i] = F(i-1) + F(i-2);
}
// ...
cout << F(100) << '\n';

程式寫法

Bottom Up

  • 只需要迴圈
  • 速度快
  • 清楚知道狀態轉移順序
  • 可以做的優化較多

Top Down

  • 遞迴
  • 稍慢
  • 想法簡單

動態規劃題思考步驟

  1. 覺得要用動態規劃
  2. 定義狀態
  3. 根據狀態想出狀態轉移式
  4. 檢查時間複雜度
  5. 寫成程式

背包問題

經典題

背包問題

  • 一個背包能裝載重量 TT
  • nn 個物品重量分別為 c1,c2,,cnc_1,c_2,\dots,c_n
  • 價值分別為 w1,w2,,wnw_1,w_2,\dots,w_n
  • 至多能裝入總價值多少的東西?

定義狀態

  • d(i,v)d(i,v)表示:
    • 編號1i1 \sim i的物品
    • 選一些放入大小為vv的背包中
    • 最多可以得到的價值

狀態轉移

  • d(i,v)=0d(i,v)=0     if i=0i=0 or v=0v = 0
  • d(i,v)=d(i,v)=-\infty     if v<0v<0
  • d(i,v)=max(d(i1,v),d(i1,vci)+wi)d(i,v)=max(d(i-1,v), d(i-1,v-c_i)+w_i)
    if i>1i>1 and v>0v > 0

 

  • d(n,T)d(n,T) 就是我們要的答案!

複雜度

  • 狀態數: n×Tn\times T
  • 狀態轉移時間: O(1)O(1)
  • 總共時間: n×T×O(1)=O(n×T)n\times T\times O(1)=O(n\times T)

Top Down

int dp[MAXN][MAXT] = {};
bool vis[MAXN][MAXT] = {};

int d(int i, int v){
    if(i==0||v==0) return 0;
    if(v<0) return -9999999;
    if(vis[i][v]) return dp[i][v];
    vis[i][v] = true;
    return dp[i][v] = 
           max(d(i-1,v), d(i-1,v-c[i])+w[i]);
}

Bottom Up

int dp[MAXN][MAXT] = {};

for(int i=1; i<=n; ++i)
    for(int v=c[i]; v<=T; ++v)
        dp[i][v] = 
        max(dp[i-1][v], dp[i-1][v-c[i]]+w[i]);

滾動陣列

int dp[2][MAXT] = {};

for(int i=1; i<=n; ++i)
    for(int v=c[i]; v<=T; ++v)
        dp[i&1][v] = 
        max(dp[(i-1)&1][v], dp[(i-1)&1][v-c[i]]+w[i]);

int ans = dp[n&1][T];

狀態壓縮

int dp[MAXT] = {};

for(int i=1; i<=n; ++i)
    for(int v=T; v>=c[i]; --v)
        dp[v] = max(dp[v], dp[v-c[i]]+w[i]);

int ans = dp[T];

重要參考文獻

練習題

BasicDynamicProgramming

By jacky860226

BasicDynamicProgramming

  • 589