---
# System prepended metadata

title: 'Lecture 06: Dynamic programming'
tags: [課堂筆記, 演算法]

---

# Lecture 06: Dynamic programming

> 課程內容 : 交通大學開放式課程 交大電機 江蕙如教授
> 參考書目 : Algorithm Design(2006), J. Kleinberg and E. Tardos

## Content

* [Basic](#Basic)
* [Weighted interval scheduling: a recursive procedure](#Weighted-interval-scheduling-a-recursive-procedure)
    * [1. Design a recursive algorithm](#1-Design-a-recursive-algorithm)
        * [1.1 Induction on time](#11-Induction-on-time)
        * [1.2 Induction on interval index](#12-Induction-on-interval-index)
        * [1.3 Implement of index-based induction](#13-Implement-of-index-based-induction)
    * [2. Memoization: top-down](#2-Memoization-top-down)
    * [3. Iteration: bottom-up](#3-Iteration-bottom-up)
    * [4. Summary: memoization vs. iteration](#4-Summary-memoization-vs-iteration)
        * [4.1 Keys for DP](#41-Keys-for-DP)
        * [4.2 Algorithm paradigms](#4-2-Algorithm-paradigms)
* [Appendix](#Appendix)
    * [1. Finonacci sequence](#1-Finonacci-sequence)
* [Subset sums and Knapsacks: adding a variable](#Subset-sums-and-Knapsacks-adding-a-variable)
    * [1. Subset sum](#1-Subset-sum)
        * [1.1 DP: false start](#11-DP-false-start)
        * [1.2 Improvement: add a new variable](#12-Improvement-add-a-new-variable)
    * [2. Knapsack problem](#2-Knapsack-problem)
* [Shortest paths in a grpah](#Shortest-paths-in-a-grpah)
    * [1. Review: Dijkstra's algorithm](#1-Review-Dijkstras-algorithm)
    * [2. Bellman-Ford algorithm](#2-Bellman-Ford-algorithm)
    * [3. Implement: iteration](#3-Implement-iteration)
    * [4. Negative cycles detection](#4-Negative-cycles-detection)


## Basic

Dynamic Programming(DP) 並非 coding 的方式，而是一種在數學上最佳化的機制，由 Richard E. Bellman 在 1953 年發明。

DP 的核心想法是藉由以下方式隱晦地(implicitly)探索所有可能的解 :

* 將大問題分解成一系列的小問題(subprpblems)
* 從小問題開始逐層建立正確答案

與 divide and conquer 不同的是 divide and conquer 是將明確地定義小問題再解決，但 DP 解決 subproblem 前不會明確的分析它。

> [!Important] **Core idea**
> Dynamic Programming 的核心想法就是數學歸納法(mathmatical induction)

## Weighted interval scheduling: a recursive procedure

> **Example :**
> 假設有 n 個給定開始(start)/結束(finish)時間的 interval 與對應的 weight 需要執行
> * Interval $i$ : $[s_i, f_i),\ 1 \le i \le n$
> * Weight :  $v_i,\ 1 \le i \le n$
> 
> 需要找到一個由不重疊的 interval 組成的子集 $S$ 使得他有最大的 weight sum。
> ![image](https://hackmd.io/_uploads/rJi1SD_c1g.png)

以 greedy algorithm 的角度來說並無法得到一個最佳解: 

* 將完成時間遞增排序
* 每次依序挑選一個 interval，若重疊則放棄

以 greedy algorithm 進行(下圖為例)會得到的解 $= \{I_1, I_3\}$ 且 weight sum $=v_1 + v_3 = 2$。但實際上的最佳解應該 $= \{ I_2\}$，weight sum = $v_2 = 3$。

![image](https://hackmd.io/_uploads/SkUGvv_91g.png)

### 1. Design a recursive algorithm

一開始有提過 DP 的核心概念就是 mathmatical induction，因此在進行 DP 找最佳解之前該考慮的是要依據何種 base condition 進行歸納。

#### 1.1 Induction on time

以下圖簡化後的 Example 為例，假設先對時間做歸納：考慮小一點的時間 $t'$ 的解，再加下一段時間差的貢獻。此時要考慮的問題是「時間範圍應該要怎麼選 or 怎麼切割」 

<font color="#0000E3">(1) 考慮最後一個 $I_6 = 1$ (左圖)
若最後一個 $I_6 = 1$ 有貢獻，則前一刻的時間 $t'$ 應該選在 $I_3 = 4$ 之後與 $I_4 = 6$ 之前</font>

<font color="ff0000">(2) 不考慮最後一個 $I_6 = 1$ (右圖)
若最後一個 $I_6 = 1$ 沒有貢獻，則前一個的時間 $t'$ 應該選在 $I_5 = 2$ 之後與 $I_6 = 1$ 之前</font>

![image](https://hackmd.io/_uploads/HJ3y6P_cyl.png)

結論：
以時間做 induction 不是很好的做法，因為時間範圍 $\Delta t$ 不好做切割。

#### 1.2 Induction on interval index

第二種方式以 interval 的編號做歸納：

* 先以完成時間對 interval 做排序(DP 的小技巧，主要用來簡化問題結構)
* 令 $p(j) = i$ 表示最大的 index $i$ 使得 $I_i$ 與 $I_j$ 互斥，其中 $i < j$
    * 若沒有 $I_i$ 與 $I_j$ 互斥，則 $p(j) = 0$

與 time-based 的 induction 相同，觀察 $n$ 個 interval 的最佳解與 $n-1$ 個 interval 的最佳解的關係，也就是考慮第 $n$ 個 interval 的貢獻。

<font color="#0000E3">(1) 考慮最後一個 $I_6 = 1$ (左圖)
若最後一個 $I_6 = 1$ 有貢獻，則前一個 index 應該取到 $I_3$</font>

<font color="ff0000">(2) 不考慮最後一個 $I_6 = 1$ (右圖)
若最後一個 $I_6 = 1$ 沒有貢獻，則前一個 index 應該取到 $I_5$</font>

![image](https://hackmd.io/_uploads/rJvHQOdcyg.png)

若以符號表示：

* 令 $O_j$ 表示前 $j$ 個 interval 的最佳解的集合
* 令 $OPT_j$ 表示前 $j$ 個 interval 的最佳解的值

以上述圖片為例，若有 6 個 interval $I_1, ..., I_6$，則此 6 個 interval 的解集合為 $O_6$：

* 考慮是否包含最後一個 $I_6$ 的貢獻
    * 包含 $I_6$，則 $O_6 = \{I_6, O_3\}$
    * 不包含 $I_6$，則 $O_6 = O_5$
* 最佳解為 $OPT(6) = \max\{\{v_6 + OPT(3)\},\ OPT(5)\}$

因此前 $j$ 個 interval 的最佳解的通式為

$$
OPT(j) = \max\{\{v_j + OPT(p(j))\},\ OPT(j-1)\}
$$

#### 1.3 Implement of index-based induction

參考前面所推得的結論：

$$
OPT(j) = \max\{\{v_j + OPT(p(j))\},\ OPT(j-1)\}
$$

> [!Tip] **Algorithm: index-based induction**
> ![image](https://hackmd.io/_uploads/rymyPuu9kg.png)

將上述的 pseudo code 展開後會得到一個樹狀的遞迴結構

<img src="https://hackmd.io/_uploads/Hyb_vdu9Jx.png" width=300>

### 2. Memoization: top-down

從上面展開後的樹狀遞迴結構可以發現會有非常多重複計算的項目(下圖紅色區塊)，因此可以把已經計算過的值儲存下來(`M[j]`)，之後遇到重複項直接使用，實際上只有計算左側的 $OPT(6)$ 到 $OPT(1)$。

這種方法稱為 memoization，是一種 top-down 結構的 dynamic programming。

<img src="https://hackmd.io/_uploads/ryeKF_uc1e.png" width=300>

修改過後的演算法如下：

> [!Tip] **Algorithm: memoization**
> ![image](https://hackmd.io/_uploads/rJ-G5uOq1x.png)

### 3. Iteration: bottom-up

另一種使用迭代方式進行，從底層的 `M[0]` 開始計算至 `M[n]`，是一種 bottom-up 結構的 dynamic programming。

> [!Tip]**Algorithm: iteration**
> ![image](https://hackmd.io/_uploads/BJ3Q6aO5Jx.png)

 Bottom-up 的迭代類似查表格的動作，如下圖所示。每次計算/填寫 `M[i]` 時會需要往前做查找，但需要確保前面的值已經計算過，若沒有計算則代表 DP 是錯的，可能是 induction 的條件不正確。
 
![image](https://hackmd.io/_uploads/S1CnTTdqJg.png)

### 4. Summary: memoization vs. iteration
| Memoization | Iteration |
| :-:         | :-:       |
| Top-down    | Bottom-up |
| Recursive algorithm | Iteration algorithm |
| Compute what we need only | Construct solution from smallest subproblem to the largest one |

#### 4.1 Keys for DP

* DP 通常用在最佳化問題(但有少數例外)
* DP 在有線性順序且無改變順序的問題上有不錯的表現
* DP 的組成
    * Optimal substructure: 最佳解來自 subproblem 的最佳解
    * Overlapping subproblem: subproblem 會不斷的重複出現，且 subproblem 的數量是多項式等級的

#### 4.2 Algorithm paradigms

* Brute-force: 明確檢查所有可能解的集合
* Greedy: 每個步驟選擇當下的最佳解，且選擇以後不會再反悔。每個步驟的最佳解都會隊最後的最佳解有貢獻
* Divide and conquer: 將大問題拆解成小問題，但問題的本質不變，最後再將小問題的解做合併(最難)
* Dynamic programming: 將大問題拆解成一系列重複的小問題，透過重複的小問題建構出更大的小問題的解

## Appendix

### 1. Finonacci sequence

費氏數列：

$$
F_n = F_{n-1} + F_{n-2},\ \text{where}\ F_0=0\ \text{and}\  F_1=1
$$

本身也是一種具有遞迴關係的問題

> [!Tip] **Algorithm: finonacci sequence**
> ![image](https://hackmd.io/_uploads/ry-0URtcJg.png)


將費氏數列的遞迴結構展開後如下圖所示，會發現到有很多項目被重複計算。將問題簡化以後同樣可以用 DP 的兩個方法來求費氏數列的解。

> [!Warning]
> 但實際上費氏數列並不算是正規的 DP 問題，因為只是使用 DP 的技巧計算數列的任意項，但費氏數列本身沒有牽涉到最佳化的問題。


(1) Memoization

* 將計算過的值儲
* 每次遞迴之前先檢查該數值是否已經被計算過

> [!Tip] **Algorithm: finonacci sequence(memoization ver.)**
> * 陣列 `f[n]` 儲存已經算過的函數值 $f(n)$，初始化 $f(i) = -1,\ i = 0, ..., n$
> * 設定初始值 `f[0] = 0` 與 `f[1] = 1`
> ![image](https://hackmd.io/_uploads/BkOJP0tqyg.png)


(2) Iteration

* 從最簡單的 subproblem 開始逐步建構完整問題的解

> [!Tip] **Algorithm: finonacci sequence(iteration ver.)**
> * 陣列 `f[n]` 儲存已經算過的函數值 $f(n)$，初始化 $f(i) = -1,\ i = 0, ..., n$
> * 設定初始值 `f[0] = 0` 與 `f[1] = 1`
> ![image](https://hackmd.io/_uploads/HJBxP0Yqkl.png)


## Subset sums and Knapsacks: adding a variable

### 1. Subset sum

> Question description:
> * Given
> * 有 $n$ 個物品的集合與一個背包(knapsack)
>     * 物品 $i$ 的重量為 $w_i > 0$
>     * 背包限重 $W$
> * Goal
>     * 將背包填滿使得總重最大
>     * $i.e., \max \{\Sigma_{i \in S}\ w_i\}$ 且 $\Sigma_{i \in S}\ w_i < W$

以下方表格為例(W = 11)：

| Item | Weight |
| :-:  | :-:    |
| 1 | 1 |
| 2 | 2 |
| 3 | 5 |
| 4 | 6 |
| 5 | 7 |

若使用 greedy algorithm 找每個步驟的最佳解：

$$
w_5 + w_2 + w_1 = 7 + 2 + 1 = 10
$$

但可以明顯發現 10 並不是最佳解，最佳解應該是 $w_3 + w_4 = 5 + 6 = 11$。

#### 1.1 DP: false start

以 dynamic programming 的方法找最佳解，首先先列出最佳化問題的格式

$$
\begin{align}
& \max\ \Sigma_{i \in S}\ w_i\\
& \text{s.t.}\ \Sigma_{i \in S\ }w_i < W,S \subseteq\{1, 2, ..., n\}
\end{align}
$$

Subproblem 的形式為: $OPT(i) = \max_S\Sigma_{j \in S}\ w_j,S \subseteq \{1, 2, ..., i\}$，表示物品 $1$ 到物品 $i$ 的總重。

設最佳解: $OPT(n)$ 表示物品 $1$ 到物品 $n$ 的總重，且解集合為 $O$。依照 mathmatical induction 的概念討論 2 種情況: 

(1) $w_n$ 沒貢獻 $(i.e., n \notin O)$
此時最佳解 $OPT(n) = OPT(n-1)$

(2) $w_n$ 有貢獻 $(i.e., n \in O)$
此時最佳解 $OPT(n) = w_n + OPT(n-1)$

但這個 indution 的推論是錯誤的，why ?

因為在第二段的討論中，當 $w_n$ 有貢獻添加到解集合 $O$ 之中時，可能會造成 $OPT(n)$ 計算出的背包總重超出 $W$ 的限制。

因此第二階段的討論中，若要考慮 $w_n$ 的貢獻，則需要在前一階段 $OPT(n-1)$ 中加入總重的限制必須 $< W - w_n$

#### 1.2 Improvement: add a new variable

同樣的最佳化問題形式

$$
\begin{aligned}
& \max\ \Sigma_{i \in S}\ w_i\\
& \text{s.t.}\ \Sigma_{i \in S\ }w_i < W,S \subseteq\{1, 2, ..., n\}
\end{aligned}
$$

此時 subproblem 需要多一個限制

$$
OPT(i, W) = \max_S \Sigma_{j \in S}\ w_j, S \subseteq \{1, 2, ..., i\}, \Sigma_{j \in S}\ w_j < W
$$

此時的最佳解 $OPT(n)$ 同樣討論 2 種狀況:

(1) $w_n$ 沒貢獻 $(i.e., n \notin O)$
與前面相同，此時最佳解 $OPT(n) = OPT(n-1, W)$

(2) $w_n$ 有貢獻 $(i.e., n \in O)$
此時最佳解 $OPT(n, W) = w_n + OPT(n-1, W-w_n)$

最後遞迴關係式如下:

$$
OPT(i, W) = 
\begin{cases}
&0 &\text{, if } i \text{ or } w = 0\\
&OPT(i-1, W) &\text{, if } w_i > W\\
&\max \{OPT(i-1, W),\ w_i + OPT(i-1, W-w_i)\} &\text{, otherwise}
\end{cases}
$$

其中第 1 式表示物品為 $0$ 或重量限制為 $0$ 的邊界條件。第 2 式表示下一個物品重量 $w_i$ 超過重量限制 $W$，所以最佳解為前一步的最佳解。

> [!Tip] **Algorithm: DP for subset sum(iteration ver.)**
> * 以陣列 `M[i, w]` 表示在物品數量為 $i$ 且重量限制為 $W$ 的情況下的最佳解
> ![image](https://hackmd.io/_uploads/SysMPCF51e.png)


建構出來的表格下圖所示

![image](https://hackmd.io/_uploads/SJm4PAFqJl.png)


### 2. Knapsack problem

> Question description:
> * Given
> * 有 $n$ 個物品的集合與一個背包(knapsack)
>     * 物品 $i$ 的重量為 $w_i > 0$ 且價值為 $v_i > 0$
>     * 背包限重 $W$
> * Goal
>     * 將背包填滿使得總價值最大
>     * $i.e., \max \Sigma_{i \in S}\ v_i$ 

最佳化問題形式為

$$
\begin{align}
&\max \Sigma_{i \in S}\ v_i\\
&\text{s.t. } \Sigma_{i \in S}\ w_i < W, S \subseteq\{1, 2, ..., n\}
\end{align}
$$

以下方表格為例(W = 11)：

| Item | Value | Weight |
| :-:  | :-:    | :-:   |
| 1 | 1  | 1 |
| 2 | 6  | 2 |
| 3 | 18 | 5 |
| 4 | 22 | 6 |
| 5 | 28 | 7 |

若使用 greedy algorithm 找每個步驟的最佳解：

$$
v_5 + v_2 + v_1 = 28 + 6 + 1 = 35
$$

但可以明顯發現 35 並不是最佳解，最佳解應該是 $v_3 + v_4 = 18 + 22 = 40$。

Knapsack problem 與 subset sum problem 幾乎一樣，但前者是考慮物品的總價值最大，後者是考慮總重量最大，而條件限制一樣是重量須 $< W$。

因此只要將 subset sum problem 中第 $i$ 個物品的重量 $w_i$ 項改為價值 $v_i$ 項即可

$$
OPT(i, W) = 
\begin{cases}
&0 &\text{, if } i \text{ or } w = 0\\
&OPT(i-1, W) &\text{, if } w_i > W\\
&\max \{OPT(i-1, W),\ v_i + OPT(i-1, W-w_i)\} &\text{, otherwise}
\end{cases}
$$

## Shortest paths in a grpah

### 1. Review: Dijkstra's algorithm

考慮一個 $G = (V, E)$ 以及起點 $s$ 和終點 $t$。且任意兩節點之間的 edge $(u, v) \in E$ 的權重為 $c_{uv} \ge 0$。

要找一個從 $s$ 到 $t$ 的路徑 $P$ 使得 $c(P) = \Sigma_{(u, v) \in P}\ c_{uv}$

因為 Dijkstra's algorithm 只能用在非負 edge weight 的情況，當考慮到 edge weight 有負數時(以下圖為例):

![image](https://hackmd.io/_uploads/HJ1LDRFcyl.png)

Dijkstra's algorithm 的最短路徑走法為 s -> t: 1
但實際上的最短路徑為 s -> a -> b -> t: -1

就算把負數 edge weight 的情況做等值放大後 Dijkstra's algorithm 找到的最短路徑依然不會等價於原始情況，因為最佳解 s -> a -> b -> t 會經過 3 個 edges，每個 edges 都會 +6，共加了 3 $\cdot$ 6 = 18。

![image](https://hackmd.io/_uploads/ry5Dw0Kqyg.png)

但 Dijkstra's algorithm 的路徑只走了一個 edges，只加了一個 6。不同的縮放比例導致 Dijkstra's algorith 的解不會等價於最佳解。

### 2. Bellman-Ford algorithm

Bellman-Ford algorithm 是一種使用 DP 找兩節點之間最短路徑的方法，且適用在 edge weight 有負數時。可以使用節點或邊做 induction 的推論。

> [!Note] **Lemma**
> If $G = (V, E)$ has no negative cycles, then there is a shortest path from $s$ to $t$ that is simple (i.e. not repeat nodes) and hence has at most $n-1$ edges, where $n = |V|$.

Negative cycle 如下右圖所示，表示迴圈中的 weight sum < 0 $(i.e., c(C) < 0)$。若出現 negative cycle，則路徑選擇時會不斷進入這個迴圈中，每走一圈 weight sum 就不斷下降，最後完全走不出來。

![image](https://hackmd.io/_uploads/S1eRDCtcJe.png)


假設從起點 $s$ 到終點 $t$ 並以 edges 做歸納且固定終點 $t$，則最佳解 $OPT(n-1, s)$ 表示從節點 $S$ 到終點 $t$ 之間最多經過 $n-1$ 個 edges 的最短距離路徑。因此 subproblem 的最佳解形式為 $OPT(i, v)$，表示從節點 $v$ 到終點 $t$ 之間的路徑 $P$ 最多經過 $i$ 個 edges 的最短距離。同樣討論 2 個狀況:

(1) 前一個 edge 沒有貢獻(下圖左)
前一個 edge 沒有貢獻(節點 v -> 節點 w)，表示路徑 $P$ 最多使用後面 $i-1$ 個 edges，因此 $OPT(i, v) = OPT(i-1, v)$。

(2) 第一個 edge 有貢獻(下圖右)
前一個 edge 有貢獻(節點 v -> 節點 w)，表示路徑 $P$ 最多使用 $i$ 個 edges，因此 $OPT(i, v) = c_{vw} + OPT(i-1, w)$。

![image](https://hackmd.io/_uploads/SyY7d0Yqyl.png)


> [!Important]
> 這邊的做法是固定終點位置從終點位置往回推，與一開始 DP 從頭開始歸納的推論方向不同，所以會考慮前一個 edge 有無做出貢獻而不是最後一個 edge 有沒有貢獻。

綜合兩個討論，遞迴關係式為:

$$
OPT(i, v) = 
\begin{cases}
&0                                                                  &,\text{if } i=0 \text{ and } v = t\\
&\infty                                                             &,\text{if } i=0 \text{ and } v \neq t\\
&\min\{OPT(i-1, v),\ \min_{(v, w) \in E} \{c_{vw} + OPT(i-1, w)\}\} &,\text{otherwise}
\end{cases}
$$

(其中 i=0 表示沒有經過 edges，v=t 表示自己到自己最短距離)

> [!Important]
> Bellman-Ford algorithm 透過 DP 找到的最短路徑，可以使用預備定理證明此最短路徑必為最多使用 $n-1$ 個 edges 的簡單路徑。


### 3. Implement: iteration

Bottom-up 的迭代版本實作如下

> [!Tip] **Algorithm: Bellman-Ford algorithm(iteration ver.)**
> * 以陣列 `M[n-1, v]` 表示每個 subproblem 的最佳解，例如 `M[i, v]` 表示從節點 $v$ 到終點 $t$ 且最多使用 $i$ 個 edges 的最短距離
> * `M[0, t] = 0` 表示終點 $t$ 到終點 $t$ 的邊界條件
> * `M[0, v] = inf` 表示從節點 $v$ 到終點 $t$ 且不使用 edge 的邊界條件
> 
> ![3200A66B-F0B7-47B9-8589-CD6F30DE22BB](https://hackmd.io/_uploads/BJ6_d0tcJg.jpg)

依照上述的演算法所建構出的表格如下，實際手動操作會發現填表格時是以 column by column 的方式依序填寫。

![image](https://hackmd.io/_uploads/H1WAuRY5kl.png)

### 4. Negative cycles detection

Bellman-Ford algorithm 的使用前提是圖上不能包含 negeative cycles，但反過來說它也可以用來檢查圖中是否存在 negative cycle。

> [!Note] **Thm.**
> If $OPT(n, v) = opt(n-1, v)$ for all $v$, then no negative cycles

Bellman-Ford algorithm 保證了經過 $n-1$ 次的迭代後可以找到最短路徑(穩定狀態)，此時繼續做迭代也不可能找到更短路徑。這表示圖中不可能存在 negative cycles，因為如果存在 negative cycle，則繼續迭代理論上會得到更小的路徑長。

> [!Note] **Thm.**
> If $OPT(n, v) < OPT(n-1, v)$ for some $v$，then shortest path contains a negetive cycle

換句話說，如果經過 $n$ 次迭代的最短距離可以推翻 $n-1$ 次迭代的最短距離，表示此圖/路徑會存在 negative cycle，因為繼續做迭代可以得到更短的距離。



