DP[0]

algorithm[7] 22527 Brine

我是誰

22527 鄭竣陽
Brine
BrineTW#7355
  • 你是不是忘記我是誰了ㄏㄏ
Index
Divide & Conquer

分治

這節不是 DP

  • 因為我們電研大學術長 AaW\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++);
    }
}

沒有示意圖?!

  • 這種東西應該自己去學習,你都幾歲了
  • 我今天又不是來講分治的,饒了我吧

複雜度分析

  • 我們剛才的演算法可以成功的排序好一個陣列
  • 這樣就滿足了嗎?
  • 他的複雜度會是什麼樣子?
  • 分別討論兩件事情
    • 合併
      • 合併大小為 nn 的陣列的時候需要 O(n)\mathcal O(n) 的時間
      • 每層合併時各個被分割的子陣列大小和為 nn
    • 遞迴
      • 每層將陣列大小 nn 砍半
      • 需要砍 log2n\lceil \log_2 n \rceil 次才能砍完
  • 複雜度屬於 O(nlogn)\mathcal O(n\log n)

酷酷的小細節

  • 一直遞迴其實很浪費時間和效能
  • nn 夠小時 O(n2)\mathcal O(n^2) 其實不會跟 O(nlogn)\mathcal O(n \log n) 差太多
    • 決勝的細節在常數!
    • nn 太小時直接 O(n2)\mathcal O(n^2) 的排序會比 mergesort 到底好
    • 這個神奇數字 nn' 是多少呢
      • 鬼才知道
      • 自己找

主定理

  • Master theorem
  • 分治演算法複雜度計算的公式解
  • T(n)T(n) 為演算法執行大小為 nn 的測資的步驟數
T(n)={cif n=1aT(nb)+cndif n>1T(n) = \begin{cases} c &\quad\text{if } n = 1\\ aT(\frac{n}{b}) + cn^d &\quad\text{if } n > 1\\ \end{cases}
T(n) = \begin{cases} c &\quad\text{if } n = 1\\ aT(\frac{n}{b}) + cn^d &\quad\text{if } n > 1\\ \end{cases}
  • ddlogba\log_b a 比大小!
T(n){Θ(nd)if logba<dΘ(ndlogn)if logba=dΘ(nlogba)if logba>dT(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}
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}

誰在乎 Θ(f(n))\Theta(f(n))

  • O(f(n))\mathcal O(f(n)) 只看函數的上界是否能被限制
  • Θ(f(n))\Theta(f(n)) 還要看下界
  • g(n)Θ(f(n))g(n) \in \Theta(f(n)),則:
    • 存在正數 kl,ku,n0k_l, k_u, n_0 使得:
    • n>n0:0kl×g(n)f(n)ku×g(n)\forall n > n_0: 0 \le k_l \times g(n) \le f(n) \le k_u \times g(n)
  • 可以用就好

分治法求 an (mod m)a^n\ ( \text{mod }m)

  • 直接暴力做的複雜度是多少?
    • O(n)\mathcal O(n)
    • 很糟糕嗎
    • 如果你要一直做就會很糟糕
  • 我們不訪把問題切割成兩個小問題
    • x (mod m)×y (mod m)xy (mod m)\because x\ (\text{mod } m) \times y\ (\text{mod } m) \equiv xy\ (\text{mod } m)
    •  
an (mod m)an2 (mod m)×an2 (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)
\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)={cif  n=12T(n2)+cif  n>1T(n) = \begin{cases} c &\quad\text{if}\ \ n = 1\\ 2T(\frac{n}{2}) + c &\quad\text{if}\ \ n > 1\\ \end{cases}
T(n) = \begin{cases} c &\quad\text{if}\ \ n = 1\\ 2T(\frac{n}{2}) + c &\quad\text{if}\ \ n > 1\\ \end{cases}
log22=1\log_2 2 = 1
\log_2 2 = 1
T(n)Θ(nlog22)=Θ(n)?!T(n) \in \Theta(n^{\log_2 2}) = \Theta(n)?!
T(n) \in \Theta(n^{\log_2 2}) = \Theta(n)?!

這樣沒有比較快阿

  • 如何切割和合併子問題是很重要的
a8a^8
a^8
a2a^2
a^2
a1a^1
a^1
a4a^4
a^4
a1a^1
a^1
a2a^2
a^2
a1a^1
a^1
a1a^1
a^1
a2a^2
a^2
a1a^1
a^1
a1a^1
a^1
a2a^2
a^2
a1a^1
a^1
a1a^1
a^1
a4a^4
a^4

小改動

an={an2×an2ifn0 mod 2an2×an2×aifn1 mod 2a^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}
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)={cif  n=1T(n2)+cif  n>1T(n) = \begin{cases} c &\quad\text{if}\ \ n = 1\\ T(\frac{n}{2}) + c &\quad\text{if}\ \ n > 1\\ \end{cases}
T(n) = \begin{cases} c &\quad\text{if}\ \ n = 1\\ T(\frac{n}{2}) + c &\quad\text{if}\ \ n > 1\\ \end{cases}
log21=0=d\log_2 1 = 0 = d
\log_2 1 = 0 = d
T(n)Θ(ndlogn)=Θ(n0logn)=Θ(logn)T(n) \in \Theta(n^d \log n) = \Theta(n^0 \log n) = \Theta(\log n)
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)2a^{(19)_{10}} = a^{(10011)_{2}}
  • a2=a1×a1a^2 = a^1 \times a^1
  • a4=a2×a2a^4 = a^2 \times a^2
  • a8=a4×a4a^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

