# Dynamic Programming by Colin --- ## Dynamic Programming (DP) 動態規劃 - DP = 分治 + 記憶化 - 分治:將⼤問題切成很多⼩問題,解決所有⼩問題也就解決⼤問題 - 記憶化:將算過的⼩問題答案記錄下來,再遇到就不⽤重新算(以空間換取時間) - dp就是⼀些用來降低解決問題時間複雜度的策略 - 通常需要通靈+大量練習 ---- ## DP解題步驟 1. 定義狀態:決定dp陣列中要儲存什麼東西 2. 想轉移式:想出遞迴式 3. 邊界條件:處理遞迴的起始與終止條件 4. 如何優化:如果會TLE就要思考如何優化時間複雜度 ---- - DP問題程式碼通常都很短,難的部分都是如何定義狀態或是最後的優化 - 需要大量練習來熟悉通靈方法 --- ## Example 1: 爬樓梯問題/費⽒數列 有⼀座$n$階的樓梯,⼀次可以向上跨1階或2階,請問有幾種⾛法可以⾛完這座階梯?(數字可能很⼤,請將答案對$10^9+7$取餘) $1\le n\le 10^6$,時間限制:$1s$ ---- - 模擬⼀下: ⾛上第1階時,只能從地⾯向上跨1階; ⾛上第2階時,可以從第1階向上跨1階,或是從地⾯向上跨2階; ⾛上第3階時,可以從第2階向上跨1階,或是從第1階向上跨2階; … ⾛上第n階時,可以從第n-1階向上跨1階,或是從第n-2階向上跨2階 ---- - 遞迴關係式: $f(i)$表示走到第$i$階的方法數 $f(i)=\begin{cases} 1 & \text {,} if\ i=1 \\ 2 & \text {,} if\ i=2 \\ f(i-1)+f(i-2) & \text {,} if\ i\ge 3 \end{cases}$ - 其實就是二階費式數列:前兩項之和等於第三項 - 可以寫一個簡單的遞迴程式 ---- code ```cpp= //直接遞迴 //O(2^n) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int p=1e9+7; int f(int i){ if(i==1||i==2) return i; return (f(i-1)+f(i-2))%p; } signed main(){ int n; cin>>n; cout<<f(n)<<'\n'; return 0; } ``` ---- - 但是按照這個題目給的條件:$1\le n\le 10^6$,時間限制:$1s$ - 這份code在$n$很大時一定會TLE - 為啥? ---- ![fibonacci2](https://hackmd.io/_uploads/HkqVw_VZ0.png) ---- 由遞迴樹可以發現: - 有許多已經知道答案的遞迴狀態被重複執行,十分浪費時間 - 剛才的程式碼時間複雜度其實是$O(\varphi ^n)$ - $\varphi =\frac{\sqrt{5}+1}{2}\approx 1.618$,為黃金分割率 ---- 如何改善: - 可以把已經算過的遞迴狀態儲存起來,再遇到時就不用再算一次 ---- 1.定義狀態 - 定義$dp[n+1]$ - $dp[i]$表示走到第$i$階的方法數 ---- 2.想轉移式 - $dp[i]=dp[i-1]+dp[i-2],i\ge 3$ ---- 3.邊界條件 - $dp[1]=1$ - $dp[2]=2$ - 最終答案是$dp[n]$ ---- 4.如何優化 - 有$n$個狀態 - 每次轉移只會用到前1項與前2項,故每次轉移是$O(1)$ - 總時間複雜度是$O(n)$,$1\le n\le 10^6$,可以在$1s$內完成 - 故不須優化 ---- code ```cpp= //dp(top down) //O(n) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int p=1e9+7; vector<int> dp(1000005,-1); int f(int i){ if(dp[i]!=-1) return dp[i]; if(i==1||i==2) dp[i]=i; else dp[i]=(f(i-1)+f(i-2))%p; return dp[i]; } signed main(){ int n; cin>>n; cout<<f(n)<<'\n'; return 0; } ``` ---- code ```cpp= //dp(bottom up) //O(n) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int p=1e9+7; vector<int> dp(1000005,-1); signed main(){ int n; cin>>n; dp[1]=1; dp[2]=2; for(int i=3;i<=n;i++) dp[i]=(dp[i-1]+dp[i-2])%p; cout<<dp[n]<<'\n'; return 0; } ``` ---- - dp可以分成top down與bottom up - top down是以遞迴的方式呈現,通常較為直覺(是遞迴轉移式的直接呈現),但可能會有遞迴過深(stack overflow)的問題(基本上很少遇到) - bottom up是以迴圈的方式呈現,不會有遞迴過深的問題,但比較不直覺 ---- 練習題: [CSDC310 費氏數列(DP)](https://csdc.tw/problem/310/) ---- 補充: 費式數列可以用矩陣快速冪優化到$O(logn)$ --- ## Example 2: 神偷大盜 [CSDC256 神偷大盜](https://csdc.tw/problem/256/) 有⼀位⼤盜想在⼀條有$n$間房⼦的街道上偷東⻄,會給你⼀串正整數$a[i]$分別代表第$1$~$n$間房⼦的財產總額,不能連續偷兩間相鄰的房⼦,請問這位⼤盜最多能偷到多少錢? $1\le n\le 10^6$,時間限制:$1s$ ---- 1.定義狀態 - 定義$dp[n+1]$ - $dp[i]$表示考慮$1$~$i$,偷第$i$間房子可以獲得的最大值 ---- 2.想轉移式 - $dp[i]=max(dp[j])+a[i],1\le j\le i-2$ ---- 3.邊界條件 - $dp[1]=a[1]$ - $dp[2]=a[2]$ ---- 4.如何優化 - 有$n$個狀態 - 每次轉移會用到前$i-2$項,故每次轉移是$O(n)$ - 總時間複雜度是$O(n^2)$,$1\le n\le 10^6$,無法在$1s$內完成 - 故需要優化 ---- - 觀察轉移式 - $dp[i]=max(dp[j])+a[i],1\le j\le i-2$ - 每次轉移真的需要從$dp[1]$考慮到$dp[i-2]$嗎 ---- - 題⽬有說每間房⼦的財產總額必為正整數 - $dp[3]=dp[1]+a[3]$,可得$dp[3]$必大於$dp[1]$ - $dp[4]=max(dp[1],dp[2])+a[4]$,可得$dp[4]$必大於$dp[1]$和$dp[2]$ - $dp[5]=max(dp[1],dp[2],dp[3])+a[5]$,由上述可知轉移式根本不需要考慮$dp[1]$ - $dp[6]=max(dp[1],dp[2],dp[3],dp[4])+a[6]$,由上述可知轉移式根本不需要考慮$dp[1]$和$dp[2]$ ---- - 依此類推,轉移式可以改成: - $dp[i]=max(dp[i-2],dp[i-3])+a[i]$ - 有$n$個狀態 - 每次轉移只會用到前2項與前3項,故每次轉移是$O(1)$ - 總時間複雜度是$O(n)$,$1\le n\le 10^6$,可以在$1s$內完成 ---- - 邊界條件: - $dp[1]=a[1]$ - $dp[2]=a[2]$ - $dp[3]=dp[1]+a[3]$ - 最終答案是$max(dp[n],dp[n-1])$ ---- code ```cpp= //dp(top down) //O(n) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,a[1000005]; vector<int> dp(1000005,-1); int f(int i){ if(dp[i]!=-1) return dp[i]; if(i==1||i==2) dp[i]=a[i]; else if(i==3) dp[i]=f(1)+a[3]; else dp[i]=max(f(i-2),f(i-3))+a[i]; return dp[i]; } signed main(){ cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; cout<<max(f(n),f(n-1))<<'\n'; return 0; } ``` ---- code ```cpp= //dp(bottom up) //O(n) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,a[1000005]; vector<int> dp(1000005,-1); signed main(){ cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; dp[1]=a[1]; dp[2]=a[2]; dp[3]=dp[1]+a[3]; for(int i=4;i<=n;i++) dp[i]=max(dp[i-2],dp[i-3])+a[i]; cout<<max(dp[n],dp[n-1])<<'\n'; return 0; } ``` --- ## Example 3: 棋盤捷徑問題 [AtCoder DP Contest H - Grid 1](https://atcoder.jp/contests/dp/tasks/dp_h) [CSES Grid Paths](https://cses.fi/problemset/task/1638) 輸入兩正整數$n$,$m$,以及⼀個棋盤⽅格$a[n][m]$,若$a[i][j]$是'.'表示可以通⾏,若$a[i][j]$是'\*'表示是障礙物,請問從棋盤⽅格的左上⾓⾛到右下⾓,只能向右或向下,有幾種⾛法?(數字可能很⼤,請將答案對$10^9+7$取餘) $2\le n,m\le 10^3$,時間限制:$1s$ ---- 1.定義狀態 - 定義$dp[n+1][m+1]$ - $dp[i][j]$表示走到$a[i][j]$的方法數 ---- 2.想轉移式 - $dp[i][j]=\begin{cases} 0 & \text {,} if\ a[i][j]='*' \\ dp[i-1][j]+dp[i][j-1] & \text {,} if\ a[i][j]='.' \\ \end{cases}$ ---- 3.邊界條件 - $dp[1][1]=\begin{cases} 0 & \text {,} if\ a[i][j]='*' \\ 1 & \text {,} if\ a[i][j]='.' \\ \end{cases}$ - $dp[1][j]=\begin{cases} 0 & \text {,} if\ a[1][j]='*' \\ dp[1][j-1] & \text {,} if\ a[1][j]='.' \\ \end{cases}$ - $dp[i][1]=\begin{cases} 0 & \text {,} if\ a[i][1]='*' \\ dp[i-1][1] & \text {,} if\ a[i][1]='.' \\ \end{cases}$ - 最終答案是$dp[n][m]$ ---- 4.如何優化 - 有$nm$個狀態 - 每次轉移是$O(1)$ - 總時間複雜度是$O(nm)$ - 不須優化 ---- code ```cpp= //dp(bottom up) //O(hw) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; const int p=1e9+7; int h,w,dp[1005][1005]; char arr[1005][1005]; signed main(){ //horaceorz; cin>>h>>w; for(int i=1;i<=h;i++) for(int j=1;j<=w;j++) cin>>arr[i][j]; dp[1][1]=1; for(int i=2;i<=h;i++) dp[i][1]=(arr[i][1]=='.'?dp[i-1][1]:0); for(int i=2;i<=w;i++) dp[1][i]=(arr[1][i]=='.'?dp[1][i-1]:0); for(int i=2;i<=h;i++){ for(int j=2;j<=w;j++){ dp[i][j]=(arr[i][j]=='.'?(dp[i-1][j]+dp[i][j-1])%p:0); } } cout<<dp[h][w]<<'\n'; return 0; } ``` --- ## Example 4: 假期計畫 [AtCoder DP Contest C - Vacation](https://atcoder.jp/contests/dp/tasks/dp_c) 有⼀個$n$天的假期,每天只能做A,B,C三項活動中的其中⼀種,但不可以連續兩天做相同的活動。已知第$i$天做這三項活動可以獲得的滿⾜感分別是$a[i]$,$b[i]$,$c[i]$,請問假期結束時可以獲得的最⼤滿⾜感是多少? $1\le n\le 10^5$,時間限制:$1s$ ---- - 窮舉時間複雜度是$O(3^n)$,一定會TLE - 所以要用DP處理 ---- 1.定義狀態 - 定義$dp[n+1][3]$ - $dp[i][0]$表示考慮$1$~$i$,第$i$天做A活動可以獲得的最⼤滿⾜感 - $dp[i][1]$表示考慮$1$~$i$,第$i$天做B活動可以獲得的最⼤滿⾜感 - $dp[i][2]$表示考慮$1$~$i$,第$i$天做C活動可以獲得的最⼤滿⾜感 ---- 2.想轉移式 - $dp[i][0]=max(dp[i-1][1],dp[i-1][2])+a[i]$ - $dp[i][1]=max(dp[i-1][0],dp[i-1][2])+b[i]$ - $dp[i][2]=max(dp[i-1][0],dp[i-1][1])+c[i]$ ---- 3.邊界條件 - $dp[1][0]=a[1]$ - $dp[1][1]=b[1]$ - $dp[1][2]=c[1]$ - 最終答案是$max(dp[n][0],dp[n][1],dp[n][2])$ ---- 4.如何優化 - 有$3n$個狀態 - 每次轉移是$O(1)$ - 總時間複雜度是$O(n)$ - 不須優化 ---- code ```cpp= //dp(top down) //O(n) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,a[100005],b[100005],c[100005]; vector<vector<int>> dp(100005,vector<int>(3,-1)); int f(int i,int j){ if(dp[i][j]!=-1) return dp[i][j]; if(i==1){ if(j==0) dp[i][j]=a[1]; else if(j==1) dp[i][j]=b[1]; else dp[i][j]=c[1]; }else{ if(j==0) dp[i][j]=max(f(i-1,1),f(i-1,2))+a[i]; else if(j==1) dp[i][j]=max(f(i-1,0),f(i-1,2))+b[i]; else dp[i][j]=max(f(i-1,0),f(i-1,1))+c[i]; } return dp[i][j]; } signed main(){ //horaceorz; cin>>n; for(int i=1;i<=n;i++) cin>>a[i]>>b[i]>>c[i]; cout<<max(max(f(n,0),f(n,1)),f(n,2))<<'\n'; return 0; } ``` ---- code ```cpp= //dp(bottom up) //O(n) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,a[100005],b[100005],c[100005]; vector<vector<int>> dp(100005,vector<int>(3,-1)); signed main(){ //horaceorz; 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]=max(dp[i-1][1],dp[i-1][2])+a[i]; dp[i][1]=max(dp[i-1][0],dp[i-1][2])+b[i]; dp[i][2]=max(dp[i-1][0],dp[i-1][1])+c[i]; } cout<<max(max(dp[n][0],dp[n][1]),dp[n][2])<<'\n'; return 0; } ``` --- ## Example 5: 0/1背包問題 [AtCoder DP Contest D - Knapsack 1](https://atcoder.jp/contests/dp/tasks/dp_d) 有⼀個可以耐重$W$的背包,及$N$種物品,每種物品有各⾃的重量$w[i]$、價值 $v[i]$,**且都只有⼀個**,求在不超過重量限制的情況下往背包塞盡量多的東⻄,總價值最⼤為多少? $1\le N\le 100$,$1\le W\le 10^5$,時間限制:$1s$ ---- 1.定義狀態 - 定義$dp[N+1][W+1]$ - $dp[i][j]$表示考慮到第$i$種物品且剩下空間為$j$時所能放入的最⼤價值 ---- 2.想轉移式 - $dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])$,$j\ge w[i]$ - $dp[i][j]=dp[i-1][j]$,$j<w[i]$ ---- 3.邊界條件 - $dp[1][0]=0$ - 最終答案是$dp[N][W]$ ---- 4.如何優化 - 有$NW$個狀態 - 每次轉移是$O(1)$ - 總時間複雜度是$O(NW)$ - 不須優化 ---- code ```cpp= //0/1背包問題 //dp(bottom up) //時間複雜度:O(NW) //空間複雜度:O(NW) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int N,W,w[105],v[105]; vector<vector<int>> dp(105,vector<int>(100005,0)); signed main(){ //horaceorz; cin>>N>>W; for(int i=1;i<=N;i++) cin>>w[i]>>v[i]; for(int i=1;i<=N;i++){ for(int j=1;j<=W;j++){ if(j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]); else dp[i][j]=dp[i-1][j]; } } cout<<dp[N][W]<<'\n'; return 0; } ``` ---- code ```cpp= //0/1背包問題 //dp(bottom up)+滾動數組優化 //時間複雜度:O(NW) //空間複雜度:O(W) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int N,W,w[105],v[105]; vector<int> dp(100005,0); signed main(){ //horaceorz; cin>>N>>W; for(int i=1;i<=N;i++) cin>>w[i]>>v[i]; for(int i=1;i<=N;i++){ for(int j=W;j>=w[i];j--){ dp[j]=max(dp[j],dp[j-w[i]]+v[i]); } } cout<<dp[W]<<'\n'; return 0; } ``` ---- 延伸題 [CSES Minimizing Coins 換零錢問題](https://cses.fi/problemset/task/1634/) ---- AC code ```cpp= //dp(top down) //O(nx) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,x,a[105],dp[1000005]; int f(int x){ if(dp[x]==INT_MAX){ if(x==0){ dp[x]=0; }else{ int mini=INT_MAX; for(int i=0;i<n;i++){ if(x>=a[i]) mini=min(mini,f(x-a[i])); } dp[x]=mini+1; } } return dp[x]; } signed main(){ //horaceorz; cin>>n>>x; for(int i=0;i<=x;i++) dp[i]=INT_MAX; for(int i=0;i<n;i++) cin>>a[i]; int ans=f(x); if(dp[0]!=0) cout<<-1; else cout<<ans; return 0; } ``` ---- AC code ```cpp= //dp(bottom up) //O(nx) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,x,a[105],dp[1000005]; signed main(){ //horaceorz; cin>>n>>x; for(int i=0;i<=x;i++) dp[i]=INT_MAX; for(int i=0;i<n;i++) cin>>a[i]; dp[0]=0; for(int i=0;i<n;i++){ for(int j=a[i];j<=x;j++){ dp[j]=min(dp[j],dp[j-a[i]]+1); } } if(dp[x]!=INT_MAX) cout<<dp[x]; else cout<<-1; return 0; } ``` --- ## Example 6: 無限背包問題 [AtCoder DP Contest D - Knapsack 1](https://atcoder.jp/contests/dp/tasks/dp_d) 有⼀個可以耐重$W$的背包,及$N$種物品,每種物品有各⾃的重量$w[i]$、價值 $v[i]$,**且都有無限個**,求在不超過重量限制的情況下往背包塞盡量多的東⻄,總價值最⼤為多少? $1\le N\le 100$,$1\le W\le 10^5$,時間限制:$1s$ ---- 1.定義狀態 - 定義$dp[N+1][W+1]$ - $dp[i][j]$表示考慮到第$i$種物品且剩下空間為$j$時所能放入的最⼤價值 ---- 2.想轉移式 - $dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i])$,$j\ge w[i]$ - $dp[i][j]=dp[i-1][j]$,$j<w[i]$ ---- 3.邊界條件 - $dp[1][0]=0$ - 最終答案是$dp[N][W]$ ---- 4.如何優化 - 有$NW$個狀態 - 每次轉移是$O(1)$ - 總時間複雜度是$O(NW)$ - 不須優化 ---- code ```cpp= //無限背包問題 //dp(bottom up) //時間複雜度:O(NW) //空間複雜度:O(NW) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int N,W,w[105],v[105]; vector<vector<int>> dp(105,vector<int>(100005,0)); signed main(){ //horaceorz; cin>>N>>W; for(int i=1;i<=N;i++) cin>>w[i]>>v[i]; for(int i=1;i<=N;i++){ for(int j=1;j<=W;j++){ if(j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]); else dp[i][j]=dp[i-1][j]; } } cout<<dp[N][W]<<'\n'; return 0; } ``` ---- code ```cpp= //無限背包問題 //dp(bottom up)+滾動數組優化 //時間複雜度:O(NW) //空間複雜度:O(W) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int N,W,w[105],v[105]; vector<int> dp(100005,0); signed main(){ //horaceorz; cin>>N>>W; for(int i=1;i<=N;i++) cin>>w[i]>>v[i]; for(int i=1;i<=N;i++){ for(int j=w[i];j<=W;j++){ dp[j]=max(dp[j],dp[j-w[i]]+v[i]); } } cout<<dp[W]<<'\n'; return 0; } ``` --- ## Example 7: LIS [CSES Increasing Subsequence](https://cses.fi/problemset/task/1145) LIS是最⻑遞增⼦序列(Longest Increasing Subsequence),給定⼀個⻑度為$n$的序列$a[n]$,請輸出這個序列的最⻑遞增⼦序列(嚴格遞增)的⻑度。 $1\le n\le 10^5$,時間限制:$1s$ ---- - ⼦序列:把原本的序列刪掉其中幾項,並把剩下的項依照原本的順序排好。 (例如:ace、bcde、b、空集合、abcde 等都是 abcde 的⼦序列) - 嚴格遞增⼦序列:每⼀項都⼤於前⼀項的⼦序列 - 不嚴格遞增⼦序列:每⼀項都⼤於等於前⼀項的⼦序列 (例如:1,2,3,4,5 是 1,5,2,9,2,3,4,5,1,5 的嚴格遞增⼦序列; 1,2,2,3,4,5,5 是 1,5,2,9,2,3,4,5,6,5 的不嚴格遞增⼦序列) ---- 1.定義狀態 - 定義$dp[n+1]$ - $dp[i]$表示前$i$項的LIS長度 ---- 2.想轉移式 - $dp[i]=max(dp[j])+1$,$j<i$且$a[j]<a[i]$ ---- 3.邊界條件 - $dp[1]=1$ - 最終答案是$dp[n]$ ---- 4.如何優化 - 有$n$個狀態 - 每次轉移是$O(n)$ - 總時間複雜度是$O(n^2)$,會TLE - 需要優化 ---- 重新定義狀態!!! - 定義$dp[n+1]$ - $dp[i]$儲存長度為$i$的LIS的第$i$項數字的最小值,預設為正無窮 ---- code ```cpp= //嚴格遞增LIS //O(nlogn) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,a[100005]; vector<int> dp(100005,1e18); signed main(){ //horaceorz; cin>>n; for(int i=0;i<n;i++) cin>>a[i]; for(int i=0;i<n;i++){ int j=lower_bound(dp.begin(),dp.end(),a[i])-dp.begin(); dp[j]=a[i]; } cout<<lower_bound(dp.begin(),dp.end(),1e18)-dp.begin()<<'\n'; return 0; } ``` ---- code ```cpp= //非嚴格遞增LIS //O(nlogn) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,a[100005]; vector<int> dp(100005,1e18); signed main(){ //horaceorz; cin>>n; for(int i=0;i<n;i++) cin>>a[i]; for(int i=0;i<n;i++){ int j=upper_bound(dp.begin(),dp.end(),a[i])-dp.begin(); dp[j]=a[i]; } cout<<lower_bound(dp.begin(),dp.end(),1e18)-dp.begin()<<'\n'; return 0; } ``` ---- 嚴格遞增=>lower_bound 不嚴格遞增=>upper_bound ---- 延伸題 [2021.01 APCS 4. 飛黃騰達](https://zerojudge.tw/ShowProblem?problemid=f608) ---- AC code ```cpp= #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; signed main(){ //horaceorz; int n; cin>>n; int dp[n]; pair<int,int> p[n]; for(int i=0;i<n;i++){ dp[i]=1e18; cin>>p[i].first>>p[i].second; } sort(p,p+n); for(int i=0;i<n;i++){ int j=upper_bound(dp,dp+n,p[i].second)-dp; dp[j]=p[i].second; } cout<<lower_bound(dp,dp+n,1e18)-dp; return 0; } ``` --- ## Example 8: LCS LCS是最⻑共同⼦序列(Longest Common Subsequence),給定兩個⻑度分別為$N$,$M$的字串A,B,請輸出這兩個字串最⻑共同⼦序列的⻑度。 $1\le N,M\le 1000$,時間限制:$1s$ ---- - 共同⼦序列:同時是兩個序列的⼦序列的序列。 (例如:ab、ac、abd、空集合、abc 等都是 abcde 和 abedc 的共同⼦序列) ---- 1.定義狀態 - 定義$dp[N+1][M+1]$ - $dp[i][j]$表示考慮A前$i$項與B前$j$項的LCS長度 ---- 2.想轉移式 - $dp[i][j]=dp[i-1][j-1]+1$,$A[i]=B[j]$ - $dp[i][j]=max(dp[i-1][j],dp[i][j-1])$,$A[i]\not =B[j]$ ---- 3.邊界條件 - $dp[i][0]=dp[0][j]=0$ - 最終答案是$dp[N][M]$ ---- 4.如何優化 - 有$NM$個狀態 - 每次轉移是$O(1)$ - 總時間複雜度是$O(NM)$ - 不須優化 ---- code ```cpp= //dp(top down) //O(NM) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int N,M; string A,B; vector<vector<int>> dp(1005,vector<int>(1005,-1)); int f(int i,int j){ if(dp[i][j]!=-1) return dp[i][j]; if(i==0||j==0) dp[i][j]=0; else if(A[i-1]==B[j-1]) dp[i][j]=f(i-1,j-1)+1; else dp[i][j]=max(f(i-1,j),f(i,j-1)); return dp[i][j]; } signed main(){ cin>>N>>M>>A>>B; cout<<f(N,M)<<'\n'; return 0; } ``` ---- 延伸題 [CSES Edit Distance 編輯距離](https://cses.fi/problemset/task/1639) ---- AC code ```cpp= //dp(top down) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; string a,b; int dp[5005][5005]; int f(int i,int j){ if(dp[i][j]!=INT_MAX) return dp[i][j]; if(i==0&&j==0) dp[i][j]=0; else if(i==0) dp[i][j]=j; else if(j==0) dp[i][j]=i; else if(a[i-1]==b[j-1]) dp[i][j]=f(i-1,j-1); else dp[i][j]=min(min(f(i-1,j)+1,f(i,j-1)+1),f(i-1,j-1)+1); return dp[i][j]; } signed main(){ //horaceorz; cin>>a>>b; for(int i=0;i<=a.size();i++) for(int j=0;j<=b.size();j++) dp[i][j]=INT_MAX; cout<<f(a.size(),b.size()); return 0; } ``` --- ## Example 9: 石子合併 [AtCoder DP Contest N - Slimes](https://atcoder.jp/contests/dp/tasks/dp_n) 有$n$堆石子排成一排,已知每堆石子的重量。每次只能合併相鄰的兩堆石子,合併的花費為兩堆石子的總重,若要將全部的石子合併成一堆,求最少的總花費。 $2\le n\le 400$,時間限制:$1s$ ---- 1.定義狀態 - 定義$dp[n+1][n+1]$ - $dp[i][j]$表示合併第$i$堆到第$j$堆石子所需最少花費,$i\le j$ ---- 2.想轉移式 - $dp[i][j]=min(dp[i][k]+dp[k+1][j])+w(i,j)$ - $i\le k<j$ - $w(i,j)$表示第$i$堆到第$j$堆石子的總重,可以用前綴和預處理 ---- 3.邊界條件 - $dp[i][i]=0$ - 最終答案是$dp[1][n]$ ---- 4.如何優化 - 有$n^2$個狀態 - 每次轉移是$O(n)$ - 總時間複雜度是$O(n^3)$ - $2\le n\le 400$,時間限制:$1s$ - 不須優化 ---- code ```cpp= //dp(top down) //O(n^3) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,arr[1005],p[1005],dp[1005][1005]; int w(int i,int j){ return p[j]-p[i-1]; } int f(int i,int j){ if(i==j) return 0; if(dp[i][j]!=1e18) return dp[i][j]; for(int k=i;k<j;k++){ dp[i][j]=min(dp[i][j],f(i,k)+f(k+1,j)+w(i,j)); } return dp[i][j]; } signed main(){ //horaceorz; cin>>n; p[0]=0; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++) dp[i][j]=1e18; cin>>arr[i]; p[i]=p[i-1]+arr[i]; } cout<<f(1,n)<<'\n'; return 0; } ``` ---- code ```cpp= //dp(bottom up) //O(n^3) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,arr[1005],p[1005],dp[1005][1005]; int w(int i,int j){ return p[j]-p[i-1]; } signed main(){ //horaceorz; cin>>n; p[0]=0; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ if(i==j) dp[i][j]=0; else dp[i][j]=1e18; } cin>>arr[i]; p[i]=p[i-1]+arr[i]; } for(int len=2;len<=n;len++){ for(int i=1;i<=n-len+1;i++){ int j=i+len-1; for(int k=i;k<j;k++){ dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+w(i,j)); } } } cout<<dp[1][n]<<'\n'; return 0; } ``` ---- 補充: 石子合併可以用四邊形不等式優化到$O(n^2)$ ---- 延伸題 [2024.01 APCS 4. 合併成本](https://zerojudge.tw/ShowProblem?problemid=m934) ---- AC code ```cpp= //dp(top down) //O(n^3) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,arr[105],p[105],dp[105][105]; int w(int i,int j){ return p[j]-p[i-1]; } int f(int i,int j){ if(i==j) return 0; if(dp[i][j]!=1e18) return dp[i][j]; for(int k=i;k<j;k++){ dp[i][j]=min(dp[i][j],f(i,k)+f(k+1,j)+abs(w(i,k)-w(k+1,j))); } return dp[i][j]; } signed main(){ //horaceorz; cin>>n; p[0]=0; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++) dp[i][j]=1e18; cin>>arr[i]; p[i]=p[i-1]+arr[i]; } cout<<f(1,n)<<'\n'; return 0; } ``` ---- AC code ```cpp= //dp(bottom up) //O(n^3) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,arr[105],p[105],dp[105][105]; int w(int i,int j){ return p[j]-p[i-1]; } signed main(){ //horaceorz; cin>>n; p[0]=0; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ if(i==j) dp[i][j]=0; else dp[i][j]=1e18; } cin>>arr[i]; p[i]=p[i-1]+arr[i]; } for(int len=2;len<=n;len++){ for(int i=1;i<=n-len+1;i++){ int j=i+len-1; for(int k=i;k<j;k++){ dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+abs(w(i,k)-w(k+1,j))); } } } cout<<dp[1][n]<<'\n'; return 0; } ``` --- ## Example 10: 位元問題 [AtCoder DP Contest S - Digit Sum](https://atcoder.jp/contests/dp/tasks/dp_s) 求$1$~$K$內數字符合所有位元之和為$D$的倍數的數字數量。 $1\le \vert K\vert \le 10^4$,$1\le D\le 100$,時間限制:$1s$ ---- 1.定義狀態 - 定義$dp[\vert K\vert +1][2][D]$ - $dp[i][0][j]$表示考慮到第$i$位,第$1$~$i$位數字總和mod $D$為$j$的數量 - $dp[i][1][j]$表示考慮到第$i$位,第$1$~$i$位數字總和mod $D$為$j$且<原第$1$~$i$位數字的數量 ---- 2.想轉移式 - $dp[i][0][j]=\sum(dp[i-1][0][x])$,$(x+0$~$9)$ mod $D=j$ - $dp[i][1][j]=\sum(dp[i-1][0][x])+\sum(dp[i-1][1][y])$,$(x+0$~$K[i]-1)$ mod $D=j$,$(y+K[i])$ mod $D=j$ ---- 3.邊界條件 - $dp[1][0][(0$~$9) mod D]=1$ - $dp[1][1][(0$~$K[1]) mod D]=1$ - 最終答案是$dp[\vert K\vert][1][0]-1$ (要扣掉0) ---- 4.如何優化 - 有$\vert K\vert \times D$個狀態 - 每次轉移是$O(1)$ - 總時間複雜度是$O(\vert K\vert \times D)$ - 不須優化 ---- code ```cpp= //digit dp //O(|k|*d) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; const int prime=1e9+7; string k; int d,dp[10005][2][105]; signed main(){ //horaceorz; cin>>k>>d; int len=k.size(); for(int i=0;i<len/2;i++) swap(k[i],k[len-i-1]); for(int j=0;j<d;j++){ dp[1][0][j]=0; dp[1][1][j]=0; } for(int m=0;m<=9;m++) dp[1][0][m%d]++; for(int m=0;m<=k[0]-'0';m++) dp[1][1][m%d]++; for(int i=1;i<=len;i++){ for(int j=0;j<d;j++){ for(int m=0;m<=9;m++){ dp[i+1][0][(m+j)%d]+=dp[i][0][j]; dp[i+1][0][(m+j)%d]%=prime; } for(int m=0;m<k[i]-'0';m++){ dp[i+1][1][(m+j)%d]+=dp[i][0][j]; dp[i+1][1][(m+j)%d]%=prime; } dp[i+1][1][(k[i]-'0'+j)%d]+=dp[i][1][j]; dp[i+1][1][(k[i]-'0'+j)%d]%=prime; } } cout<<(dp[len][1][0]-1+prime)%prime<<'\n'; return 0; } ``` --- 練習資源 [AtCoder DP Contest](https://atcoder.jp/contests/dp/tasks) [CSES](https://cses.fi/problemset/list/) [CSDCOJ dp](https://csdc.tw/problems/?tag=dp) [LeetCode dp](https://leetcode.com/problemset/?topicSlugs=dynamic-programming&page=1) --- Thank you for listening!
{"description":"title: Dymanic Programing","contributors":"[{\"id\":\"fd160699-cd06-45fd-abd0-c09edbc85980\",\"add\":24682,\"del\":1370}]","title":"Dynamic Programming"}
    187 views