## 動態規劃 ### 2/23 社課 --- ## 動態規劃 ---- 動態規劃(Dynamic Programming),簡稱DP,透過把原問題分解為相對簡單的子問題來求解,並且把已經算過的數字存起來,之後再碰到就可以直接用。 ---- ### 解題方法 * 問題拆解,找到問題之間的關聯 * 定義DP狀態 * 列出轉移式 * 初始化邊界條件 --- ## 費氏數列 ---- ### [Zerojudge - 東東爬階梯](https://zerojudge.tw/ShowProblem?problemid=d212) 給定一個整數$0<n<100$,假設階梯有$n$階,每次可以走一階或兩階,求有幾種走法 ---- * 走到第$i$階可以由第$i-1$階走$1$階,也可以由第$i-2$階走$2$階 * 令$dp[i]$為走$i$階的方法數 * $dp[i]=dp[i-1]+dp[i-2]$ * $dp[0]=dp[1]=1$ * $ans=dp[n]$ ---- 程式碼: ```cpp #include <bits/stdc++.h> #define int long long using namespace std; int dp[100]; signed main() { int i,n; dp[0]=dp[1]=1; for(i=2;i<100;i++) { dp[i]=dp[i-1]+dp[i-2]; } while(cin >> n) { cout << dp[n] << "\n"; } } ``` ---- ### [CSES - Dice Combinations](https://cses.fi/problemset/task/1633) 給定一個整數$1\le n\le 10^{6}$,可以骰六面骰子任意次數,求有多少種方法使得點數總和是$n$,順序交換算不同次 ---- * 總和為$i$可以由原本總和$i-1$骰到$1$,由原本總和$i-2$骰到$2$,依此類推 * 令$dp[i]$為總和$i$點的方法數 * $dp[i]=\sum_{k=1}^{6}dp[i-k],i-k\ge 0$ * $dp[0]=1$ * $ans=dp[n]$ ---- 程式碼: ```cpp #include <bits/stdc++.h> #define int long long #define MOD 1000000007 using namespace std; int dp[1000001]; signed main() { int n,i,j; cin >> n; dp[0]=1; for(i=1;i<=n;i++) { for(j=max(0ll,i-6);j<=i-1;j++) dp[i]=(dp[i]+dp[j])%MOD; } cout << dp[n] << "\n"; } ``` ---- ### [CSES - Dice Probability](https://cses.fi/problemset/task/1725) 給定整數$1\le n\le 100$,$1\le a,b\le 6n$,骰六面骰子$n$次,求點數總和介於$a$和$b$之間的機率 ---- * 總和為$i$可以由原本總和$i-1$骰到$1$,其中骰到1的機率為$\frac{1}{6}$,依此類推 * 令$dp[i][j]$為骰了$i$次,總和$j$點的機率 * $dp[i][j]=\frac{1}{6}\sum_{k=1}^{6}dp[i-1][j-k],j-k\ge 0$ * $dp[0][0]=1$ * $ans=\sum_{k=a}^{b}dp[n][k]$ ---- 程式碼: ```cpp #include <bits/stdc++.h> using namespace std; double dp[101][601]; int main() { int n,a,b,i,j,k; double ans=0; cin >> n >> a >> b; dp[0][0]=1; for(i=1;i<=n;i++) { for(j=0;j<=i*6;j++) { for(k=max(0,j-6);k<=j-1;k++) dp[i][j]+=dp[i-1][k]; dp[i][j]/=6; } } for(;a<=b;a++) { ans+=dp[n][a]; } cout << fixed << setprecision(6) << ans << "\n"; } ``` --- ## 前綴和 ---- ### [CSES - Static Range Sum Queries](https://cses.fi/problemset/task/1646) 給定一長度為$n$的陣列$x_1,x_2,\dots,x_n$,並有$q$次詢問,每次詢問求陣列$[a,b]$範圍內的和,$1\le n,q\le2\cdot 10^5$ * 一維前綴和 ---- * 令$dp[i]$為前$i$項的前綴和 * $dp[i]=dp[i-1]+x_i$ * $dp[0]=0$ * $ans=dp[b]-dp[a-1]$ ---- 程式碼: ```cpp #include <bits/stdc++.h> #define int long long using namespace std; int dp[200001]; signed main() { int n,q,x,i,a,b; cin >> n >> q; for(i=1;i<=n;i++) { cin >> x; dp[i]=dp[i-1]+x; } while(q--) { cin >> a >> b; cout << dp[b]-dp[a-1] << "\n"; } } ``` ---- ### [CSES - Forest Queries](https://cses.fi/problemset/task/1652) 給定一$n\times n$的二維陣列,每個位置是$0$或$1$,並有$q$次詢問,每次詢問求陣列$(y_1,x_1)\sim(y_2,x_2)$範圍內的和,$1\le n\le1000,1\le q\le2\cdot 10^5$ * 二維前綴和 ---- * 令$dp[i][j]$為$(1,1)\sim(i,j)$範圍內的和 * $dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+c_{i,j}$ * $dp[0][0]=dp[0][1]=\dots=dp[0][n]=0$ * $dp[0]dp[0]=dp[1][0]=\dots=dp[n][0]=0$ * $ans=dp[y_2][x_2]-dp[y2][x1-1]-dp[y1-1][x2]+dp[y1-1][x1-1]$ ---- 預處理: ![1](https://hackmd.io/_uploads/By0V3xShp.png) ---- 詢問: ![2](https://hackmd.io/_uploads/HyFS2lB26.png) ---- 程式碼 ```cpp #include <bits/stdc++.h> #define int long long using namespace std; int dp[1001][1001]; signed main() { int n,q,i,j,y1,x1,y2,x2; char c; cin >> n >> q; for(i=1;i<=n;i++) { for(j=1;j<=n;j++) { cin >> c; if(c=='*') dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+1; else dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]; } } while(q--) { cin >> y1 >> x1 >> y2 >> x2; cout << dp[y2][x2]-dp[y2][x1-1]-dp[y1-1][x2]+dp[y1-1][x1-1] << "\n"; } } ``` --- ## 最大子陣列 ---- ### [CSES - Maximum Subarray Sum](https://cses.fi/problemset/task/1643) 給定一個長度為$n$的陣列$x_1,x_2,\dots,x_n$,$1\le n\le 2\cdot 10^5$,求最大連續非空子陣列和 ---- * 要有至少一個元素 * 令$dp[i]$為前$i$項的最大連續非空子陣列和 * $dp[i]=max(dp[i-1]+x_i,x_i)$ * $dp[0]=0$ * $ans=max(dp[1],dp[2],\dots,dp[n])$ ---- 程式碼: ```cpp #include <bits/stdc++.h> #define int long long using namespace std; int dp[200001]; signed main() { int n,x,ans=LONG_LONG_MIN,i; cin >> n; for(i=0;i<n;i++) { cin >> x; dp[i]=max(dp[i-1]+x,x); ans=max(ans,dp[i]); } cout << ans << "\n"; } ``` ---- ### [ZeroJudge - 投資遊戲](https://zerojudge.tw/ShowProblem?problemid=m373) 給定一個長度為$n$的陣列$x_1,x_2,\dots,x_n$,$1\le n\le 150000$,求最大連續子陣列和,其中可以跳過$k$個數字,$k\le 20$ ---- * 可以不包含任何元素 * 令$dp[i][j]$為跳過$i$個數字,前$j$項的最大連續子陣列和 * $dp[0][0]=dp[1][0]=\dots=dp[k][0]=0$ * $dp[0][j]=max(dp[j-1]+x_j,0)$ * $dp[i][j]=max(dp[i-1][j-1],dp[i][j-1]+x_j)$ * $ans=max(dp[k][1],dp[k][2],\dots,dp[k][n])$ ---- 程式碼: ```cpp #include <bits/stdc++.h> #define int long long using namespace std; int x[150001],dp[21][150001]; signed main() { int n,k,i,j; cin >> n >> k; for(j=1;j<=n;j++) { cin >> x[j]; dp[0][j]=max(dp[0][j-1]+x[j],0ll); } for(i=1;i<=k;i++) { for(j=1;j<=n;j++) { dp[i][j]=max(dp[i-1][j-1],dp[i][j-1]+x[j]); } } cout << *max_element(dp[k]+1,dp[k]+n+1) << "\n"; } ``` --- ## 背包問題 ---- ### [AtCoder - Knapsack 1](https://atcoder.jp/contests/dp/tasks/dp_d) 有$N$種物品,每種物品有重量$w$與價值$v$ 每個物品最多可以選一次,求選的物品重量不超過$W$的情況,價值總和最大是多少 * $0/1$背包問題,代表選或不選 ---- * 令$dp[i][j]$為選前$i$個物品且重量$\le j$的最大價值 * $dp[i][j]=max(dp[i-1][j],dp[i-1][j-w_i]+v_i)$ * $dp[0][0]=dp[0][1]=\dots=dp[0][W]=0$ * $ans=dp[N][W]$ ---- 程式碼: ```cpp #include <bits/stdc++.h> #define int long long using namespace std; int dp[101][100001]; signed main() { int N,W,w,v,i,j; cin >> N >> W; for(i=1;i<=N;i++) { cin >> w >> v; for(j=0;j<=W;j++) { dp[i][j]=max(dp[i-1][j],j-w>=0?dp[i-1][j-w]+v:0); } } cout << dp[N][W] << "\n"; } ``` ---- ### [Luogu - 宝物筛选](https://www.luogu.com.cn/problem/P1776) 有$n$種物品,每種物品有重量$w$、價值$v$與數量$m$ 每個物品最多可以選$m$次,求選的物品重量不超過$W$的情況,價值總和最大是多少 * 有限背包問題,每種物品有數量限制 ---- * 把每一種物品拆成$m$個物品,用$0/1$背包解 * TLE ---- * 令$m=2^{p}-1+k$,其中$k$滿足$k\le2^{p}-1$,則物品可分成$2^{0},2^{1},\dots,2^{p-1},k$個 * 以上的數字可以湊出所有$\le m$的任何數字 * 每個數字為一堆,當成一個物品,用$0/1$背包解 ---- * 證明: * 1. 假設所需的數字$\le 2^{p}-1$,則這個數字可以表示為二進制的$p$位數字,考慮每一位可以是$0$或$1$,這個數字有$2^p$種排列方式,又數字最小值為每位都是$0$ $=0$,最大值為每位都是$1$ $=2^0\times\frac{2^p-1}{2-1}=2^p-1$,因此這$2^p$個數剛好對應到$0\sim2^p-1$ * 2. 假設所需的數$>2^{p}-1$,且最大值為$m$,這個數字$\le2^{p}-1+k$,由1.可知$2^{p}-1$內的數字都可以湊出來,因此$\ge k$且$\le2^{p}-1+k$的數字也可以湊出來,又$k\le2^{p}-1$,因此$> 2^{p}-1$且$\le2^{p}-1+k$的數字都可以被湊出來 ---- 程式碼: ```cpp #include <bits/stdc++.h> #define int long long using namespace std; int dp[40001]; signed main() { int n,W,v,w,m,i,j,tmp; cin >> n >> W; for(i=1;i<=n;i++) { cin >> v >> w >> m; for(tmp=1;tmp<=m;tmp*=2) { m-=tmp; for(j=W;j>=tmp*w;j--) { dp[j]=max(dp[j],dp[j-tmp*w]+tmp*v); } } if(m) { for(j=W;j>=m*w;j--) { dp[j]=max(dp[j],dp[j-m*w]+m*v); } } } cout << dp[W] << "\n"; } ``` ---- ### [Luogu - 疯狂的采药](https://www.luogu.com.cn/problem/P1616) 有$m$種草藥,每種草藥有採收需要時間$a$與價值$b$ 每種草藥可以無限制地採,求採收時間不超過$t$的情況,價值總和最大是多少 * 無限背包問題,物品沒有數量限制 ---- * 令$dp[i]$為總時間$\le i$的最大價值 * $dp[i]=max(dp[i],dp[i-a_j]+b_j)$ * $dp[0]=0$ * $ans=dp[t]$ * 因為在任何時間都可以選任何草藥,所以每種草藥可以取無限次 ---- 程式碼: ```cpp #include <bits/stdc++.h> #define int long long using namespace std; int a[10001],b[10001],dp[10000001]; signed main() { int t,m,i,j; cin >> t >> m; for(i=1;i<=m;i++) cin >> a[i] >> b[i]; for(i=1;i<=t;i++) { for(j=1;j<=m;j++) { if(a[j]<=i) dp[i]=max(dp[i],dp[i-a[j]]+b[j]); } } cout << dp[t] << "\n"; } ``` --- ## 硬幣問題 ---- ### [CSES - Minimizing Coins](https://cses.fi/problemset/task/1634) 給定$n$枚硬幣,價值$c_1,c_2,\dots,c_n$元,求最少要用幾個硬幣才能湊出$x$元,$1\le n\le 100,1\le x\le 10^6$ ---- * 令$dp[j]$為湊出$j$元所需的最少硬幣數 * $dp[j]=min(dp[j],dp[j-c_i]+1)$ * $dp[0]=0$ * 先假設$dp[1]=dp[2]=\dots=dp[x]=INF$ * $ans=dp[x],dp[x]\not =INF$ * $ans$無解$,dp[x]=INF$ ---- 程式碼: ```cpp #include <bits/stdc++.h> #define INF 0x3F3F3F3F using namespace std; int dp[1000001]; int main() { memset(dp,0x3F,sizeof(dp)); dp[0]=0; int n,x,c,i,j; cin >> n >> x; for(i=1;i<=n;i++) { cin >> c; for(j=c;j<=x;j++) { if(dp[j-c]!=INF) dp[j]=min(dp[j],dp[j-c]+1); } } if(dp[x]!=INF) cout << dp[x] << "\n"; else cout << -1 << "\n"; } ``` ---- ### [CSES - Coin Combinations I](https://cses.fi/problemset/task/1635) 給定$n$枚硬幣,價值$c_1,c_2,\dots,c_n$元,求湊出$x$元的方法數,不同順序算不同種,$1\le n\le 100,1\le x\le 10^6$ ---- * 令$dp[i]$為湊出$i$元的方法數 * $dp[i]=dp[i]+dp[i-c_j]$ * $dp[0]=1$ * $ans=dp[x]$ * 在每個價錢都跑一次所有硬幣,所以不同順序也會被算進去 ---- 程式碼: ```cpp #include <bits/stdc++.h> #define int long long #define MOD 1000000007 using namespace std; int c[101],dp[1000001]; signed main() { int n,x,i,j; cin >> n >> x; for(i=1;i<=n;i++) cin >> c[i]; dp[0]=1; for(i=1;i<=x;i++) { for(j=1;j<=n;j++) { if(i-c[j]>=0) dp[i]=(dp[i]+dp[i-c[j]])%MOD; } } cout << dp[x] << "\n"; } ``` ---- ### [CSES - Coin Combinations II](https://cses.fi/problemset/task/1636) 給定$n$枚硬幣,價值$c_1,c_2,\dots,c_n$元,求湊出$x$元的方法數,不同順序算同一種,$1\le n\le 100,1\le x\le 10^6$ ---- * 令$dp[j]$為湊出$j$元的方法數 * $dp[j]=dp[j]+dp[j-c_j]$ * $dp[0]=1$ * $ans=dp[x]$ * 每個硬幣依序跑一次所有價錢,因此必定照著題目給的順序,同一種組合只會有一種排列 ---- 程式碼: ```cpp #include <bits/stdc++.h> #define int long long #define MOD 1000000007 using namespace std; int dp[1000001]; signed main() { int n,x,c,i,j; cin >> n >> x; dp[0]=1; for(i=1;i<=n;i++) { cin >> c; for(j=c;j<=x;j++) dp[j]=(dp[j]+dp[j-c])%MOD; } cout << dp[x] << "\n"; } ```
{"title":"動態規劃","description":"動態規劃(Dynamic Programming),簡稱DP,透過把原問題分解為相對簡單的子問題來求解,並且把已經算過的數字存起來,之後再碰到就可以直接用。","contributors":"[{\"id\":\"3aeed4e7-1118-47e7-b0cc-18caea236427\",\"add\":11027,\"del\":854}]"}
    258 views