動態規劃

來點例題

  • Fibonacci’s sequence:\text{Fibonacci's sequence}:
    • f0=0,f1=1f_0 = 0, f_1 = 1
    • n>1:fn=fn1+fn2\forall n > 1: f_n = f_{n - 1} + f_{n - 2}
  • fnf_n
  • 來直接遞迴吧

直接遞迴 code

  • 複雜度?
int f(int n) {
    if (n < 2) return n;
    return f(n - 1) + f(n - 2);
}
f3f_3
f_3
f2f_2
f_2
f1f_1
f_1
f1f_1
f_1
f0f_0
f_0

複雜度

  • fnf_n 會呼叫兩次 ff
  • nn 每次最少減少 11
  • 複雜度 O(2n)\mathcal O(2^n)
  • 其實他應該是 O(ϕn)\mathcal O(\phi^n) 但我不會證
  • 反正他就是指數時間演算法

問題在哪裡

  • 跟剛才算 ana^n 的時候,我們做了什麼
  • an2a^{\lfloor \frac{n}{2} \rfloor} 記下來
    • 這個動作的核心理念是?
    • 做過的事情不要再做一次
  • 這就是動態規劃的精隨
  • 好好記錄,把 O(ϕn)\mathcal O(\phi^n) 變成 O(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
      • 重複計算到多個一樣的子問題
      • 例:計算 f9f_9 時會算到 f4f_4 很多次
    • 最佳子結構 optimal substructure
      • 問題的最佳解之子問題的解亦為最佳解
      • 可分治性

DP 演算法需要什麼

  • 定義狀態
  • 轉移(遞迴)式
  • 邊界條件
  • 來點例子吧!

CSES 1633 骰子組合

  • 看得懂題目嗎?
  • 骰任意次骰子,骰出 nn 點的可能性有幾種?
  • n106n \le 10^6,看起來得要 O(n)\mathcal O(n) 做完
  • 來試試看 dp!
    • 定義狀態:
      • dp[i]=\text{dp}[i] = 點數和為 ii 的方法數
    • 轉移式:
      • dp[i]=n=i6i1dp[n]\text{dp}[i] = \displaystyle \sum_{n = i - 6}^{i - 1} \text{dp}[n]
    • ​邊界條件:
      • dp[0]=1,i<0:dp[i]=0\text{dp}[0] = 1, \forall i < 0 : \text{dp}[i] = 0

試試看!

  • 實作時間!
  • 記得取模!

DP 的複雜度?

  • dp 時,通常就是:
    • 遍歷每個狀態
    • 將資訊轉移至該狀態
  • 複雜度:O(狀態數×轉移時間)\mathcal O(狀態數 \times 轉移時間)
  • 剛剛的例子
    • O(n×6)=O(6n)=O(n)\mathcal O(n \times 6) = \mathcal O(6n) = \mathcal O(n)
DP Classics

動態規劃經典題

Maximum Consecutive Sum

最大連續和

最大連續和

  • 給定一長度為 nn 的整數陣列 aa,求一個連續區間和的最大值
    • n500n \le 500
      • 窮舉區間左右界 l,rl, r,計算區間和,比較並選出最大值
      • 複雜度 O(n3)\mathcal O(n^3)
    • n10000n \le 10000
      • ​有什麼東西是重複算到的?
      • 區間和!
      • 先計算陣列的前綴和 sum[i]=j=1iaj\text{sum[i]}=\displaystyle \sum^{i}_{j = 1} a_j,再窮舉左右界 l,rl, r
      • 查詢只要拿 sum[r]sum[l1]\text{sum}[r] - \text{sum}[l - 1]O(1)\mathcal O(1)
      • 複雜度 O(n2\mathcal O(n^2)!

前綴和也算一種 DP

  • dp[i]=j=1iaj\text{dp}[i] = \displaystyle \sum_{j = 1}^{i} a_j
  • 轉移式:dp[i]=dp[i1]+ai\text{dp}[i] = dp[i-1] + a_i
  • 邊界條件:dp[0]=0\text{dp}[0] = 0
  • 聽起來很唬爛但這是真的
  • 前綴和還有很多應用
    • ​和好朋友差分一起可以做很多事情
    • 什麼是差分?
      • difference\text{difference}
      • dif[i]=aiai1, dif[1]=a1\text{dif}[i] = a_i - a_{i-1},\ \text{dif}[1] = a_1
    • ​差分陣列的前綴和陣列為原陣列,反之亦然
    • 給你 nn 條線段,有 kk 線段覆蓋的區間有多少個?

快,還要更快

  • n106n \le 10^6
    • 注意到在 sum[r]\text{sum}[r] 相同時,sum[l1]\text{sum}[l-1] 越小越好
    • 每次提取最小的 sum[l1]\text{sum}[l-1]
    • 用 priority queue 實作
    • 複雜度 O(nlogn)\mathcal O(n \log n)
  • n107n \le 10^7
    • 你要先確定你可以吃完輸入欸
    • 這麼大的東西可能會用別的方法給你輸入(亂數種子之類的)
    • 連帶 log\log 都不行,怎麼辦
    • 我們剛才做了哪些「不必要的事」?

敏銳的觀察

  • 何必一直提取呢
    • dp[i]\text{dp}[i] 為前 ii 項的最小 sum[i]\text{sum}[i]
    • 轉移式 dp[i]=min(dp[i1],sum[i])\text{dp}[i] = \min(\text{dp}[i-1], \text{sum}[i])
    • 邊界條件 dp[0]=0\text{dp}[0] = 0
  • 這聽起來很不演算法欸(?)
  • 另外一種想法:
    • dp[i]\text{dp}[i] 為以 aia_i 為結尾的最大連續和
    • 轉移式 dp[i]=max(0,dp[i1])+ai\text{dp}[i] = \max(0, \text{dp}[i - 1]) + a_i
      • 為什麼要長這樣?
      • 如果要從前面連續不好的話乾脆從我開始!
    • 邊界條件 dp[0]=0\text{dp}[0] = 0
Paths on grids

網格上路徑數量

走路

  • 一個笛卡爾座標系兩點間走捷徑有幾種路徑?
    • 規定只能在整數點間移動
    • 每次移動距離為 11
  • ​蛤
    • ​笛卡爾座標系 aka\text{aka} 直角座標系
    • 捷徑:路徑長最短的路徑
  • 如果我說這題可以 O(Δx+Δy)\mathcal O(\Delta x + \Delta y) 算出來你會相信嗎?
    • 數學!
    • 那如果座標點上有障礙物呢?

把問題化簡

  • 不失一般性,把問題化簡為從左上到右下的樣子
  • 令起點座標 =(0,0)=(0, 0);終點座標 =(Δx,Δy)=(\Delta x, \Delta y)
起點
終點

觀察性質

  • 想要離終點越來越近,勢必不能往上或往左走
  • 只能從左邊或上面到達自己
  • dp[i][j]\text{dp}[i][j] 為從 (0,0)(0, 0) 走到 (i,j)(i, j) 的方法數
  • 轉移式 dp[i][j]=dp[i1][j]+dp[i][j1]\text{dp}[i][j] = \text{dp}[i-1][j] + \text{dp}[i][j-1]

 

  • 邊界條件

 

  • 迴圈的順序非常重要!
dp[0][0]=!blocked[0][0]\text{dp}[0][0] = \text{!blocked}[0][0]
\text{dp}[0][0] = \text{!blocked}[0][0]
Longest Common Subsequence

最長共同子序列

最長共同子序列

  • Longest Common Subsequence
  • LCS("AawSoDian","BrineSoNotDian")="SoDian"\text{LCS}("AawSoDian", "BrineSoNotDian") = "SoDian"
  • 給定字串 sa,sbs_a, s_b,求 LCS(sa,sb)|\text{LCS}(s_a, s_b)|
  • dp[i][j]=LCS(sa[0:i],sb[0:j])\text{dp}[i][j] = \text{LCS}(s_a[0:i], s_b[0:j])
  • 狀態轉移式?
    • sa[i],sb[j]s_a[i], s_b[j] 皆為 LCS 的一部分
      • dp[i][j]=dp[i1][j1]+1\text{dp}[i][j] = \text{dp}[i-1][j-1]+1
    • sa[i],sb[j]s_a[i], s_b[j] 不皆為 LCS 的一部分
      • dp[i][j]=max(dp[i][j1],dp[i1][j])\text{dp}[i][j] = \max(\text{dp}[i][j-1], \text{dp}[i-1][j])

是哪個 case?

  • 只有在 sa[i]=sb[j]s_a[i] = s_b[j] 時用第一種轉移就好
    • why?
      • dp[i][j1]dp[i1][j1]\text{dp}[i][j-1] \ge \text{dp}[i-1][j-1]
      • dp[i1][j]dp[i1][j1]\text{dp}[i-1][j] \ge \text{dp}[i-1][j-1]
      • 沒有 +1+1 就沒有必要用 dp[i1][j1]\text{dp}[i-1][j-1]
  • 邊界條件 dp[0][0]=0\text{dp}[0][0] = 0
    • dp[3][0]\text{dp}[3][0] 直接去世
    • i=0j=0:dp[i][j]=0\forall i = 0 \lor j = 0: \text{dp}[i][j] = 0

例題

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

By Brine

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

演算法[7]

  • 681