# 動態規劃優化 by: hush --- ## 內容 ---- 這堂課主要會教各種dp的優化,通常會去優化轉移的效率,或是優化儲存空間 --- ## 前綴和優化 ---- - 例題: [csdc412](https://csdc.tw/problem/412/) ---- - 題意: 有$n$個物品編號$1$~$n$,可以取任意個物品但任意兩物品的編號相差要$\ge m$,求有幾種取法(答案對$10^9+7$取餘) $1\le m\le n\le 10^6$ ---- - 定義狀態: $dp[n+1]$,$dp[i]$代表考慮前$i$個物品且必須取第$i$個物品有幾種可能 - 轉移式: $dp[i]=\sum\limits_{j=0}^{i-m}{dp[j]},\ i> m$ ---- - 初始條件: $dp[i]=1,\ 0\le i\le m$ - 答案: $\sum\limits_{i=0}^{n}{dp[i]}$ ---- - 時間複雜度: 有$O(n)$個狀態,每次轉移要$O(n-m)$時間,共要$O(n(n-m))$,最差情況下會TLE需要優化 - 優化方式: 可以前綴和優化,每次算出$dp[i]$就同時維護$pre[i](pre[i]=\sum\limits_{j=0}^{i}{dp[j]}=pre[i-1]+dp[i])$,可以做到$O(1)$轉移 ---- - 新的轉移式: $dp[i]=pre[i-m],\ i>m$ - 甚至可以不用$dp$陣列,只留下$pre$陣列,答案就會是$pre[n]$ ---- AC code ```cpp= #include<bits/stdc++.h> #define int long long using namespace std; constexpr int MOD=1000000007; int pre[1000005]={1}; signed main() { int n,m; cin>>n>>m; for (int i=1;i<=n;i++) { if (i<=m) pre[i]=(pre[i-1]+1)%MOD; else pre[i]=(pre[i-1]+pre[i-m])%MOD; } cout<<pre[n]<<'\n'; } ``` ---- - 練習題: [atcoder_DP_M](https://atcoder.jp/contests/dp/tasks/dp_m) ---- AC code ```cpp= #include<bits/stdc++.h> #define int long long #define itn int #define endl '\n' #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); using namespace std; constexpr int MOD=1000000007; vector<int> a(105); vector<vector<int>> dp(105,vector<int>(100005,0)); signed main() { //colinorz; int n,k; cin >> n >> k; for (int i=1;i<=n;i++) cin >> a[i]; for (int i=0;i<=n;i++) for (int j=0,sum=0;j<=k;j++) { if (i>0) sum=(sum+dp[i-1][j])%MOD; if (i>0 && j-a[i]-1>=0) sum=(sum-dp[i-1][j-a[i]-1]+MOD)%MOD; if (j==0) dp[i][j]=1; else if (i==0) dp[i][j]=0; else dp[i][j]=sum; } cout << dp[n][k] << endl; } ``` --- ## 滾動數組優化 ---- - 內容: 很多問題(例:背包問題)的轉移式會長得像 $dp[i]...=dp[i-1]...$醬子 你最多只需要兩個陣列的空間(或一行) - 用處: OJ的空間一般只能開到$2\times 10^7$左右,遇到很壞的問題會需要優化空間,把陣列降維 ---- - 兩個陣列: 將兩個陣列交替使用,可以使用取餘數 - 一個陣列: 需要注意轉移的方向決定迴圈方向,以下用背包問題舉例 ---- 無限背包問題: ```cpp= for (int i=0;i<n;i++) for (int j=0;j<n;j++) dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]) ``` 變成 ```cpp= for (int i=0;i<n;i++) for (int j=0;j<n;j++) dp[j]=max(dp[j],dp[j-w[i]]+v[i]) ``` ---- 0/1背包問題: ```cpp= for (int i=0;i<n;i++) for (int j=0;j<n;j++) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]) ``` 變成 ```cpp= for (int i=0;i<n;i++) for (int j=n;j>=0;j--) dp[j]=max(dp[j],dp[j-w[i]]+v[i]) ``` 為什麼迴圈倒過來跑 ---- 無限背包的轉移 | i\j | 0 | 1 | 2 | 3 | 4 | 5 | | --- | --- | --- | --- | --- | --- | --- | | 2 | | | | ▲ | | | | 3 | | ▲ | | ● | | | 要先更新前面的,所以從迴圈往右 ---- 0/1背包的轉移 | i\j | 0 | 1 | 2 | 3 | 4 | 5 | | --- | --- | --- | --- | --- | --- | --- | | 2 | | ▲ | | ▲ | | | | 3 | | | | ● | | | 前面的不能被蓋掉,後面的沒用,迴圈往左 ---- - 挑戰題: [csdc259](https://csdc.tw/problem/259/) ---- AC code ```cpp= #include<bits/stdc++.h> #define int long long using namespace std; int dp[2][10000002]={}; int v[10005],w[10005]; signed main() { int n,m,k=0;cin>>n>>m; for (int i=1;i<=n;i++) cin>>v[i]; for (int i=1;i<=n;i++) cin>>w[i]; for (int i=1;i<=m;i++) if(i>=w[1])dp[1][i]=v[1]; for (int i=2;i<=n;i++,k=(k+1)&1) for (int j=m;j;j--) if (j>=w[i])dp[k][j]=max(dp[k][j-w[i]]+v[i],dp[(k+1)&1][j]); else dp[k][j]=dp[(k+1)&1][j]; cout<<dp[(k+1)&1][m]/3<<'\n'; } ``` --- ## 矩陣快速冪 ---- - 矩陣乘法介紹: 若 $A$ 為 $m \times n$ 矩陣,$B$ 為 $n \times p$ 矩陣,則它們的乘積 $C = AB$ 為一個 $m \times p$ 的矩陣,其每個元素 $c_{ij}$ 計算方式如下: $c_{ij} = \sum_{k=1}^{n} a_{ik} \cdot b_{kj}$ 這表示 $C$ 的第 $i$ 行第 $j$ 列的元素,是矩陣 $A$ 的第 $i$ 行與矩陣 $B$ 的第 $j$ 列對應元素相乘後的總和。 ---- 例如考慮兩個數字矩陣: $A = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}, \quad B = \begin{pmatrix} 5 & 6 \\ 7 & 8 \end{pmatrix}$ 則它們的乘積 $C = AB$ 為: $C = \begin{pmatrix} 1 \cdot 5 + 2 \cdot 7 & 1 \cdot 6 + 2 \cdot 8 \\ 3 \cdot 5 + 4 \cdot 7 & 3 \cdot 6 + 4 \cdot 8 \end{pmatrix}$ $=\begin{pmatrix} 19 & 22 \\ 43 & 50 \end{pmatrix}$ ---- 也可以用來表達方程組: $\left\{ \begin{array}{l} 1x + 2y = 5 \\ 3x + 4y = 11 \\ \end{array} \right.$ 將其轉換為矩陣形式: $\begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} = \begin{pmatrix} 5 \\ 11 \end{pmatrix}$ ---- - 費氏數列的定義為: $\left\{ \begin{array}{l} F(n) = F(n-1) + F(n-2)\\ F(n-1) = F(n-1) + 0\\ \end{array} \right.$ ---- - 我們可以將遞迴式轉換成矩陣形式: $\begin{pmatrix} F(n) \\ F(n-1) \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix} \begin{pmatrix} F(n-1) \\ F(n-2) \end{pmatrix}$ ---- - 由此遞推可得: $\begin{pmatrix} F(n) \\ F(n-1) \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}^{n-1} \begin{pmatrix} F(1) \\ F(0) \end{pmatrix}$ - 結論: 透過矩陣快速冪方法計算矩陣$\begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}^{n-1}$,即可在$O(\log n)$時間內求得答案。 ---- - 例題: [csdc311](https://csdc.tw/problem/311/) ---- AC code ```cpp= #include<bits/stdc++.h> #define int long long #define itn int #define endl '\n' #define ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); #define vvi vector<vector<int>> using namespace std; const int MOD=1e9+7; vvi operator * (const vvi& a,const vvi& b) { vvi tmp(a.size(),vector<int>(b[0].size(),0)); for (int i=0;i<a.size();i++) for (int j=0;j<b[0].size();j++) for (int k=0;k<b.size();k++) tmp[i][j]+=(a[i][k]*b[k][j])%MOD; return tmp; } vvi p={{1,1},{1,0}},f={{1,0},{0,1}}; signed main() { int n; cin >> n; if (n==0||n==1) cout << n <<endl; else { for (n--;n>0;n>>=1) { if (n&1!=0) f=(f*p); p=(p*p); } cout << f[0][0]%MOD << endl; } } ``` ---- - 練習題: [csdc矩陣快速冪](https://csdc.tw/problems/?tag=%E7%9F%A9%E9%99%A3%E5%BF%AB%E9%80%9F%E5%86%AA) ---- csdc410 AC code ```cpp= #include<bits/stdc++.h> #define int long long #define itn int #define endl '\n' #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); using namespace std; constexpr int MOD=1e9+7; int n,m,k,s,sum=0; vector<vector<int>> ans; vector<vector<int>> operator*(const vector<vector<int>>& a,const vector<vector<int>>& b) { fill(ans.begin(),ans.end(),vector<int>(n,0)); for (int i=0;i<a.size();i++) for (int j=0;j<b[0].size();j++) for (int k=0;k<b.size();k++) ans[i][j]=(ans[i][j]+a[i][k]*b[k][j])%MOD; return ans; } vector<vector<int>> dp,tmp; vector<int> init; signed main() { colinorz; cin >> n >> m >> k; tmp.resize(n,vector<int>(n,0)),ans.resize(n,vector<int>(n,0)),dp.resize(n,vector<int>(n,0)); for (int i=0;i<n;i++) dp[i][i]=1; for (int i=0;i<n;i++) tmp[i][(i+1)%n]=1; while (m--) { int a,b; cin >> a >> b; tmp[a-1][b-1]=1; } cin >> s; init=tmp[s-1]; for (int p=k;p>0;p>>=1) { if (p&1) dp=dp*tmp; tmp=tmp*tmp; } for (int i=0;i<n;i++) sum=(sum+dp[s-1][i])%MOD; cout << sum << endl; } ``` --- ## 單調對列優化 (感謝資芽) ---- 給你一個整數序列(長度$n$),取任意個整數且相鄰兩個距離$\le k$,求取出的最大加總,$O(n)$時間內完成 ---- - 定義狀態: $dp[n+1]$,$dp[i]$代表考慮前$i$個數且必須取第$i$個數的最大加總 - 轉移式: $dp[i]=a[i]+\max(dp[i-j]),\ j\in [1,k]$ ---- - 初始條件: $dp[0]=0$ - 答案: $\max(dp[i]),\ i\in [0,n]$ - 時間複雜度: 有$O(n)$個狀態,每次轉移要$O(k)$時間,共要$O(nk)$,最差情況下會TLE需要優化 ---- - 優化方式: 注意到如果$i<j,\ dp[i]\le dp[j]$,那$dp[i]$就不需要被考慮了 所以我們用一個容器維護那些需要被考慮的數 - 實作細節: 1.算完$dp[i]$就把push數對$(dp[i],i)$進容器 2.若容器裡所有$dp[j]\le dp[i]$的數對都pop掉 3.最大值會在最前面(index最小的),pop掉過期的元素就是答案 (此容器為遞減的對列故稱單調對列) ---- - 時間複雜度: 因為一個$dp$值只會被push和pop一次,所以均攤下來維護單調對列和轉移只需要$O(n)$的時間 - 備註: 可以使用deque或是陣列實作,deque較方便但常數大 ---- e.g.: $a[5]=\{\ 3,-2,-1,-2,\ 2\},\ k=3$ | 1 | 2 | 3 | 4 | 5 | |:---:|:---:|:---:|:---:|:---:| | ( ) | ( ) | ( ) | ( ) | ( ) | $dp[1]=3,\ \text{push}(3,1)$ ---- e.g.: $a[5]=\{\ 3,-2,-1,-2,\ 2\},\ k=3$ | 1 | 2 | 3 | 4 | 5 | |:-------:|:---:|:---:|:---:|:---:| | $(3,1)$ | ( ) | ( ) | ( ) | ( ) | $dp[1]=3,\ \text{push}(3,1)$ ---- e.g.: $a[5]=\{\ 3,-2,-1,-2,\ 2\},\ k=3$ | 1 | 2 | 3 | 4 | 5 | |:-------:|:---:|:---:|:---:|:---:| | $(3,1)$ | ( ) | ( ) | ( ) | ( ) | $dp[2]=-2+3,\ \text{push}(1,2)$ ---- e.g.: $a[5]=\{\ 3,-2,-1,-2,\ 2\},\ k=3$ | 1 | 2 | 3 | 4 | 5 | |:-------:|:---:|:---:|:---:|:---:| | $(3,1)$ | $(1,2)$ | ( ) | ( ) | ( ) | $dp[2]=-2+3,\ \text{push}(1,2)$ ---- e.g.: $a[5]=\{\ 3,-2,-1,-2,\ 2\},\ k=3$ | 1 | 2 | 3 | 4 | 5 | |:-------:|:---:|:---:|:---:|:---:| | $(3,1)$ | $(1,2)$ | ( ) | ( ) | ( ) | $dp[3]=-1+3,\ \text{push}(2,3)$ ---- e.g.: $a[5]=\{\ 3,-2,-1,-2,\ 2\},\ k=3$ | 1 | 2 | 3 | 4 | 5 | |:-------:|:---:|:---:|:---:|:---:| | $(3,1)$ | $(2,3)$ | ( ) | ( ) | ( ) | $dp[3]=-1+3,\ \text{push}(2,3)$ ---- e.g.: $a[5]=\{\ 3,-2,-1,-2,\ 2\},\ k=3$ | 1 | 2 | 3 | 4 | 5 | |:-------:|:---:|:---:|:---:|:---:| | $(3,1)$ | $(2,3)$ | ( ) | ( ) | ( ) | $dp[4]=-2+3,\ \text{push}(1,4)$ ---- e.g.: $a[5]=\{\ 3,-2,-1,-2,\ 2\},\ k=3$ | 1 | 2 | 3 | 4 | 5 | |:-------:|:---:|:---:|:---:|:---:| | $(3,1)$ | $(2,3)$ | $(1,4)$ | ( ) | ( ) | $dp[4]=-2+3,\ \text{push}(1,4)$ ---- e.g.: $a[5]=\{\ 3,-2,-1,-2,\ 2\},\ k=3$ | 1 | 2 | 3 | 4 | 5 | |:-------:|:---:|:---:|:---:|:---:| | () | $(2,3)$ | $(1,4)$ | () | ( ) | $dp[5]=2+2,\ \text{push}(4,5)$ ---- e.g.: $a[5]=\{\ 3,-2,-1,-2,\ 2\},\ k=3$ | 1 | 2 | 3 | 4 | 5 | |:-------:|:---:|:---:|:---:|:---:| | () | $(4,5)$ | () | () | ( ) | $dp[5]=2+2,\ \text{push}(4,5)$ ---- ### 有限背包 ---- - 轉移式: $dp[i][j]=\max(dp[i-1][j],dp[i][j-t\cdot w[i]]+$ $t\cdot v[i]),\ t\le k[i]$ - 翻譯: $dp[i][j]$為往前$k[i]$個$dp值$的最大值(每次跳$w[i]$個),所以對每個$\mod w[i]$相同的$j$做單調對列DP就可以在$O(NW)$時間內算完 ---- - 練習題: [APCSS_14_P4]() [zj_a161](https://zerojudge.tw/ShowProblem?problemid=a161) ---- APCSS_14_P4 AC code ```cpp= #include<bits/stdc++.h> #define int long long #define itn int #define endl '\n' #define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0); using namespace std; struct node { int w,v,c; node(int _w,int _v,int _c) {w=_w,v=_v,c=_c;} }; vector<node> a(1005,node(0,0,0)); int dp[1005][1005]; deque<pii> dq; //(k, dp) signed main() { //colinorz; memset(dp,0,sizeof(dp)); int n,m; cin >> n; for (int i=1;i<=n;i++) cin >> a[i].w >> a[i].v >> a[i].c; cin >> m; for (int i=1;i<=n;i++) { for (int j=0;j<a[i].w;j++) { for (int k=j,cnt=0;k<=m;k+=a[i].w,cnt++) { if (!dq.empty() && cnt-dq.front().fi>a[i].c) dq.pop_front(); while (!dq.empty() && dp[i-1][k]>=dq.back().se+(cnt-dq.back().fi)*a[i].v) dq.pop_back(); dq.push_back({cnt,dp[i-1][k]}); dp[i][k]=dq.front().se+(cnt-dq.front().fi)*a[i].v; } dq.clear(); } } cout << dp[n][m] << endl; } ``` ---- zj_a161 AC code ```cpp= ``` --- # 謝謝大家
{"title":"動態規劃優化","description":"by: hsuh","contributors":"[{\"id\":\"b49547c8-0e7f-46d0-99b2-8a45dfee8e90\",\"add\":15074,\"del\":3893}]"}
    77 views