Skip to content

Commit 11d7331

Browse files
authored
Merge pull request #181 from chvmvd/add-week13-article
Add week13 article
2 parents 20bf732 + ed89eee commit 11d7331

File tree

6 files changed

+415
-0
lines changed

6 files changed

+415
-0
lines changed

docs/02algorithms/11dp/index.mdx

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
---
2+
sidebar_position: 11
3+
---
4+
5+
import ViewSource from "@site/src/components/ViewSource";
6+
import Answer from "@site/src/components/Answer";
7+
8+
# 動的計画法
9+
10+
最後に、経路探索や自然言語処理などにも使われる動的計画法について学習します。
11+
12+
## フィボナッチ数列
13+
14+
動的計画法の説明に必ずといってもいいほどよく使われるフィボナッチ数列を例に説明していきます。
15+
16+
フィボナッチ数列 $F_n$ ( $0, 1, 1, 2, 3, 5, 8, 13, 21, 34 \cdots$ )は、次のように定義されるのでした。
17+
18+
$$
19+
\begin{align*}
20+
&F_0 = 0 \\
21+
&F_1 = 1 \\
22+
&F_n = F_{n - 1} + F_{n - 2} \quad (n\geq 2)
23+
\end{align*}
24+
$$
25+
26+
### 再帰を使った場合
27+
28+
再帰を使ったプログラムは次のように作れました。
29+
30+
<ViewSource path="/recursion/fib.ipynb" />
31+
32+
しかし、これでは計算量が $O(2^n)$ なので `n` が小さければこれで問題ありませんが、`n` が大きくなると時間がかかりすぎます。このプログラムで `n = 40` とすると、実行するのに 1 分もかかりました。
33+
34+
原因は、次の図をみるとよくわかります。同じ数字を何度も計算してしまっています。たとえば、`fib(2)` は 3 回も計算しています。`n` が大きくなると大変です。
35+
36+
```mermaid
37+
flowchart
38+
A["fib(5)"] --> B["fib(4)"]
39+
A --> C["fib(3)"]
40+
B --> D["fib(3)"]
41+
B --> E["fib(2)"]
42+
C --> F["fib(2)"]
43+
C --> G["fib(1)"]
44+
D --> H["fib(2)"]
45+
D --> I["fib(1)"]
46+
E --> J["fib(1)"]
47+
E --> K["fib(0)"]
48+
F --> L["fib(1)"]
49+
F --> M["fib(0)"]
50+
H --> N["fib(1)"]
51+
H --> O["fib(0)"]
52+
```
53+
54+
### DP
55+
56+
これを解決するのが、動的計画法(Dynamic Programming)です。DP とよく言われます。
57+
58+
DP とは大きな問題を解くときに出てくる小さな問題の解を表に記録していき、その解を利用して次の計算を進めていくアルゴリズムのことです。
59+
60+
DP には大きく分けて、二種類あります。トップダウン方式とボトムアップ方式です。
61+
62+
#### トップダウン方式
63+
64+
計算結果をメモ化して、行うのがトップダウン方式です。メモ化再帰とも呼ばれます。
65+
66+
配列に計算結果をメモしておいて、すでに計算してあったらその値を利用します。
67+
68+
メモ化したプログラムは次のようになります。これなら、計算量は $O(n)$ なので、`n = 40` どころか `n = 100` でも余裕です。
69+
70+
<ViewSource path="/dp/fib_memoization.ipynb" />
71+
72+
冒頭の `memo = [-1 for _ in range(10000)]` は、リストの内包表記と呼ばれ、配列 `memo``-1` で埋める操作です。`[-1, -1, ..., -1]` となっています。
73+
74+
#### ボトムアップ方式
75+
76+
ボトムアップ方式は、`f(2)` を求めてから `f(3)` を求めるというように下から順番に求めていこうというものです。
77+
78+
プログラムは次のようになります。こちらも、計算量は $O(n)$ です。
79+
80+
<ViewSource path="/dp/fib_bottom_up.ipynb" />
81+
82+
## 部分和問題
83+
84+
次に、部分和問題を解いてみましょう。
85+
86+
> **部分和問題**(ぶぶんわもんだい)は、計算複雑性理論・暗号理論における問題で、与えられた $n$ 個の整数 $a_1,\dots,a_n$ から部分集合をうまく選んで、その集合内の数の和が与えられた数 $N$ に等しくなるようにできるかどうかを判定する問題である。NP 完全であることが知られている。
87+
> -- <cite>[フリー百科事典『ウィキペディア(Wikipedia)』](https://ja.wikipedia.org/wiki/部分和問題)</cite>
88+
89+
例としては、3、4、6 を使って 10 を作れるかという問題であれば、4 と 6 を足せば、10 であるのでできるという答えになります。
90+
91+
### 全探索
92+
93+
この問題は、全探索すれば解くことは可能です。$a_i(1\leq n)$ を含めるか含めないかの 2 通りずつがあるので、計算量は、$O(2^n)$ です。
94+
95+
### 動的計画法
96+
97+
#### 表を考える
98+
99+
表を用意します。
100+
101+
$i$ 行 $j$ 列は、$\{a_k\}(1\leq k\leq i)$ の中からいくつかを使って $j$ をつくることができるかの真偽値とします。(真は 1、偽は 0 とします)
102+
103+
3、4、6 を使って 10 を作れるかという問題を考えます。
104+
105+
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
106+
| --------------------------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
107+
| $\varnothing$ | | | | | | | | | | | |
108+
| $\{a_1\}=\{3\}$ | | | | | | | | | | | |
109+
| $\{a_1,a_2\}=\{3,4\}$ | | | | | | | | | | | |
110+
| $\{a_1,a_2,a_3\}=\{3,4,6\}$ | | | | | | | | | | | |
111+
112+
0 行目を考えます。
113+
114+
$0$ 行 $j$ 列は、$\varnothing$ の中からいくつかを使って $j$ をつくることができるかの真偽値となります。
115+
116+
よって、次のようになります。
117+
118+
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
119+
| --------------------------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
120+
| $\varnothing$ | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
121+
| $\{a_1\}=\{3\}$ | | | | | | | | | | | |
122+
| $\{a_1,a_2\}=\{3,4\}$ | | | | | | | | | | | |
123+
| $\{a_1,a_2,a_3\}=\{3,4,6\}$ | | | | | | | | | | | |
124+
125+
1 行目を考えます。
126+
127+
$1$ 行 $j$ 列は、$\{a_1\}=\{3\}$ の中からいくつかを使って $j$ をつくることができるかの真偽値となります。
128+
129+
よって、次のようになります。
130+
131+
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
132+
| --------------------------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
133+
| $\varnothing$ | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
134+
| $\{a_1\}=\{3\}$ | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
135+
| $\{a_1,a_2\}=\{3,4\}$ | | | | | | | | | | | |
136+
| $\{a_1,a_2,a_3\}=\{3,4,6\}$ | | | | | | | | | | | |
137+
138+
次に、2 行目を考えます。
139+
140+
$2$ 行 $j$ 列は、$\{a_1,a_2\}=\{3,4\}$ の中からいくつかを使って $j$ をつくることができるかの真偽値となります。
141+
142+
よって、次のようになります。
143+
144+
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
145+
| --------------------------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
146+
| $\varnothing$ | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
147+
| $\{a_1\}=\{3\}$ | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
148+
| $\{a_1,a_2\}=\{3,4\}$ | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
149+
| $\{a_1,a_2,a_3\}=\{3,4,6\}$ | | | | | | | | | | | |
150+
151+
次に、3 行目を考えます。
152+
153+
$3$ 行 $j$ 列は、$\{a_1,a_2,a_3\}=\{3,4,6\}$ の中からいくつかを使って $j$ をつくることができるかの真偽値となります。
154+
155+
よって、次のようになります。
156+
157+
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
158+
| --------------------------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
159+
| $\varnothing$ | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
160+
| $\{a_1\}=\{3\}$ | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
161+
| $\{a_1,a_2\}=\{3,4\}$ | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
162+
| $\{a_1,a_2,a_3\}=\{3,4,6\}$ | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 1 | 1 |
163+
164+
#### 漸化式を考える
165+
166+
これを元に漸化式を作っていきます。
167+
168+
$i$ 行目を考える時、$a_i$ を入れるか入れないかの二択になります。
169+
170+
- $a_i$ を入れないときは、一個上のセルの値の真偽値そのままです。つまり、$i-1$ 行 $j$ 列の真偽値となります。
171+
- $a_i$ を入れるときは、$a_k(1\leq k\leq i-1)$ を使って、**$j-a_i$** を作ることができていれば、$j$ を作れそうです。例えば、$3$ と $4$ を使って、$10-6=4$ が作れていれば、$10$ を作ることができます。
172+
そうすると、$i-1$ 行 $j-a_i$ 列の真偽値となります。
173+
174+
そうすると、次の漸化式が作れます。
175+
176+
$$
177+
\mathit{dp}[i][j]=dp[i-1][j]\lor dp[i-1][j-a_i]
178+
$$
179+
180+
ここで、$j-a_i<0$ のことを考えると、漸化式は次のようになります。
181+
182+
$$
183+
\mathit{dp}[i][j]=
184+
\begin{dcases}
185+
dp[i-1][j] & \text{if $a_i>j$,} \\
186+
dp[i-1][j]\lor dp[i-1][j-a_i] & \text{else.}
187+
\end{dcases}
188+
$$
189+
190+
#### プログラムを書く
191+
192+
まず、表を次のように初期化します。
193+
194+
$$
195+
\mathit{dp}[i][j]=
196+
\begin{dcases}
197+
\mathrm{true} & \text{if $j=0$,} \\
198+
\mathrm{false} & \text{else.}
199+
\end{dcases}
200+
$$
201+
202+
その後、さきほど求めた漸化式に従って、表を更新していきます。
203+
204+
$$
205+
\mathit{dp}[i][j]=
206+
\begin{dcases}
207+
dp[i-1][j] & \text{if $a_i>j$,} \\
208+
dp[i-1][j]\lor dp[i-1][j-a_i] & \text{else.}
209+
\end{dcases}
210+
$$
211+
212+
プログラムは次のようになります。計算量は、$O(nN)$ です。
213+
214+
<ViewSource path="/dp/ssp.ipynb" />
215+
216+
`dp = [[False for _ in range(N + 1)] for _ in range(len(a) + 1)]` は `len(a) + 1` 行 `N + 1` 列の二次元配列を `False` で埋めています。
217+
218+
## 練習問題
219+
220+
動的計画法における有名問題であるナップサック問題を解いてみましょう。
221+
222+
> **ナップサック問題**(ナップサックもんだい、Knapsack problem)は、計算複雑性理論における計算の難しさの議論の対象となる問題の一つで、$n$ 種類の品物(各々、価値 $v_i$、重量 $w_i$)が与えられたとき、重量の合計が $W$ を超えない範囲で品物のいくつかをナップサックに入れて、その入れた品物の価値の合計を最大化するには入れる品物の組み合わせをどのように選べばよいか」という整数計画問題である。同じ種類の品物を 1 つまでしか入れられない場合($x_i\in \{0, 1\}$)や、同じ品物をいくつでも入れてよい場合($x_i$ は 0 以上の整数)など、いくつかのバリエーションが存在する。
223+
> -- <cite>[フリー百科事典『ウィキペディア(Wikipedia)』](https://ja.wikipedia.org/wiki/ナップサック問題)</cite>
224+
225+
$x_i\in \{0,1\}$ とした、0-1 ナップサック問題を解いてみてください。
226+
227+
<Answer>
228+
229+
表を用意します。
230+
231+
$i$ 行 $j$ 列は、重さが $j$ 以下になるように $i$ 番目までの品物の中からいくつかをナップサックに入れたときの価値の最大値とします。
232+
233+
$v_1=2,v_2=3,v_3=6,w_1=2,w_2=3,w_3=5,W=10$ として考えてみます。
234+
235+
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
236+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
237+
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
238+
| 1 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
239+
| 2 | 0 | 0 | 2 | 3 | 3 | 5 | 5 | 5 | 5 | 5 | 5 |
240+
| 3 | 0 | 0 | 2 | 3 | 3 | 6 | 6 | 8 | 9 | 9 | 11 |
241+
242+
これから、漸化式を考えます。
243+
244+
- $i$ 番目の品物を使わなければ、一個上のセルの値そのままです。つまり、$i-1$ 行 $j$ 列の値です。
245+
- $i$ 番目の品物を使う場合は、$i-1$ 行 $j-w_i$ 列の値に $v_i$ を足したものになりそうです。
246+
247+
これから、漸化式を作ると次のようになります。
248+
249+
$$
250+
dp[i][j]=
251+
\begin{dcases}
252+
dp[i-1][j] & \text{if $w_i>j$,} \\
253+
\mathrm{max}(dp[i-1][j],dp[i-1][j-w_i]+v_i) & \text{else.}
254+
\end{dcases}
255+
$$
256+
257+
プログラムは次のようになります。計算量は、$O(nW)$ です。
258+
259+
<ViewSource path="/dp/knapsack.ipynb" />
260+
261+
`dp = [[0 for _ in range(W + 1)] for _ in range(len(v) + 1)]` は `len(v) + 1` 行 `W + 1` 列の配列を 0 埋めしています。
262+
263+
</Answer>

docs/index.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ sidebar_position: 1
2222

2323
## 更新履歴
2424

25+
1/15 第十三週の分を執筆 動的計画法
26+
2527
1/15 第十二週の分を執筆 誤差 連立方程式の解法
2628

2729
1/8 第十一週の分を執筆 ソートアルゴリズム

static/dp/fib_bottom_up.ipynb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"nbformat": 4,
3+
"nbformat_minor": 2,
4+
"metadata": {},
5+
"cells": [
6+
{
7+
"metadata": {},
8+
"source": [
9+
"def fib(n):\n",
10+
" a, b = 0, 1\n",
11+
" if n == 0:\n",
12+
" return a\n",
13+
" if n == 1:\n",
14+
" return b\n",
15+
" for _ in range(n - 1):\n",
16+
" a, b = b, a + b\n",
17+
" return b\n",
18+
"\n",
19+
"\n",
20+
"print(fib(100))"
21+
],
22+
"cell_type": "code",
23+
"outputs": [
24+
{
25+
"output_type": "stream",
26+
"name": "stdout",
27+
"text": [
28+
"354224848179261915075\n"
29+
]
30+
}
31+
],
32+
"execution_count": null
33+
}
34+
]
35+
}

static/dp/fib_memoization.ipynb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"nbformat": 4,
3+
"nbformat_minor": 2,
4+
"metadata": {},
5+
"cells": [
6+
{
7+
"metadata": {},
8+
"source": [
9+
"memo = [-1 for _ in range(10000)]\n",
10+
"\n",
11+
"\n",
12+
"def fib(n):\n",
13+
" if n == 0:\n",
14+
" return 0\n",
15+
" elif n == 1:\n",
16+
" return 1\n",
17+
" elif memo[n] != -1:\n",
18+
" return memo[n]\n",
19+
" else:\n",
20+
" memo[n] = fib(n - 1) + fib(n - 2)\n",
21+
" return fib(n - 1) + fib(n - 2)\n",
22+
"\n",
23+
"\n",
24+
"print(fib(100))"
25+
],
26+
"cell_type": "code",
27+
"outputs": [
28+
{
29+
"output_type": "stream",
30+
"name": "stdout",
31+
"text": [
32+
"354224848179261915075\n"
33+
]
34+
}
35+
],
36+
"execution_count": null
37+
}
38+
]
39+
}

static/dp/knapsack.ipynb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"nbformat": 4,
3+
"nbformat_minor": 2,
4+
"metadata": {},
5+
"cells": [
6+
{
7+
"metadata": {},
8+
"source": [
9+
"def knapsack(v, w, W):\n",
10+
" # 初期化\n",
11+
" dp = [[0 for _ in range(W + 1)] for _ in range(len(v) + 1)]\n",
12+
" # DP\n",
13+
" for i in range(1, len(v) + 1):\n",
14+
" for j in range(W + 1):\n",
15+
" if w[i - 1] > j:\n",
16+
" dp[i][j] = dp[i - 1][j]\n",
17+
" else:\n",
18+
" dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1])\n",
19+
" return dp[len(v)][W]\n",
20+
"\n",
21+
"\n",
22+
"print(knapsack([2, 3, 6], [2, 3, 5], 10))"
23+
],
24+
"cell_type": "code",
25+
"outputs": [
26+
{
27+
"output_type": "stream",
28+
"name": "stdout",
29+
"text": [
30+
"11\n"
31+
]
32+
}
33+
],
34+
"execution_count": null
35+
}
36+
]
37+
}

0 commit comments

Comments
 (0)