DP[0]
algorithm[7] 22527 Brine
我是誰
22527 鄭竣陽
Brine
BrineTW#7355
你是不是忘記我是誰了ㄏㄏ
Index
Divide & Conquer
分治
這節不是 DP 嗎
- 因為我們電研大學術長 \(\text{AaW}\) 燒雞了
- 所以這節課會先講一點點的分治
分治的本質
- 做為一個懶惰的人,手上的事情當然是越少事情越好對吧
- 所以分治要做的事情就是:
- 把當前問題分成很多和現在問題具相同性質的子問題
- 把子問題丟給別人解決
- 把解決的子問題整合後變成當前問題的答案
- 聽起來怪怪的?
- 子問題怎麼解決?
- 有沒有發現這跟寫遞迴的時候一模一樣?
- 設定「邊界條件」
- 當問題規模足夠小時,不再分割問題,而是暴力求解!
- 子問題怎麼解決?
排序演算法
- 各位都到這個地步了,應該都會排序了吧
- 現在,來試試看不要只會用 STL 裡面的 sort() 吧
- 要如何從無到有生出一個排序演算法呢?
- 試試看分治的想法,太難的事做不起就不要做!
- 若把問題簡化為「將兩個已排好的陣列合併」呢
- 我們現在已經會合併問題了!
- 再來就要想要怎麼分割問題
- 把陣列從中間切成一半!
合併排序
- 我們剛才做出了所謂的 mergesort 了!
- 整理過後的步驟就是:
- 把陣列分割成兩半
- 將兩邊的陣列排序
- 將兩個陣列合併
- 什麼時候要停下來?
- 什麼時候不用排序陣列?
- 當陣列的大小為 1 的時候
- 遞迴的邊界條件出來了!
Mergesort Code
- 現在看得懂遞迴還有我的
毒瘤語法了嗎
typedef vector<int>::iterator It;
void mergesort(It l, It r) {
if (r - l == 1) return;
It m = l + (r - l >> 1);
mergesort(l, m), mergesort(m, r);
vector<int> a(l, m), b(m, r);
a.push_back(INT32_MAX), b.push_back(INT32_MAX);
It afront = a.begin(), bfront = b.begin();
for (It it = l; it < r; ++it) {
*it = (*afront <= *bfront ? *afront++ : *bfront++);
}
}
沒有示意圖?!
這種東西應該自己去學習,你都幾歲了- 我今天又不是來講分治的,饒了我吧
複雜度分析
- 我們剛才的演算法可以成功的排序好一個陣列
- 這樣就滿足了嗎?
- 他的複雜度會是什麼樣子?
- 分別討論兩件事情
- 合併
- 合併大小為 \(n\) 的陣列的時候需要 \(\mathcal O(n)\) 的時間
- 每層合併時各個被分割的子陣列大小和為 \(n\)
- 遞迴
- 每層將陣列大小 \(n\) 砍半
- 需要砍 \(\lceil \log_2 n \rceil \) 次才能砍完
- 合併
- 複雜度屬於 \(\mathcal O(n\log n)\) !
酷酷的小細節
- 一直遞迴其實很浪費時間和效能
- 在 \(n\) 夠小時 \(\mathcal O(n^2)\) 其實不會跟 \(\mathcal O(n \log n)\) 差太多
- 決勝的細節在常數!
- 當 \(n\) 太小時直接 \(\mathcal O(n^2)\) 的排序會比 mergesort 到底好
- 這個神奇數字 \(n'\) 是多少呢
- 鬼才知道
- 自己找
主定理
- Master theorem
- 分治演算法複雜度計算的公式解
- 令 \(T(n)\) 為演算法執行大小為 \(n\) 的測資的步驟數
T(n) =
\begin{cases}
c &\quad\text{if } n = 1\\
aT(\frac{n}{b}) + cn^d &\quad\text{if } n > 1\\
\end{cases}
- 把 \(d\) 和 \(\log_b a\) 比大小!
T(n) \in
\begin{cases}
\Theta(n^d) &\quad\text{if } \log_b a < d\\
\Theta(n^d \log n) &\quad\text{if } \log_b a = d\\
\Theta(n^{\log_b a}) &\quad\text{if } \log_b a > d\\
\end{cases}
誰在乎 \(\Theta(f(n))\) 阿
- \(\mathcal O(f(n))\) 只看函數的上界是否能被限制
- \(\Theta(f(n))\) 還要看下界
- 若 \(g(n) \in \Theta(f(n))\),則:
- 存在正數 \(k_l, k_u, n_0\) 使得:
- \(\forall n > n_0: 0 \le k_l \times g(n) \le f(n) \le k_u \times g(n)\)
可以用就好
分治法求 \(a^n\ ( \text{mod }m)\)
- 直接暴力做的複雜度是多少?
- \(\mathcal O(n)\)
- 很糟糕嗎
- 如果你要一直做就會很糟糕
- 我們不訪把問題切割成兩個小問題
- \(\because x\ (\text{mod } m) \times y\ (\text{mod } m) \equiv xy\ (\text{mod } m)\)
\therefore a^n\ (\text{mod } m) \equiv a^{\lfloor \frac{n}{2} \rfloor}\ (\text{mod } m) \times a^{\lceil \frac{n}{2} \rceil}\ (\text{mod } m)
T(n) =
\begin{cases}
c &\quad\text{if}\ \ n = 1\\
2T(\frac{n}{2}) + c &\quad\text{if}\ \ n > 1\\
\end{cases}
\log_2 2 = 1
T(n) \in \Theta(n^{\log_2 2}) = \Theta(n)?!
這樣沒有比較快阿
- 如何切割和合併子問題是很重要的
a^8
a^2
a^1
a^4
a^1
a^2
a^1
a^1
a^2
a^1
a^1
a^2
a^1
a^1
a^4
小改動
a^n =
\begin{cases}
a^{\lfloor \frac{n}{2} \rfloor} \times a^{\lfloor \frac{n}{2} \rfloor} &\quad\text{if} \quad n \equiv 0 \text{ mod }2\\
a^{\lfloor \frac{n}{2} \rfloor} \times a^{\lfloor \frac{n}{2} \rfloor} \times a &\quad\text{if} \quad n \equiv 1 \text{ mod }2\\
\end{cases}
T(n) =
\begin{cases}
c &\quad\text{if}\ \ n = 1\\
T(\frac{n}{2}) + c &\quad\text{if}\ \ n > 1\\
\end{cases}
\log_2 1 = 0 = d
T(n) \in \Theta(n^d \log n) = \Theta(n^0 \log n) = \Theta(\log n)
typedef long long ll;
const int M = 1e9 + 225;
int power(ll a, int n) {
if (n == 1) return a;
ll half = power(a, n / 2);
ll full = half * half % M;
if (n % 2 == 0) return full;
return full * a % M;
}
Exponentiation by squaring
- 遞迴式
可以不要分治做嗎
- 可以
- \(a^{(19)_{10}} = a^{(10011)_{2}}\)
- \(a^2 = a^1 \times a^1\)
- \(a^4 = a^2 \times a^2\)
- \(a^8 = a^4 \times a^4\)
Exponentiation by squaring
- 迭代式
- 可以用位元運算
typedef long long ll;
const int M = 1e9 + 225;
int powerBySquaring(int a, int n) {
ll current = a;
ll answer = 1;
while (n > 0) {
if (n % 2 == 1) answer = answer * current % M;
current = current * current % M;
n /= 2;
}
}
分治無所不在
例題
其他資料
Dynamic Programming
動態規劃
來點例題
- \(\text{Fibonacci's sequence}:\)
- \(f_0 = 0, f_1 = 1\)
- \(\forall n > 1: f_n = f_{n - 1} + f_{n - 2}\)
- 求 \(f_n\)
- 來直接遞迴吧
直接遞迴 code
- 複雜度?
int f(int n) {
if (n < 2) return n;
return f(n - 1) + f(n - 2);
}
f_3
f_2
f_1
f_1
f_0
複雜度
- \(f_n\) 會呼叫兩次 \(f\)
- \(n\) 每次最少減少 \(1\)
- 複雜度 \(\mathcal O(2^n)\)
- 蛤
- 其實他應該是 \(\mathcal O(\phi^n)\) 但我不會證
- 反正他就是指數時間演算法
問題在哪裡
- 跟剛才算 \(a^n\) 的時候,我們做了什麼
- 把 \(a^{\lfloor \frac{n}{2} \rfloor}\) 記下來
- 這個動作的核心理念是?
- 做過的事情不要再做一次
- 這就是動態規劃的精隨
- 好好記錄,把 \(\mathcal O(\phi^n)\) 變成 \(\mathcal O(n)\)!
Fibonacci code
- top-down
vector<int> dp(30, -1);
int f(int n) {
if (dp[n] >= 0) return dp[n];
return dp[n] = f(n - 1) + f(n - 2);
}
int main() {
dp[0] = 0;
dp[1] = 1;
int q;
while (cin >> q) cout << f(q) << '\n';
}
Fibonacci code
- bottom-up
vector<int> dp(30);
int main() {
dp[1] = 1;
for (int i = 2; i < dp.size(); i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
int q;
while (cin >> q) cout << f[q] << '\n';
}
怎麼決定怎麼做
- 什麼時候要使用 bottom-up,什麼時候要用 top-down?
- 一定都是對的!
- 但是為了不要蓋過一些東西可能需要加上一些有的沒有的判定
- 會影響寫出來的難易程度
- 請自行評估
什麼是 DP
- 分治的時候多記錄一點東西
- 要能夠 dp,問題必須要有:
- 重複子問題 overlapping subproblem
- 重複計算到多個一樣的子問題
- 例:計算 \(f_9\) 時會算到 \(f_4\) 很多次
- 最佳子結構 optimal substructure
- 問題的最佳解之子問題的解亦為最佳解
- 可分治性
- 重複子問題 overlapping subproblem
DP 演算法需要什麼
- 定義狀態
- 轉移(遞迴)式
- 邊界條件
- 來點例子吧!
CSES 1633 骰子組合
- 看得懂題目嗎?
- 骰任意次骰子,骰出 \(n\) 點的可能性有幾種?
- \(n \le 10^6\),看起來得要 \(\mathcal O(n)\) 做完
- 來試試看 dp!
- 定義狀態:
- 令 \(\text{dp}[i] =\) 點數和為 \(i\) 的方法數
- 轉移式:
- \(\text{dp}[i] = \displaystyle \sum_{n = i - 6}^{i - 1} \text{dp}[n]\)
-
邊界條件:
- \(\text{dp}[0] = 1, \forall i < 0 : \text{dp}[i] = 0\)
- 定義狀態:
試試看!
- 實作時間!
- 記得取模!
DP 的複雜度?
- dp 時,通常就是:
- 遍歷每個狀態
- 將資訊轉移至該狀態
- 複雜度:\(\mathcal O(狀態數 \times 轉移時間)\)
- 剛剛的例子
- \(\mathcal O(n \times 6) = \mathcal O(6n) = \mathcal O(n)\)
DP Classics
動態規劃經典題
Maximum Consecutive Sum
最大連續和
最大連續和
- 給定一長度為 \(n\) 的整數陣列 \(a\),求一個連續區間和的最大值
- \(n \le 500\)
- 窮舉區間左右界 \(l, r\),計算區間和,比較並選出最大值
- 複雜度 \(\mathcal O(n^3)\)
-
\(n \le 10000\)
- 有什麼東西是重複算到的?
- 區間和!
- 先計算陣列的前綴和 \(\text{sum[i]}=\displaystyle \sum^{i}_{j = 1} a_j\),再窮舉左右界 \(l, r\)
- 查詢只要拿 \(\text{sum}[r] - \text{sum}[l - 1]\),\(\mathcal O(1)\)!
- 複雜度 \(\mathcal O(n^2\))!
- \(n \le 500\)
前綴和也算一種 DP
- 令 \(\text{dp}[i] = \displaystyle \sum_{j = 1}^{i} a_j\)
- 轉移式:\(\text{dp}[i] = dp[i-1] + a_i\)
- 邊界條件:\(\text{dp}[0] = 0\)
- 聽起來很唬爛但這是真的
-
前綴和還有很多應用
- 和好朋友差分一起可以做很多事情
-
什麼是差分?
- \(\text{difference}\)
- \(\text{dif}[i] = a_i - a_{i-1},\ \text{dif}[1] = a_1\)
- 差分陣列的前綴和陣列為原陣列,反之亦然
- 給你 \(n\) 條線段,有 \(k\) 線段覆蓋的區間有多少個?
快,還要更快
- \(n \le 10^6\)
- 注意到在 \(\text{sum}[r]\) 相同時,\(\text{sum}[l-1]\) 越小越好
- 每次提取最小的 \(\text{sum}[l-1]\)
- 用 priority queue 實作
- 複雜度 \(\mathcal O(n \log n)\)
- \(n \le 10^7\)
- 你要先確定你可以吃完輸入欸
- 這麼大的東西可能會用別的方法給你輸入(亂數種子之類的)
- 連帶 \(\log\) 都不行,怎麼辦
- 我們剛才做了哪些「不必要的事」?
敏銳的觀察
- 何必一直提取呢
- 令 \(\text{dp}[i]\) 為前 \(i\) 項的最小 \(\text{sum}[i]\)
- 轉移式 \(\text{dp}[i] = \min(\text{dp}[i-1], \text{sum}[i])\)
- 邊界條件 \(\text{dp}[0] = 0\)
- 這聽起來很不演算法欸(?)
- 另外一種想法:
- 另 \(\text{dp}[i]\) 為以 \(a_i\) 為結尾的最大連續和
- 轉移式 \(\text{dp}[i] = \max(0, \text{dp}[i - 1]) + a_i\)
- 為什麼要長這樣?
- 如果要從前面連續不好的話乾脆從我開始!
- 邊界條件 \(\text{dp}[0] = 0\)
Paths on grids
網格上路徑數量
走路
- 一個笛卡爾座標系兩點間走捷徑有幾種路徑?
- 規定只能在整數點間移動
- 每次移動距離為 \(1\)
-
蛤
- 笛卡爾座標系 \(\text{aka}\) 直角座標系
- 捷徑:路徑長最短的路徑
- 如果我說這題可以 \(\mathcal O(\Delta x + \Delta y)\) 算出來你會相信嗎?
- 數學!
- 那如果座標點上有障礙物呢?
把問題化簡
- 不失一般性,把問題化簡為從左上到右下的樣子
- 令起點座標 \(=(0, 0)\);終點座標 \(=(\Delta x, \Delta y)\)
起點 | ||||||||
---|---|---|---|---|---|---|---|---|
卡 | 卡 | |||||||
卡 | ||||||||
終點 |
觀察性質
- 想要離終點越來越近,勢必不能往上或往左走
- 只能從左邊或上面到達自己
- 令 \(\text{dp}[i][j]\) 為從 \((0, 0)\) 走到 \((i, j)\) 的方法數
- 轉移式 \(\text{dp}[i][j] = \text{dp}[i-1][j] + \text{dp}[i][j-1]\)
- 邊界條件
- 迴圈的順序非常重要!
\text{dp}[0][0] = \text{!blocked}[0][0]
Longest Common Subsequence
最長共同子序列
最長共同子序列
- Longest Common Subsequence
- \(\text{LCS}("AawSoDian", "BrineSoNotDian") = "SoDian"\)
- 給定字串 \(s_a, s_b\),求 \(|\text{LCS}(s_a, s_b)|\)
- 令 \(\text{dp}[i][j] = \text{LCS}(s_a[0:i], s_b[0:j])\)
- 狀態轉移式?
- \(s_a[i], s_b[j]\) 皆為 LCS 的一部分
- \(\text{dp}[i][j] = \text{dp}[i-1][j-1]+1\)
-
\(s_a[i], s_b[j]\) 不皆為 LCS 的一部分
- \(\text{dp}[i][j] = \max(\text{dp}[i][j-1], \text{dp}[i-1][j])\)
- \(s_a[i], s_b[j]\) 皆為 LCS 的一部分
是哪個 case?
- 只有在 \(s_a[i] = s_b[j]\) 時用第一種轉移就好
- why?
- \(\text{dp}[i][j-1] \ge \text{dp}[i-1][j-1]\)
- \(\text{dp}[i-1][j] \ge \text{dp}[i-1][j-1]\)
- 沒有 \(+1\) 就沒有必要用 \(\text{dp}[i-1][j-1]\) 了
- why?
- 邊界條件 \(\text{dp}[0][0] = 0\)?
- 算 \(\text{dp}[3][0]\) 直接去世
- \(\forall i = 0 \lor j = 0: \text{dp}[i][j] = 0\)