# DP # DP DP(動態規劃),把會重複用到的值存取下來讓在做運算的時候可以更加的快速 ## 費氏數列 ### 遞迴 ```cpp int fib(int n) { if(n <= 1) return 1; return fib(n - 1) + fib(n - 2); } ``` 複雜度 $O(\phi^N)$ 可以觀察到,對於每一個$fib(N)$都需要重新算過一次,所以複雜度十分不友善 ### DP解 ```cpp int N; int dp[N + 1] = {0}; void fib(int n) { if(n == 0) {dp[0] = 1; return;} if(n == 1) {dp[1] = 1; return;} fib(n - 1); fib(n - 2); dp[n] = dp[n - 1] + dp[n - 2]; } ``` 也可以用for迴圈跑(速度較快) ```cpp int dp[N + 1]; dp[0] = dp[1] = 1; for(int i = 2; i <= N; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } ``` 複雜度 $O(N)$ 費氏數列其實可以壓到$O(log N)$,以後會提到 ## LIS最長遞增子序列 在解出$DP$之前第一步是要先列轉移式 { $$ \begin{cases} dp[i] = max(dp[i], dp[j] + 1) \quad &if \;\;ar[i] > ar[j]\\ dp[i] = max(dp[i], dp[j]) \quad &else \end{cases} $$ } ### DP 解 ```cpp= int dp[N], ar[N]; dp[0] = 1; for(int i = 1; i < N; i++) for(int j = 0; j < i; j++) dp[i] = max(dp[i], dp[j] + int(ar[i] > ar[j])); int mx = -1; for(auto &i : dp) mx = max(mx, i); ``` 時間複雜度$O(N^2)$ 空間複雜度$O(N)$ ### 二分搜解 時間複雜度 ```cpp= vector<int> dp; int ar[N]; dp.emplace_back(int(1e9)); for(auto &i : ar) { if(i > dp.back()) dp.emplace_back(i); else *lower_bound(dp.begin(), dp.end(), i) = i; } ``` 時間複雜度$O(NlogN)$ 空間複雜度$O(N)$ 最常遞增子序列也可以用$CDQ$分治去解,時間複雜度為$O(NlogN)$, 用$CDQ$解的優勢是可以快速地拓展到高維 ## LCS最常共同子序列 給予兩個字串$S, T$,問他們的最常共同子序列為何 ### 轉移式 { $$ \begin{cases} dp[i][j] = dp[i-1]dp[j-1] + 1 \quad &if \;\; S[i] == T[j] \\ dp[i][j] = max(dp[i-1][j], dp[j][i-1]) \quad &else \end{cases} $$ } ### dp解 ```cpp= string S, T; const int S_len = S.size(), T_len = T.size(); int dp[S_len + 1][T_len + 1]; for(int i = 1; i <= S_len; i++) { for(int j = 1; j <= T_len; j++) { if(S[i] == T[j]) dp[i][j] = dp[i-1][j-1] + 1; else dp[i][j] = max(dp[i-1][j], dp[j][i-1]); } } ``` ## 背包問題 ### 0/1背包問題 $dp[i][k]$代表在再放第`i`個物品的時候,當背包容量為`k`可放的最大值 $w[i]$第`i`個物品的重輛 $val[i]$第`i`個物品的價值 { $$ \begin{cases} dp[0][j] = 0\\ dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + val[i]) \end{cases} $$ } 就是對於每一個物品選擇要放或不放 然後要注意的是`j`在遞迴的時候要從$K$跑到$w[i]$,不然會重複取到 ```cpp= int dp[N + 1][K + 1]; //有N個物品,背包容量為K memset(dp[0], 0, sizeof(dp[0])); for(int i = 1; i <= N; i++) { for(int j = K; j >= w[i]; j--) { dp[i][j] = max(dp[i-1][j] + dp[i-1][j-w[i]] + val[i]); } } ``` ### 滾動DP 可以發現到,每一次被取到的都會只有`dp[i-1]`,所以其實只要去紀錄前一列`dp`的值就可以了 ```cpp= int dp[2][K+1]; memset(dp[0], 0, sizeof(x)); for(int i = 1; i <= N; i++) { for(int j = K; j >= w[i]; j--) { dp[i&1][j] = max(dp[~i&1][j], dp[~i&1][j-w[i]] + val[i]); } } ``` ### 無限背包問題 就是一件物品可以放無限多件在背包裡 剛剛有說如果$j$不從$K$往下跑的話會重複取到,剛剛好符合無限背包的條件,所以只又把$j$改成從$w[i]跑到K就可以了$ ```cpp= int dp[2][K+1]; memset(dp[0], 0, sizeof(dp[0])); for(int i = 1; i <= N; i++) { for(int j = w[i]; j <= K; j++) { dp[i&1][j] = max(dp[~i&1][j], dp[~i&1][j-w[i]] + val[i]); } } ``` ### 有限背包問題 那如果限定一個物品最多只能放$m$次呢,如果把它拆成**0/1背包**來做的話,時間複雜度會變成$O(mNK)$,顯然不太優秀,所以我們會把它拆成`2`的次方來去處理 時間複雜度$O(NKlogm)$ $cnt[i]為第i個物品可以放幾次$ ```cpp= int dp[2][K+1]; bool op = 1; memset(dp[0], 0, sizeof(dp[0])); for(int i = 1; i <= N; i++) { for(int j = 1; cnt[i] >= j;cnt[i] -= j, j <<= 1) { int weight = j * w[i], value = j * val[i]; for(int k = K; k >= weight; k--) dp[op][k] = max(dp[~op][k], dp[~op][k-weight] + value); op ^= 1; } int weight = cnt[i] * w[i], value = cnt[i] * val[i]; for(int k = K; k >= w[i]; k--) { dp[op][k] = max(dp[~op][k], dp[~op][k-weight] + value); } if(cnt[i]) op ^= 1; } ```` 有限背包問題也可以用單調對列壓成$O(NK)$,以後會提到 以上為基本的$DP$,接下來會進到更高深的$DP$ ## 矩陣DP 簡單來說就是對於一些轉移是是用加或減,轉移是是固定取某些的$DP$,透過一定的規則來把轉移式換成矩陣,再利用`快速冪`來達到$O(log N)$級別的演算法 ### 費氏數列 { $$ \begin{cases} dp_0 = dp_1 = 1\\ dp_n = dp_{n-1} + dp_{n-2} \quad n >=2 \end{cases} $$ } 轉換成 { $$ \left[ \begin{array}{c} a_1 & a_0 \end{array} \right] \times \left[ \begin{array}{cc} 1 & 1 \\ 1 & 0 \end{array} \right] ^N $$ } ```cpp= struct matrix { V<V<int>> mat; } ``` ## 區間DP 就是用$DP$去維護一個區間的值,最經典的題目是給予$N$個矩陣,求將他們依序相乘所需花費的最小計算量 $dp[i][j]是第[i, j]中的矩陣相乘所需要話費的最小計算量$ $ar[i] 為第i個矩陣的大小 col \times row,$ { $$ dp[i][j] = \begin{cases} 1 \quad &if \; i = j \\\\ max(\\ \quad dp[i+1][j] + (ar[i].col \times ar[i].row) \times ar[j].col), &else\\ \quad dp[i][j-1] + (ar[i].col \times (ar[j].col * ar[j].row)\\ ) \end{cases} $$ } ## 狀態壓縮 有時候在做$DP$的時候,不只需要考慮一種狀態,可能同時會有許多種狀態,狀態壓縮就是把需要用到的狀態壓成以二進位來表示,如果有$N$個條件需要考慮($0$或$1$),就會需要用到$2^N$個狀態 ### 旅行售貨員問題 ## 單調對列 對於特定的$DP$來說,他的答案可能會有$單調性$,也就是答案會是遞增的。 這時候就可以使用`單調對列`,就是在做$DP$的過程中,在維護最大值的情況下把不可能會用到的刪掉,大多都會用`deque`來去維護,可以達到$O(N)$的複雜度 ## 斜率優化 雖然單調對列看起來已經十分的實用,但是對於一些後面的值會出現在前面的$dp$就不管用了,於是就出現了斜率優化,藉由對$dp$式子進行一些數學的簡化,來讓我們可以更好的去解決他 ### 凸包 即給予多個點,招出一個凸多邊形他的頂點解由給予的點組成,且裡面包含所有的點 ### 李超線段樹 ## 自己出題目