# [AIdrifter CS 浮生筆錄](https://hackmd.io/@iST40ExoQtubds5LhuuaAw/rypeUnYSb?type=view#Competitive-Programmer%E2%80%99s-Handbook) <br> Competitive Programmer’s Handbook <br> Ch7: Dynamic Programming # Dynamic Programming **Dynamic programming** is a technique that combines the correctness of **complete search** and the efficiency of **greedy algorithms**. Dynamic programming can be applied if the problem can be divided into overlapping subproblems that can be solved independently. :::info DP = Complete Search + Greedy algotithms ::: 仔細想想蠻有道理的,Greedy Algorithms只要湊到最終解就好,但是DP透過**建表**會把每個子問題(subproblems)答案都算出來。 There are two uses for dynamic programming: - **Finding an optimal solution**: 找到最佳子問題的解 We want to find a solution that is as large as possible or as small as possible. - **Counting the number of solutions** 算出全部的解,這邊我覺得改成最佳解集合包含最佳子問題必較好。(Ref: Cormen We want to calculate the total number of possible solutions. 如果要把程式競賽當職業,DP會是一個里程碑。 Understanding dynamic programming is a **milestone** in every competitive programmer's career. While the basic idea is simple, the challenge is how to apply dynamic programming to different problems. ## Coin Problem 可以先參考第六章的 : [Greedy Algorithms : Coin Problem](https://hackmd.io/gesAEePtTvOEFyGSYG8aRA#Coin-Problem) 題目: Out task is to form the sum $n$ using as few coins as possible. 給一組硬幣 $\{1,2,5,10,20,50,100,200\}$ ,若$n=520$,如何找出最少的硬幣組成? 硬幣可以重複選取。 Ans: $n = 520 = 200+200+100+20$ DP的本質是brute force(complete asearch),但是因為它採用表格欄位去紀錄答案(memorization),所以每個subproblem只要算一次就好。 The dynamic programming algorithm is efficient because it uses ${memoization}$ and calculates the answer to each subproblem only once. ### Recursive Formuation For example, if $\texttt{coins} = \{1,3,4\}$, the first values of the function are as : $\texttt{solve}(10)=3$, because at least 3 coins are needed to form the sum 10. The optimal solution is $3+3+4=10$. \begin{array}{lcl} \texttt{solve}(0) & = & 0 \\ \texttt{solve}(1) & = & 1 \\ \texttt{solve}(2) & = & 2 \\ \texttt{solve}(3) & = & 1 \\ \texttt{solve}(4) & = & 1 \\ \texttt{solve}(5) & = & 2 \\ \texttt{solve}(6) & = & 2 \\ \texttt{solve}(7) & = & 2 \\ \texttt{solve}(8) & = & 2 \\ \texttt{solve}(9) & = & 3 \\ \texttt{solve}(10) & = & 3 \\ \end{array} 因此我們可以依據條件寫成等式 : \begin{equation*} \begin{split} \texttt{solve}(x) = \min( & \texttt{solve}(x-1)+1, \\ & \texttt{solve}(x-3)+1, \\ & \texttt{solve}(x-4)+1). \end{split} \end{equation*} 擴展成通式(General Recursive Function) \begin{equation*} \texttt{solve}(x) = \begin{cases} \infty & x < 0\\ 0 & x = 0\\ \min_{c \in \texttt{coins}} \texttt{solve}(x-c)+1 & x > 0 \\ \end{cases} \end{equation*} ```cpp int solve(int x){ if(x<0) return INF; // infinity if(x==0) return 0; int best = INF; for(auto c:C){ best = min(best, solve(x-c)+1); } return best; } ``` ### Using Memoization $O(nK)$ There may be an exponential nunmber of ways to construct the sum. 剛剛上面方法會花掉exponential time去計算$x$,我們利用$memoization$把小問題的解存起來,不用每次都重算。 ```cpp bool ready[N] = {0}; int value[N] = {0}; ``` 只要多這三行: ```cpp if(ready[x]) return value[x]; value[x] = best; ready[x] = true; ``` 修改後, **recursion & Memoziation**採用**top-down**方式建構答案。 ```cpp int solve(int x){ if(x<0) return INF; if(x==0) return 0; if(ready[x]) return value[x]; // if slove(x) is ready, stop to compute it. int best = INF; for(auto c:C){ best = min(best, solve(x-c)+1); } // save subproblem solution of slove(x) value[x] = best; ready[x] = true; return best; } ``` **DP or Iterative** 採用**bottom up**方式建構答案。 ```cpp value[0] = 0; // caculate slove(x) from 1 to n for (int x = 1; x <= n; x++) { value[x] = INF; for (auto c : coins) { // check index boundary if (x-c >= 0) { // don't take c take c value[x] = min(value[x], value[x-c]+1); } } } ``` :::info 小作業: INF vs INT_MAX 差異在哪邊? ::: ### Constructing A Solution 有時候題目要求,印出 $solve(10) = 4+4+3$,我們要如何做呢? Ans: 我們用一個array去記錄每個最佳子問題的第一個硬幣是誰。 照貪婪演算法,第一個硬幣也會是最大的硬幣,所以湊起來就是最佳解的全部硬幣組合。 ```cpp int first[N]; ``` 把`value[x-c]+1 < value[x]`這條件從`min()`搬到`if()`上面 ```cpp value[0] = 0; for (int x = 1; x <= n; x++) { value[x] = INF; for (auto c : coins) { // value[x-c]+1 < value[x] if (x-c >= 0 && value[x-c]+1 < value[x]) { value[x] = value[x-c]+1; // first[x] = c; } } } ``` 用這方式去印答案: ```cpp while (n > 0) { cout << first[n] << "\n"; n -= first[n]; } ``` ### Couting the number of Solutions 和前面不一樣的是,這次要算出solve(x)有多少種硬幣組合方式,solve(5)為例 : ![](https://i.imgur.com/JOrXHdq.png) Let $\texttt{solve}(x)$ denote the number of ways we can form the sum $x$. For example, if $\texttt{coins}=\{1,3,4\}$, then $\texttt{solve}(5)=6$ and the recursive formula is \begin{equation*} \begin{split} \texttt{solve}(x) = & \texttt{solve}(x-1) + \\ & \texttt{solve}(x-3) + \\ & \texttt{solve}(x-4) . \end{split} \end{equation*} 通式如下,要記住**不放**也算一種方法: There is only one way to form an empty sum \begin{equation*} \texttt{solve}(x) = \begin{cases} 0 & x < 0\\ 1 & x = 0\\ \sum_{c \in \texttt{coins}} \texttt{solve}(x-c) & x > 0 \\ \end{cases} \end{equation*} ```cpp // There is only one way to form an empty sum count[0] = 1; int solve(x){ for (int x=1; x<=n ; x++){ for (auto c : Coins){ if (x-c >= 0) count[x] += count[x-c]; } } } ``` 有時候我們不想算出全部solve(x)的解,我們可以運用模數運算(modulo)去化簡: Often the number of solutions is so large that it is not required to calculate the exact number but it is enough to give the answer modulo $m$ where, for example, $m=10^9+7$. This can be done by changing the code so that all calculations are done modulo $m$. In the above code, it **suffices** to add the line 舉例來說 $m=10^9+7$. 把這步加在`count[x] += count[x-c]` 之後。 ```cpp count[x] += count[x-c]; count[x] %= m; ``` Ref: [Why we like Modulo 10^9+7 (1000000007)](https://www.geeksforgeeks.org/modulo-1097-1000000007/) :::info 這邊之後找個範例補充一下,霧煞煞。 ::: ## Longest increasing subsequence $O(n^2)$ 找出不連續的最長遞增子序列。 This is a maximum-length sequence of array elements that goes from left to right, and each element in the sequence is larger than the previous element. For example, in the array ![](https://i.imgur.com/ES5APlK.png) ![](https://i.imgur.com/SxF7fXj.png) Let ${length}(k)$ denote the length of the longest increasing subsequence that ends at position $k$ ![](https://i.imgur.com/Abf71PB.png) - length(k) = max(lenght(k), length[i]+1) - 如果A[K] > A[i] => `length[i] + 1 `(加入k元素) vs `length[k]` (不選k元素,因為沒有變大) ```cpp for (int k = 0; k < n; k++) { length[k] = 1; // at least one for (int i = 0; i < k; i++) { if (array[i] < array[k]) { length[k] = max(length[k],length[i]+1); } } } ``` :::info It is also possible to implement the dynamic programming calculation more efficiently in $O(n \log n)$ time. Can you find a way to do this? ::: ## Paths in grid $O(n^2)$ 從左上走到右下,盡量得到最大的數字加總。 we only move down and right. Each square contains a positive integer, and the path should be constructed so that the sum of the values along the path is as large as possible. ![](https://i.imgur.com/EPId8jF.png) $\texttt{sum}(y,x)$ 為該square的路徑加總的最大總和。 Let $\texttt{sum}(y,x)$ denote the maximum sum on a path from the upper-left corner to square $(y,x)$. ![](https://i.imgur.com/3snPKmT.png) 通式如下: $\texttt{sum}(y,x) = \max(\texttt{sum}(y,x-1),\texttt{sum}(y-1,x))+\texttt{value}[y][x]$ if $\texttt{sum}(y,x) = 0$ , $x=0$ or $y=0$ // 因為這種path不存在,走不到。 ```cpp for (int y = 1; y <= n; y++) { for (int x = 1; x <= n; x++) { sum[y][x] = max(sum[y][x-1],sum[y-1][x])+value[y][x]; } } ``` ## Knapsack Problem $O(nW)$ 題目:有辦法用$[w_1,w_2,\ldots,w_n]$ n件物品,組合成重量x嗎? Problem: Given a list of weights $[w_1,w_2,\ldots,w_n]$, $W = w_1+w_2+\ldots,w_n$ determine all sums that can be constructed using the weights. For example, if the weights are $[1,3,3,5]$, the following sums are possible: ![](https://i.imgur.com/AwX1pqH.png) 定義 $\texttt{possible}(x,k)$,如果可以用第$k$個物品組成重量$x$ 即為$true$ Let $\texttt{possible}(x,k)=\textrm{true}$ if we can construct a sum $x$ using the first $k$ weights, and otherwise $\texttt{possible}(x,k)=\textrm{false}$. 等式如下: - $\texttt{possible}(x,k) = \texttt{possible}(x-w_k,k-1) \lor \texttt{possible}(x,k-1)$ - $\texttt{possible}(x-w_k,k-1)$ : 拿第k個物品。 - $\texttt{possible}(x,k-1)$ : 不拿第k個物品。 先用這條件去初始化表格第一列(橘色): \begin{equation*} \texttt{possible}(x,0) = \begin{cases} \textrm{true} & x = 0\\ \textrm{false} & x \neq 0 \\ \end{cases} \end{equation*} ![](https://i.imgur.com/oFcwPKL.png) ```cpp possible[0][0] = true; for (int k = 1; k <= n; k++) { for (int x = 0; x <= W; x++) { if (x-w[k] >= 0) possible[x][k] |= possible[x-w[k]][k-1]; possible[x][k] |= possible[x][k-1]; } } ``` 這邊提供一種one-dimensional做法,從右到左update。 However, There is a better implementation that only uses a one-dimensional array $\texttt{possible}[x]$ that indicates whether we can construct a subset with sum $x$. The trick is to update the array **from right to left** for each new weight: ```shell {1,3,3,5} 0 1 2 3 4 5 6 7 8 9 10 11 12 X k=1 {1} X X k=2 {1,3} X X X X k=3 {1,3,3} X X X X X X k=4 {1,3,3,5} X X X X X X X X X X X ``` ```cpp possible[0] = true; for (int k = 1; k <= n; k++) { for (int x = W; x >= 0; x--) { if (possible[x]) possible[x+w[k]] = true; } } ``` ### $01$ Knapsack Problem $O(nm)$ 總共有n件物品,重量從$W1....Wn$,對應的價錢是$V1....Vn$ 經典的$0/1$背包問題。 ![](https://i.imgur.com/IaLGOV1.png) ## Edit distance $O(nm)$ **萊文斯坦距離**,又稱Levenshtein距離,是編輯距離的一種。 指兩個字串之間,由一個轉成另一個所需的最少編輯操作次數。 The ${edit distance}$ or ${Levenshtein distance}$ is the **minimum number** of editing operations needed to transform a string - insert a character (e.g. ABC→ABC**A**) - remove a character (e.g. A**B**C→AC) - modify a character (e.g. A**B**C→A**D**C) $\texttt{distance}(a,b)$ 為字串長度$a$ 和字串長度$b$下的最短萊文斯坦距離。 Suppose that we are given a string $\texttt{x}$ of length $n$ and a string $\texttt{y}$ of length $m$, and we want to calculate the edit distance between $\texttt{x}$ and $\texttt{y}$. define a function $\texttt{distance}(a,b)$ that gives the edit distance between prefixes $\texttt{x}[0 \ldots a]$ and $\texttt{y}[0 \ldots b]$. Thus, using this function, the edit distance between $\texttt{x}$ and $\texttt{y}$ equals $\texttt{distance}(n-1,m-1)$. **通式如下:** \begin{equation*} \begin{split} \texttt{distance}(a,b) = \min(& \texttt{distance}(a,b-1)+1, \\ & \texttt{distance}(a-1,b)+1, \\ & \texttt{distance}(a-1,b-1)+\texttt{cost}(a,b)). \end{split} \end{equation*} Here $\texttt{cost}(a,b)=0$ if $\texttt{x}[a]=\texttt{y}[b]$, and otherwise $\texttt{cost}(a,b)=1$. The formula considers the following ways to edit the string $\texttt{x}$: - (e.g. ABC→ABC**A**) 拿掉$b$內的**A**就和$a$一樣了。 $\texttt{distance}(a,b-1)$: insert a character at the end of $\texttt{x}$ - (e.g. A**B**C→AC) 拿掉$a$內的**B**就和$b$一樣了。 $\texttt{distance}(a-1,b)$: remove the last character from $\texttt{x}$ - (e.g. A**B**C→A**D**C) 各拿掉$a$內的**B** 和 $b$內的**D**就都一樣了。 $\texttt{distance}(a-1,b-1)$: match or modify the last character of $\texttt{x}$ ![](https://i.imgur.com/fou9e2k.png) The lower-right corner of the table tells us that the edit distance between $\texttt{LOVE}$ and $\texttt{MOVIE}$ is 2. 還有因為轉折點在$I$ 和 $V$之間,我們可以知道$MOVI$ 和 $LOV$ 兩字串中, 只要從 $MOVI$ 這邊移掉 $I$ 後, 就是**萊文斯坦距離**差 $1$ 的 $MOV$ 和 $LOV$。 ![](https://i.imgur.com/1bGMadZ.png) ```cpp class Solution { public: int minDistance(string word1, string word2) { int m = word1.size(), n = word2.size(); vector<vector<int>> dp(m+1, vector<int>(n+1, 0)); for(int j=1; j<=n; j++) dp[0][j] = j; int cost = 0; for(int i=1; i<=m; i++) { dp[i][0] = i; for(int j=1; j<=n; j++) { word1[i-1]!=word2[j-1]? cost=1:cost=0; dp[i][j] = min(min(dp[i-1][j]+1, dp[i][j-1]+1), dp[i-1][j-1]+cost); } } return dp[m][n]; } }; ``` Ref: [LeetCode] Edit Distance http://bangbingsyb.blogspot.com/2014/11/leetcode-edit-distance.html ## Counting tilings 又稱**Domino Tiling**,二格骨牌(Domino)。 找出 $n \times m$ 的grid可以放幾種的Domino ? ![](https://i.imgur.com/V7L01tx.png) [Pic]:one valid solution for the 4×7 grid ![](https://i.imgur.com/au2KMin.png) 其實是有數學公式的: $\ \prod_{a=1}^{\lceil n/2 \rceil} \prod_{b=1}^{\lceil m/2 \rceil} 4 \cdot (\cos^2 \frac{\pi a}{n + 1} + \cos^2 \frac{\pi b}{m+1})$ ## Conclusion - Optimal substructure - overloapping subproblem - 2^n(exponetial) > n^k(polynomial) - 如果不考慮overlap就是divde and conquer - No after affect - the optimal solution of a subproblem will not change when it was used to solve a bigger problem optimally. ```shell 大->小 (Top Down) 大->小(Bottom up) Recursive -> recursion & Memoziation -> Dynamic Programming 不需要考慮(找mem) 要確定會不會重複算 ``` - DP精神:要考慮如何處理overlaping 沒有的話就是單純的divide and conquer(有overlap但不鳥他) - DP = recursion + re-use - Reference: [Difference between Divide and Conquer Algo and Dynamic Programming](https://stackoverflow.com/questions/13538459/difference-between-divide-and-conquer-algo-and-dynamic-programming)