Basic
Dynamic Programming

日月卦長

先備知識

  • High Dimensional Array
  • Recursion
  • Bitwise operator
  • Math

Fibonacci number

\(F_n = F_{n-1}+F_{n-2}\)
\(F_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

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

重覆子問題

$$F_n$$

$$F_{n-1}$$

$$F_{n-2}$$

$$F_{n-2}$$

$$F_{n-3}$$

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

最佳子結構

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

\(F_0=0\\F_1=1\)

\(F_n = F_{n-1}+F_{n-2}\)

時間複雜度計算

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

\(F_0=0\\F_1=1\)

\(F_n = F_{n-1}+F_{n-2}\)

  • 狀態數量: \(n\)
  • 狀態轉移時間: \(O(1)\)
  • 總時間: \(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. 寫成程式

背包問題

經典題

背包問題

  • 一個背包能裝載重量 \(T\)
  • 有 \(n\) 個物品重量分別為 \(c_1,c_2,\dots,c_n\)
  • 價值分別為 \(w_1,w_2,\dots,w_n\)
  • 至多能裝入總價值多少的東西?

定義狀態

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

狀態轉移

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

 

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

複雜度

  • 狀態數: \(n\times T\)
  • 狀態轉移時間: \(O(1)\)
  • 總共時間: \(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

  • 631