# NYCU PCCA 2022 Winter Camp 0126 ## [基礎動態規劃](https://www.youtube.com/watch?v=Im8BTk1Pykw&list=PLtRlodjJ-YdIizrpGnftvH7hgJEQN7j9E&index=6) 今天的內容只有這個 但是影片幾乎快3小時... ### 前言 動態規劃(Dynamic Programming, DP)是一種演算法的設計方式。 不具備演算法定義上固定的指令集和流程,也因此==解題競賽經常出現==。 ### 先備知識 建議先了解再繼續閱讀~ 1. Big O 要會估計 2. 暴力搜尋要使用DP優化 3. recursive (遞迴) ### 爬樓梯問題 每一階樓梯可以往上跨1或是2步,請問跨到第n階有幾種方式? ![](https://media.discordapp.net/attachments/934058652577443853/935893141356183654/unknown.png) 假設 n = 4,會有5種方式。 我們可以一個一個找,也就是暴力模擬,但是當 n 很大時,就會TLE 事實上,如果你的觀察力敏銳,就能發現: **走到第 n 階的方法數是第 n-1 階的方法數加上第 n-2 階的方法數!!!** with knowing this, we can... 1. 從第 n 層慢慢遞迴下去 (Top-down) 2. 從第 0 階慢慢往上推 (Bottom-up) ### What is dp? 核心觀念:<font size = 4>==將大問題轉成同性質小問題==</font> 則小問題便會因為性質相同,能變成更小的問題,直到規模足夠小的時候,能直接得知答案,這相當於求原問題的遞迴解 * 法 1:**不重複計算** e.g. 費氏數列 f(n) = f(n-1) + f(n-2) f(1)=1, f(2)=1 如果要計算 f(6),要像下圖一樣 f(6) = <font color = "#F0F">f(5)</font> + <font color = "#2D9">f(4)</font> = <font color = "#F0F">f(4) + f(3)</font> + <font color = "#2D9">f(3) + f(2)</font> = <font color = "#F0F">f(3) + f(2) + f(2) + f(1)</font> + <font color = "#2D9">f(2) + f(1) + f(2)</font> = . . . . . . ![](https://media.discordapp.net/attachments/934041752317349958/935897802742038578/unknown.png) 這當中有許多重複的計算,那如果我們把算過的資訊記錄下來呢? ![](https://media.discordapp.net/attachments/934041752317349958/935902573712539678/unknown.png) 就只要計算 6 次! * 法 2:**狀態壓縮** 不儲存不需要的狀態,壓縮掉資訊,以提高執行效率 e.g. 爬樓梯的 dp[5] 1 1 1 1 1 1 1 1 2 1 1 2 1 1 2 1 1 2 1 1 1 2 2 1 2 1 2 1 2 2 只要處存有 8 種方式就好,不需要知道每種方式的詳細步驟 <font size = 4>如果某些狀況在未來擁有相同發展,便可以被壓成相同狀態來看待!</font> * 法 3:**轉移** 轉移就是如何將原本問題與子問題的解產生關聯,進而從原本的解,求得原問題的解 * **複雜度估計** 複雜度基本上就是狀態數量乘以狀態轉移的複雜度 e.g. 爬樓梯問題需要計算的狀態量為n個,空間複雜度為O(n) 時間複雜度為狀態數量乘以2,共是2n,時間複雜度為O(n) * **總結** 1. 當你看到一個題目能切成相同性質的子問題時,往DP的方向去思考 2. 試著去定義狀態,並盡量壓縮掉無意義的資訊(避免陣列開太大、空間複雜度太大) 3. 考慮狀態轉移式,注意邊界問題 4. 複雜度是否合理,如果太大,試著優化狀態轉移式,若還是不行就得重新定義狀態 :::warning **最難的地方:定義狀態轉移式** → 只能靠多刷題累積觀察經驗 ::: ### 題目類型 #### 1. 計數型 一般會叫你計算出總共有多少情況 e.g. 前面提到的爬樓梯問題, 爬格子問題 . . . **爬格子問題**:有 n * m 的方格,想要從最左上到最右下,且只能走最短路徑 (只能往右和往下走),共有幾種方法? ![](https://scontent.ftpe9-1.fna.fbcdn.net/v/t1.15752-9/271718908_939661920243187_4303867167502735636_n.png?_nc_cat=101&ccb=1-5&_nc_sid=ae9488&_nc_ohc=MqAufUNPzvIAX8Qy4Lv&tn=XBNYG0ekgr0Nzyql&_nc_ht=scontent.ftpe9-1.fna&oh=03_AVKqgw5bxv_DaDJltEwfU5KSErlgGyP5ee4RPCCWgrL12g&oe=6217E169) 解法:由於只能往右和往下走,在某一格的方法數必定來自左邊的方法數和右邊的方法數相加 > **計數型的轉移必須包含所有類型,才不至於少算 > 各種情形必須獨立不重複,才不至於多算 > → 「不漏不重」** #### 2. 最佳化型 如果將爬樓梯問題改成最佳化型: 走上第 i 階階梯要花 cost[i] (cost[i]>0) 元,從第 0 階開始往上,每次可以跨一階或是兩階,請問走到第 n 階花費最少要付多少錢? 第 n 階的最小花費要不來自第 n-1 階或 n-2 階再加上走到第 n 階的費用 ``` cpp dp[n] = min(dp[n - 1], dp[n - 2]) + cost[n] ``` :::danger ⚠注意!!! 使用DP的前提是必須證明「子問題的最佳解能得到原問題的最佳解」 ::: 舉個反例: 假設我們現在尋求走上第 n 階時,個位數最小的花費 如果第 5 階的個位數有 6 和 9 兩種可能 但是踏上第 6 階的花費如果是 2 塊錢, 那麼最低花費應該是 (9 + 2) % 10 = 1,而不是 (6 + 2) % 10 = 8 錯誤的原因是:只存最小花費 **未必使之後的解是最小花費** 那怎麼辦呢? 改善方法:除了==改變狀態==,也可以對狀態==追加描述、增維== 將狀態改為 dp[i][j] 表示踏上第 i 階時,達到個位數花費 j 是否有可能 ``` cpp dp[i][j] = (dp[i - 1][k] || dp[i - 2][k]) // k 滿足 k + cost[i] 的個位數為 j ``` ### 例題舉隅 #### 1. 切棍子問題 有一根長度為n的木棍,cuts數組標記棍子上一些需要被切開的位置,每次切開所花費的花費是那條木棍的長度,求最佳切割的順序下的最小成本。(Leetcode 1547) ![](https://media.discordapp.net/attachments/934041752317349958/936067429493268590/unknown.png) 我們所知道的正解: 7+4+3+2=16 ![](https://media.discordapp.net/attachments/934041752317349958/936068239346253864/unknown.png) <h5> 1. 暴力解?</h5> 複雜度為 O(m!) <h5> 2. DP 解法?</h5> 假設我們已經知道所有子問題的解了..... ```cpp dp(i,j)=min(dp(i,k)+dp(k,j))+cuts[j]-cut[i], k=i+1,.....,j-1 ``` 意義: 棍子的最小花費就是枚舉第一刀的切法後,兩條子木棍的最小花費總和加上該木棍的長度! 實作: ```cpp= sort(cuts.begin(), cuts.end()); int stick(int st, int en) { if(en - st == 1) return 0; if(dp[st][en] != b) return dp[st][en]; for(int i = st + 1; i < en; i++) { if(dp[st][en] == 0) { dp[st][en]=stick(st, i) + stick(i, en) + (cuts[en] - cuts[st]); } else { dp[st][en] = min(dp[st][en], stick(st, i) + stick(i, en) + (cuts[en] - cuts[st])); } } return dp[st][en]; } ``` 複雜度: 假設 cuts 裏面有m個元素,每次狀態轉移要花 O(m),總共有 m^2^ 種狀態,故複雜度是 O(m^3^) #### 2. 找零錢問題 給你一個金額 n 元,請你回答有多少種硬幣組合方式。 你的幣值有 1元, 5元, 10元, 25元, 50元 請注意,當 n = 0 時,我們算它是一種方式 (from Lucky Cat) 直觀想法: ``` cpp dp[n] = dp[n-1] + dp[n-5] + dp[n-10] + dp[n-25] + dp[n-50]; ``` Does it work? dp[1] = 1, dp[5] = 2 假設用剛剛的式子算 n=6,則 dp[6] = dp[6-1] + dp[6-5] = 3 但是 n=6 只有 111111 和 51 兩種方法 why? 因為重複計算 15 和 51 為了避免這種排列順序不同,組合卻相同的情形,我們希望讓==小的先放、大的後放==,這樣保證了順序必定由小至大,不會反過來,不會出現僅排列不同的問題。 考慮前 n 種硬幣組成金額 m 的情形,可以分成使用第 n 種硬幣,以及不使用,共兩種可能。 不使用的話就只靠前 n-1 種硬幣;使用的話就必須前 n 種組成 m-value[n], 加上第 n 種硬幣剛好金額 m。 ```cpp dp[n][m] = dp[n-1][m] + dp[n][m-value[n]] ``` dp[n-1][m] 定義上必不包含第 n 種硬幣; 而 dp[n][m-value[n]] 則包含 0 至多個第 n 種硬幣,加上一個第 n 種硬幣,則至少有 1 個。 於是兩邊獨立不交集,又互相補完 0 和多個第 n 種硬幣的可能情形。 **滾動陣列** 考慮第 n 種硬幣時,從轉移來看,實際上只需要 dp[n] 和 dp[n-1] 兩條陣列。 因此,可用兩條陣列 p, q 分別代表 dp[n-1] 和 dp[n],之後交替代表 dp[n] 和 dp[n-1], 即為滾動數組,可將空間複雜度自 nm 降至 2n。 實作方法 (通常使用3. 4.): 1. 視奇偶互相交替使用 2. 將新算的結果複製到舊的 3. 宣告 int dp[2][M]; 用 dp[n&1] 和 dp[(n-1)&1] 分別代表 > n&1 和 n%2 是等價的 5. 宣告兩條陣列和兩個指標 p, q 並透過交換 p, q 來輪替 :::spoiler 聽起來已經不錯了 ![](https://media.discordapp.net/attachments/934058652577443853/936089931846152212/4945172.png?width=759&height=427) 還可以優化喔 不要停下來 ::: 我只想用一條陣列啦! 考慮到方向性,m 一定是從金額較小的 m-value[n] 轉移落來的,方向單一,故可用一條陣列 而剛進到第 n 個硬幣時,陣列存的是 n-1 時的內容,便可寫成: ```cpp dp[m] += dp[m-value[n]]; ``` ```cpp for (int i = value[n]; i <= M; i++) { dp[i] += dp[i-value[n]] } // 注意for迴圈順序,如果順敘寫反那答案就會錯喔~ ``` <h4> 3. 背包問題 </h4> 有 n 個物品,每個物品有自己的 價值 : c[i] 和 體積 : w[i] 還有一個容量為 m 的背包 求最佳情況下,可以放入背包的最高價值 <h5> Greedy? </h5> 如果我們直接根據每個物品的價值除以體積來挑呢? 反例 : 當背包容量為 6 且有下方四個物品 | | 價值 | 體積 | CP值 | | -------- | -------- | -------- | -------- | | 物品A | 2 | 3 | 0.66 | | 物品B | 4 | 3 | 1.33 | | 物品C | 2 | 4 | 0.5 | | 物品D | 5 | 4 | 1.25 | :::warning 不會是最佳解 ::: <h5> 暴力? </h5> 直接爆開所有組合 :::warning 開心TLE😀 ::: <h5> 以DP觀點來思考 </h5> * **壓縮狀態** 知道有更高價值的解,就不需要紀錄 * **最簡單的狀況** 當沒有物品時,背包體積 0 ~ m 的最佳解 ? * **同性質小問題** 已知 i – 1 個物品,體積 0 ~ m 的解 → 前 i 個物品,背包體積 0 ~ m 的解 ? <h5> 初始條件及轉移式 </h5> 初始化 : ``` cpp for(j: 0 ~ m) dp[0][j] = 0 ``` 轉移 : ``` cpp dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + price[i]) ``` 舉例來說... | | 價值 | 體積 | CP值 | | -------- | -------- | -------- | -------- | | 物品A | 2 | 3 | 0.66 | | 物品B | 4 | 3 | 1.33 | | 物品C | 2 | 4 | 0.5 | | 物品D | 5 | 4 | 1.25 | #### 4. 最長遞增子序列(LIS) 給一個長度為 n 的陣列 挑出陣列中幾個遞增的元素 找到所有可能中,最長會是多長 ##### 用 DP 觀點來思考 * **壓縮狀態** 知道有更長的解,就不需要紀錄 * **最簡單的狀況** 當陣列長度為 1 的時候,解為 ? * **同性質小問題** 已知 0 ~ i – 1 前所有陣列的解 → 0~i 陣列的解 ? ##### 初始條件及轉移式 初始化 : ```cpp for(i : 0 ~ n) dp[i] = 1 ``` 轉移 : ```cpp if(arr[i] > arr[j]) dp[i] = max(dp[i], dp[j] + 1) ``` ### 其他 #### 1. 回溯法 額外存取最佳解的來源 適合用在問題為「如何得到最佳解」的情況 #### 2. 滾動陣列 動態規劃中,有些資訊再也不會用到了,我們可以透過拿這些陣列來做新一輪的 DP,而不是在開新的陣列,以壓縮記憶體空間。 --- ## 課後講義-DAG DP ### 什麼是DAG? DAG (Directed Acyclic Graph),又稱作無環有向圖 ![](https://i.imgur.com/kwsoUwk.png) 顧名思義為一個有向圖,圖上不會有環,各節點沒必要相連,也可以被視作為樹。 ### DAG 和 DP 的關係 如果將 DAG 圖上的節點看作是某個狀態 而箭頭從 A 指向 B,代表B為A的一個子問題 當我們滿足所有子問題之後,就能夠得到原問題的解了 因此 DAG 跟動態規劃看似無關,但不少問題都可以轉化為 DAG 問題了。 最常見的有三種問題: 1. 最長路徑 2. 最短路徑 3. 路徑計數問題 接下來就介紹幾個運用例題: ### 嵌套矩形問題 有n個矩形,每個矩形可以用兩個整數a,b描述表示長和寬。 矩形X(a,b)可以巢狀嵌套在矩形Y(c,d)中若且為若a<c, b<d 或 b<c, a<d(相當於把矩形X旋轉90度)。 例如(1,5)可以嵌套在(6,2)內,但不能嵌套在(3,4)內。你的任務是選出盡量多的矩形排成一航,使得除了最後一個之外,每一個矩形都可以嵌套在下一個矩形內 #### 解題分析: 矩形之間的「可嵌套」必定不會有雙向邊(大的不能套到小的),而且也不可能會有環,因此是一個典型的二元關係。那這種情況就可以建成一個DAG圖。 把每一個點當成一個矩形,而A指向B則代表A能夠嵌套B。在這樣的情況下,我們要求的就是DAG上的最長路徑。 ![](https://i.imgur.com/hC9vuB5.png) #### 實際作法: 我們可以去計算每個點的入度(即被幾個箭頭指向),如果為0的話就是無法再被嵌套了,即能夠得到該點的解答 #### 狀態轉移式: dp[點A能被嵌套的最大數量]=max(dp[子節點Xi能被嵌套的最大數量])+1, i=所有指向A的點 每次我們確定一個節點的最佳解時,就可以將其轉移到母問題,然後拔掉這個點還有相連的邊 這樣我們能逐漸地計算出每一個點的最佳解,最終得到答案。 ### 硬幣問題 有n種硬幣,面值分別為V~1~, V~2~, ..., V~n~,每種都有無限多。給定非負整數S,可以選用多少個硬幣,使得面值之和恰好為S? 輸出硬幣數目的最小值和最大值。(1≤n≤100, 0≤S≤10000, 1≤V~i~≤S) #### 解題分析: 雖然看上去和上一題很不一樣,但本題的本質也是DAG上的路徑問題。我們把每種面值看作一個點,表示「還需要湊足的面值」,則初始狀態為S,目標狀態為0。若目前在狀態i。每使用一個硬幣j,狀態便轉移到i−V~j~ ![](https://i.imgur.com/QmgNyCD.png) ## 快樂 DP 時間 ### 基礎題單 [Radio Commercials](https://open.kattis.com/problems/commercials) [Alphabet](https://open.kattis.com/problems/alphabet) [Walrus Weights](https://open.kattis.com/problems/walrusweights) [Knapsack](https://open.kattis.com/problems/knapsack) [Increasing Subsequence](https://open.kattis.com/problems/increasingsubsequence) [Nine Packs](https://open.kattis.com/problems/ninepacks) [Spiderman’s Workout](https://open.kattis.com/problems/spiderman) [Presidential Elections](https://open.kattis.com/problems/presidentialelections) [uva674 快樂找零錢](https://onlinejudge.org/index.php?option=onlinejudge&Itemid=8&page=show_problem&problem=615) [uva11351 約瑟夫問題](https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=2326) [TOJ80 game](https://toj.tfcis.org/oj/pro/80/) ### 進階題單 [Canonical Coin Systems](https://open.kattis.com/problems/canonical) [2021icpc台北站的i (題敘去右邊statment裡的題本找)](https://codeforces.com/gym/103443/problem/I) [Restaurant Orders](https://open.kattis.com/problems/orders) [leetcode446](https://leetcode.com/problems/arithmetic-slices-ii-subsequence/) [leetcode354(想要nlogn很難)](https://leetcode.com/problems/russian-doll-envelopes/)