<style> .reveal .slides { text-align: left; font-size:30px } </style> # DP optimization Introduction to Competitive Programming 2022/05/25 ---- - DP with Data Structure - 單調隊列優化 - 最大子矩陣 - nearest greater element --- ## DP with Data Structure Longest Increasing Subsequence 給你一個長度為 $N$ $(1 \le N \le 10^5)$ 的整數序列 $a$, 求出最長的子序列其元素為非嚴格遞增 ---- ## DP 轉移式 `dp[i]` 為以第 $i$ 個元素為結尾的最佳解 轉移由前面某個元素 `a[j]` 為結尾且 `a[j]` $\le$ `a[i]` `dp[i] = max(dp[j]) + 1` ; \( `j < i` $\land$ `a[j]` $\le$ `a[i]`) 複雜度 $O(N^2)$ ---- `dp[i] = max(dp[j]) + 1` ; \( `j < i` $\land$ `a[j]` $\le$ `a[i]`) 要求以上轉移式,等價於求所有 $\le$ `a[i]` 的元素中,最大的 dp 值 可以用線段樹維護,線段樹儲存的為所有出現過的值 以序列 [1, 3, 2, 4, 3] 為例 可以算出分別的 dp 值為 [1, 2, 2, 3, 3] ![](https://i.imgur.com/UwoR6xX.png =300x) 最底層的元素為到目前為止 分別以數字 1, 2, 3, 4 為結尾的元素中最大的 DP 值 ---- 計算 DP 從第一個元素開始,一開始線段樹每個值都設為 0 每次算 DP[i] 等於前面所有小於自己的元素中最大的 DP 值 直接 query 線段樹小於等於自己的區間的最大值即可 ```cpp dp[i] = segTree.query(0, lower_bound(ind.begin(),ind.end(),a[i]))-ind.begin()) + 1; ``` 原本需要迴圈跑過全部小於自己的所有元素 DP 值, 現在只需要區間詢問最大值+1 即為 dp[i] 複雜度 $O(NlgN)$ ---- ### 單調隊列優化 例題 [相隔小於一定距離最小總和子序列](https://zerojudge.tw/ShowProblem?problemid=c528) 給定一個長度為 $N$ 的整數序列及一個正整數 $K$, 請蓋掉任意個數字使得原序列中任意的連續 $K$ 個數字都至少有一個數字被蓋掉了,請問蓋掉的數字的總和最小為多少? ---- 給定一個長度為 $N$ 的整數序列及一個正整數 $K$, 請蓋掉任意個數字使得原序列中任意的連續 $K$ 個數字都至少有一個數字被蓋掉了,請問蓋掉的數字的總和最小為多少? ### 轉移式 `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 10^{12}$ 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]); } } ``` 看上面程式碼會發現 3~5行求的就是區間 [i-k,i-1] 之間的最小值 因此可以用線段樹優化,找區間最小值 ```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)$ ---- 而線段樹實作複雜度很高, 實際上可以用 STL priority_queue 儲存 每次放進去 pair(dp[i], i); 每次取 priority_queue 最小的 dp 值, 而如果最小值過期則直接 pop 掉 --- ### 單調隊列 會發現我們在求第 $i$ 項的dp值時 只需要保留 $[i-k,i-1]$ 的 dp 值就好,更前面的完全用不到 並且對於所有(i, j) 如果 dp[i] $\ge$ dp[j] $\land$ i < j 只需要保留 dp[j] 即可, dp[i] 不可能更新最小值 所以最後剩下的 DP 值都會符合 如果 i < j 則 dp[i] < dp[j] 而可以用 deque 儲存這些元素 ---- ### deque STL 資料結構,每次可以 $O(1)$ 從 front, end 新增刪除元素 因此 每次算出第 i 項的 dp 值放進 deque 前 裡面的比 dp[i] 大的值都可以 pop 掉,只需保留比他小的 dp 值就好 並且如果裡面有值過期(index < i-k),則也將他刪除 deque 裡面的元素最後只會單調遞增 $dp[i]<dp[j] \ \forall \ i<j$ ---- ### 例子 n = 9, k = 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 \times M ( N, M \le 2000)$ 的矩陣,上面有一些格子有障礙物, 要找出一個子矩陣,沒有任何障礙物。 問你所有合法的子矩陣中,最大面積為多少? ![](https://i.imgur.com/UKFciNm.png) 以上圖為例,假設黑色為障礙物,則合法中最大的子矩陣為紅色框起來的,面積為6 </div> ---- ## 想法1 <div style="font-size: 30px"> 暴力窮舉所有矩陣判斷合不合法 $O(N^6)$ * 窮舉左上角 $O(N^2)$ * 窮舉右下角 $O(N^2)$ * 窮舉矩陣內的每一格判斷是否有障礙物 $O(N^2)$ </div> ---- ## 想法2 <div style="font-size: 30px"> * 窮舉左上角 $O(N^2)$ * 窮舉右下角 $O(N^2)$ * ~~窮舉矩陣內的每一格判斷是否有障礙物 $O(N^2)$~~ 計算前綴和,左上到每個點為右下的矩形裡面的障礙物數量 ![](https://i.imgur.com/ERdxJhx.png) 如果要計算區間內的障礙物數量,用排容原理 假設要求 [2,2] 到 [3,4] 子矩陣的障礙物數量 則為 prefix[3][4] - prefix[3][1] - prefix[1][4] + prefix[1][1] </div> ---- * 窮舉左上角 $O(N^2)$ * 窮舉右下角 $O(N^2)$ 複雜度 $O(N^4)$ 此作法複雜度足夠快去解昨天 [CPE 第五題](https://onlinejudge.org/external/1/108.pdf) ---- ## 想法3 ### DP 先 $O(N^2)$ 計算出每格往上最高可以到多高 ![](https://i.imgur.com/SK23MaS.png) 之後窮舉以每格為右下角的所有可能矩形 每次往左判斷從起始格到當前往上最高可以多高 以 [3,4] 為例 往左 1 格高度最高可以為 2 往左 2 格高度最高可以為 2 往左 3 格高度最高可以為 1 ---- * 窮舉右下角 $O(N^2)$ * 窮舉左界 $O(N)$ 複雜度 $O(N^3)$ 但...還不夠 $N^3 = 8 \cdot 10^9 \to TLE$ ---- ## O($N^2$) 做法 先 DP 每格往上有幾格可以用 一樣窮舉以每個點當右下 對於每一個 row 變成轉換為以下問題 [ZJ c907](https://zerojudge.tw/ShowProblem?problemid=c907) 現有 N 個寬度為1單位的長條圖(例如下圖所示),試求此長條圖中可以形成的最大矩形面積。 ![](https://i.imgur.com/kUmzTaM.png =300x) ---- ## 作法 使用 stack 維護遞增子序列 stack 裡儲存:從 index $i$ 開始,可以放的最高高度為 $h$ [2, 5, 7, 4, 5] ![](https://i.imgur.com/svsn4qA.png =250x) 前三個長度為遞增的,因此可以直接放進 stack 裡面 stack: [(1, 2), (2, 5), (3, 7)] ---- ![](https://i.imgur.com/svsn4qA.png =250x) 到前 3 個為止 stack 裡的都是合法的 從 index 1 開始到目前第 3 個為止可以放的矩形最高高度為 2 從 index 2 開始到目前第 3 個為止可以放的矩形最高高度為 5 從 index 3 開始到目前第 3 個為止可以放的矩形最高高度為 7 而第 4 個高度為 4,如果直接放進去 stack 會破壞維護的性質 因此所有高度比他高的都要 pop 掉 而 pop 掉的時候順便更新答案 ---- ## pop 更新答案 ![](https://i.imgur.com/H0B7bYv.png =250x) stack: [(1, 2), (2, 5), (3, 7)] 當前為 (4, 4) 因此要 pop 掉 (2, 5), (3, 7) 也就是 index 2 開始高度 5 只能到 index 3 index 3 開始高度 7 只能到 index 3 分別用以上兩種高度 $\times$ 寬度更新答案(最大面積) (3-2+1)*5, (3-3+1)*7 ---- ![](https://i.imgur.com/sijCLqp.png =250x) 更新完後即可把當前高度放進 stack 裡面 原本要放進去為 (4, 4) 從 index 4 開始到目前為止可以放的矩形最高高度為 4 但會發現 pop 掉的位置高度都 $\ge$ 當前高度 因此放進去的為 pop 掉的所有位置最前面那個 為 (2, 4) 從 index 2 開始到目前為止可以放的矩形最高高度為 4 ---- ![](https://i.imgur.com/UYsSpYw.png =250x) 做完後 stack 裡會有 [(1, 2), (2, 4), (5,5)] 為遞增子序列分別為從 從 index 1 開始到最後可以放的矩形最高高度為 2 從 index 2 開始到最後可以放的矩形最高高度為 4 從 index 5 開始到最後可以放的矩形最高高度為 5 而也可以用這些去更新答案(最大面積) (5-1+1)*2, (5-2+1)*2, (5-5+1)*5 ---- ## 複雜度 從左往右掃描過去 每個高度會被放到 stack 一次,最多也只會 pop 出來一次 複雜度為 $O(M)$ 而如果是要求最大子矩形的面積總共有 N 行 每行做一次複雜度為 $O(NM)$ --- ## nearest greater element 給你一個長度為 $N$ $(1 \le N \le 10^6)$ 的正整數序列 $a$ 對於每個數字,求出往左跟往右第一個大於他的數字, 如果找不到就輸出 -1 sample input ``` 5 4 2 1 7 3 ``` sample output ``` -1 7 4 7 2 7 -1 -1 7 -1 ``` ---- 暴力可以 O($N^2$) 每個數字往左或往右找 或者維護一顆 取max的線段樹從當前位置找下去, 遞迴時 greedy 選盡量靠近的位置 複雜度 O(NlgN) ---- 用 stack 做 類似求最大子矩陣作法 往左跟往右各做一次 分別維護一個遞減序列 從第一個元素開始 push 進去 如果 stack 的 top $\le$ 當前元素則 pop 掉 直到 stack 變空的或 top 比當前元素大為止 ---- a = [4, 2, 1, 7, 3] 第 1 個元素: stack 是空的左邊沒有比他大的值 接著放進 stack 裡, stack = [4] 第 2 個元素: stack 的 top 為 4 比當前元素大, 紀錄答案並放進 stack 裡, stack = [4, 2] 第 3 個元素: stack 的 top 為 2 比當前元素大, 紀錄答案並放進 stack 裡, stack = [4, 2, 1] 第 4 個元素: stack 的 top 為 1, 比當前元素小 pop 到空或者當前元素大為止 接著放進 stack 裡, stack = [7] 第五個元素: stack 的 top 為 7, 比當前元素大, 紀錄答案並放進 stack 裡 ---- 複雜度 $O(N)$ --- ### Question Time and Practice [Homework Link](https://vjudge.net/contest/497108) 提交期限到下星期三 6/1 23:59 截止
{"metaMigratedAt":"2023-06-17T00:51:08.805Z","metaMigratedFrom":"YAML","title":"DP optimization","breaks":true,"contributors":"[{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":7231,\"del\":665}]","description":"Introduction to Competitive Programming"}
    764 views