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;
    }
}

分治無所不在

  • 分治可以做到:
    • 樹狀陣列 BIT
    • 線段樹 Segment Tree
    • 最低共祖 LCA
    • 多項式乘法 by Karatsuba
    • 矩陣乘法 by Strassen
    • 快速傅立葉轉換 FFT
    • ...

例題

其他資料

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
      • 問題的最佳解之子問題的解亦為最佳解
      • 可分治性

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\))!

前綴和也算一種 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])\)

是哪個 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]\) 了
  • 邊界條件 \(\text{dp}[0][0] = 0\)?
    • 算 \(\text{dp}[3][0]\) 直接去世
    • \(\forall i = 0 \lor j = 0: \text{dp}[i][j] = 0\)

例題

建北電資 27th 演算法[7] DP[0]

By Brine

建北電資 27th 演算法[7] DP[0]

演算法[7]

  • 485