<style> .reveal .slides { text-align: left; } </style> # DP optimization Competitive programming 2 2021/10/25 ---- * 滾動 * 二進位分解 * 矩陣快速冪 * 單調隊列 * 狀態壓縮 --- ## 滾動DP 給兩個字串,求其中的最長共同子序列 (Longest Common Subsequence) x = 'cabdefga' y = 'dcea' LCS(x,y) = 3 ('dea', 'cea') ---- 先列出 dp 轉移式 dp[i][j] = max(dp[i-1][j],dp[i][j-1]); if x[i] != y[j] dp[i-1][j-1] + 1 if x[i] == y[j] 儲存狀態 如果 |x| 為 n, |y| 為 m 則需要開 dp[n][m] 的陣列 ---- ### 滾動優化 - 空間優化 <div style="font-size: 30px"> 可以注意到當我們在求 dp[i][j] 時 只需要用到 dp[ i-1 ][ j ], dp[ i ][ j-1 ], dp[ i-1 ][ j-1 ] 不需要用到 dp[i-2][k], dp[i-3][k], dp[i-4][k], ... (k $\in$ 1~m ) 所以我們只需要開 dp[2][m] 的大小 </div> dp[i&1][j] = max(dp[~i&1][j],dp[i&1][j-1]); if x[i] != y[j] dp[~i&1][j-1] + 1 if x[i] == y[j] 最後答案會存在 dp[n&1][m] &1 為取二進位最後一個位元是 0 或 1 ~ 是取補數,每個位元 0 變 1,1 變 0 ---- <div style="font-size: 30px"> 滾動不僅降低空間複雜度, 執行時間也會因為空間大小而有所差別 部分題目可能會因為沒有滾動造成常數過大而TLE 以下為例題有無滾動的差別 [b953: 能力越大責任越大](https://zerojudge.tw/ShowProblem?problemid=b953) </div> ![](https://i.imgur.com/MFf3Hrb.png) --- ## 有限背包問題 <div style="font-size: 30px"> 有一個容量為 $W$ 的背包,及 $N$ 種物品,每種物品有各自的重量 $w_i$ 和價值 $v_i$,且每種物品數量有 $k_i$ 個,求在重量不超過 $W$ 的情況下,背包能塞的最大價值是多少 </div> W = 10 N = 3 item1 : w_i = 3, v_i = 5, k_i = 3 item2 : w_i = 2, v_i = 2, k_i = 5 item3 : w_i = 4, v_i = 6, k_i = 1 <div style="font-size: 30px"> 最佳拿法 item1 拿2個, item3 拿1個, 價值為 16 </div> ---- ### 作法 <div style="font-size: 30px"> 將每種物品取不同個視為不同東西 </div> item1 : w_i = 3, v_i = 5, k_i = 3 <div style="font-size: 30px"> 以 item1 有三個為例,則視為 3 個不同物品 </div> w_i = 3, v_i = 5 w_i = 6, v_i = 10 w_i = 9, v_i = 15 <div style="font-size: 30px"> 分別去做背包 每個獨立物品做背包複雜度 $O(W)$ 現在有 $N$ 種物品且每種有 $K$ 個 複雜度為 $O(NWK)$ </div> ---- ### 二進制分解 <div style="font-size: 30px"> 其實每種物品有 $K$ 個,不需要K種組合(1~K)都做一次 我們只需要做 $\lfloor lgK \rfloor$ 次就好 分別捆成 1個、2個、4個、8個、16個、...、$2^{\lfloor lgK \rfloor}$ 這些捆的組合,就可以湊出數量1~k的所有物品 以數量為9為例 分別做1 2 4 8則 </div> 1=1 2=2 3=1+2 4=4 5=4+1 6=4+2 7=4+2+1 8=8 9=8+1 ---- ### 複雜度 <div style="font-size: 35px"> $O(NWK)$ 降成 $O(NWlgK)$ </div> --- ## 矩陣快速冪 <div style="font-size: 25px"> 先備知識 : 快速冪 </div> #### 問題 <div style="font-size: 35px"> 求出費波那契數的第 $k (0 \le k \le 10^{18})$ 項 % 1000000007 直接 $O(N)$ 做 $\to$ TLE </div> ---- #### 轉移式 f[n] = f[n-1] + f[n-2] #### 列成矩陣 $\begin{bmatrix} f[n]\\f[n-1] \end{bmatrix}\quad = \begin{bmatrix} 1&1\\1&0 \end{bmatrix}\quad \times \begin{bmatrix} f[n-1]\\f[n-2] \end{bmatrix}\quad$ ---- ### 求出第N項 一開始我們有第0項和第1項 要求出第 $N$ 項的話 $\begin{bmatrix} f[n]\\f[n-1] \end{bmatrix}\quad = {\begin{bmatrix} 1&1\\1&0 \end{bmatrix}\quad}^{N-1} \times \begin{bmatrix} f[1]\\f[0] \end{bmatrix}\quad$ ---- ### 複雜度 要算出 ${\begin{bmatrix} 1&1\\1&0 \end{bmatrix}\quad}^{N-1}$ 用快速冪優化 $O(lgN)$ 矩陣乘法為 $k^3$ ( $k$ 為矩陣大小) 因此總複雜度為 $O(k^3lgN)$ ---- ### 程式碼 多載矩陣乘法 ```cpp= #define MOD 1'000'000'007 #define ll long long vector<vector<ll>> operator*(const vector<vector<ll>>& lhs,const vector<vector<ll>>& rhs){ vector<vector<ll>> ret(lhs.size(),vector<ll>(rhs[0].size(),0)); for(int i=0;i<lhs.size();i++){ for(int j=0;j<rhs[0].size();j++){ for(int k=0;k<rhs.size();k++){ ret[i][j] += lhs[i][k] * rhs[k][j] % MOD; ret[i][j] %= MOD; } } } return ret; } ``` ---- ### 程式碼 矩陣快速冪 ```cpp= vector<vector<ll>> init_value={{1},{0}}; //第0,1項 vector<vector<ll>> base={{1,1},{1,0}}; //費式數列轉移式 vector<vector<ll>> matrix={{1,0},{0,1}}; //單位矩陣 // x^y while(y){ if(y&1){ matrix = matrix * base; } base = base * base; y >>= 1; } matrix = matrix * init_value; cout<< matrix[0][0] << endl; ``` --- ### 單調隊列優化 例題 [相隔小於一定距離最小總和子序列](https://zerojudge.tw/ShowProblem?problemid=c528) ---- ### 轉移式 dp[i] 定義為 以第i個結尾,並且蓋掉第i個的最佳解 dp[i] = min(arr[i] + dp[j]) ,i-k $\le$ j $<$ i ---- 注意題目 n,k 大小 $k \le n \le 10^6$ 直接做 $\to$ TLE ```cpp= for(int i=0;i<n;i++){ //TLE dp[i] = (i<k?arr[i]:INF); for(int j=i-1;j>=max(i-k,0);j--){ dp[i] = min(dp[i],dp[j] + arr[i]); } } ``` ---- ### 優化? ```cpp= for(int i=0;i<n;i++){ //TLE dp[i] = (i<k?arr[i]:INF); for(int j=i-1;j>=max(i-k,0);j--){ dp[i] = min(dp[i],dp[j] + arr[i]); } } ``` <div style="font-size: 30px"> 看上面程式碼會發現 3~5行求的就是區間 [i-k,i-1] 之間的最小值 因此可以用線段樹優化,找區間最小值 </div> ```cpp= for(int i=0;i<n;i++){ mn = (i<k?arr[i]:INF); mn = min(mn,segTree.query(max(i-k,0),i-1)); segTree.update(i,mn); } ``` 複雜度 O(nlgn) ---- ### 單調隊列 但還可以做得更好,會發現我們在求第i項的dp值時 只需要保留[i-k,i-1]的值就好,更前面的完全用不到 並且如果有一個算出來的值比前面的都好,那前面的值都不需保留 因為已經有一個更好的了 ---- ### deque STL 資料結構,每次可以 $O(1)$ 從頭尾新增刪除元素 因此 我們每次算出一個dp值就看說 deque 裡面的比他小的元素都刪掉,只需保留比他大的就好 並且如果裡面有值過期(index < i-k),則也將他刪除 deque裡面的元素最後只會單調遞增 ---- ### 例子 9 3 8 3 6 5 7 -5 1 8 5 ---- ### 範例 ```cpp= deque<pair<int,int>> dq; //index, value int calc(){} for(int i=0;i<n;i++){ // dp 求最大值 while(!dq.empty() && dq.front().first < i-k) dq.pop_front(); //判斷dq裡的東西有沒有過期 dp[i] = calc(); while(!dq.empty() && dq.back().second <= dp[i]) dq.pop_back(); //判斷dq裡的東西是不是比當前dp[i]小,如果比較小就去掉 dq.push_back(make_pair(i,dp[i])); } //保證dq從左至右value由大到小 ``` ---- ### 複雜度 $O(N)$ --- ### 狀態壓縮 旅行商人問題 <div style="font-size: 30px"> 給你一張圖,n個點m條邊 保證連通,邊上有權重 一開始有一個商人在節點0的位置, 他希望走過所有的點(0~n-1每個點), 並且最後回到節點0 </div> ---- <div style="font-size: 30px"> 會發現如果每個點對之間都有連邊 那直接暴力dfs複雜度會變成 $O(N!)$ 而如果 dp 的話記錄說哪幾個點有走過, 總狀態數為 $2^n$ (每個點有走過或沒走過) 並且從當下狀態轉移到下一個狀態複雜度為 $O(N)$, 選擇下一個點要走到哪 </div> ---- <div style="font-size: 30px"> 而對於每個點是否走過,我們可以用一個數字來記錄儲存狀態 假設5個點,第0,1,4個點有走過,第2,3還沒走過 則可以表示成 10011 從左至右分別為第4、第3...第0個點是否走過 然後存成十進位 19(10011) 而全部都有走過的狀態為 11111 反過來為 00000 因此我們開的大小為 dp[n][1<<n] n為當前狀態最後一個點走到的點是誰,以及目前走過哪些點 </div> ---- 而要判斷當下狀態status的某個點x是否走過 ```cpp= flag = status >> x & 1; // status右移x格去 and 1 如果為1代表走過 ``` 轉移狀態,將當前狀態status多加一個點x ```cpp= next_status = status | (1 << x) ; // 原本狀態跟第x個點的位置去 or // 記得位元運算都盡量括號,避免優先度太低造成bug ``` ---- 複雜度 $O(2^nn^2)$ ```cpp= int dp[n][1<<n]; int dis[n][n]; //可以先用floyd-warshall跑過全點對最短路徑 memset(dp,0x3f3f3f3f,sizeof(dp)); for(int status=0;status<(1<<n);status++){ //窮舉每一種狀態 for(int j=0;j<n;j++){ //前一個點 if(~status>>j&1) continue; for(int k=0;k<n;k++){ //下一個點 if(status>>k&1) continue; dp[k][status|(1<<k)] = min(dp[k][status|(1<<k)],dp[j][status] + dis[j][k]); } } } } ``` --- ### Question Time and Practice [Homework Link](https://vjudge.net/contest/464705) <div style="font-size: 30px"> 提交期限兩星期,下下星期一 18:30 截止 </div>
{"metaMigratedAt":"2023-06-16T13:07:35.178Z","metaMigratedFrom":"YAML","title":"DP optimization","breaks":true,"contributors":"[{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":7403,\"del\":606}]"}
    776 views