ダイナミックプログラミング (Dynamic Programming, 動的計画法) とは、 最適化問題を解くために有効なアルゴリズムです。
n種類(整数個)の要素に関する最適解を求めるのに、 まず1種類の要素だけが使えるものとして、解を表に入れていきます。 そのあと、別の要素をもう1種類だけ追加して、表の値がどう変化するか を調べます。 それが終わると、また別の要素を1種類だけ追加して、表の値が どう変化するかを調べます。 このように要素を順に追加していき、全部の要素を追加し終った後の 表の状態で最適解がわかるというわけです。
具体例を見た方がわかりやすいので、次のナップザック問題を使って 動的計画法を説明します。
N種類の品物 Ai (ただし 1≦i≦n)の 「価格, Ai.value」と「大きさ, Ai.size」 が与えられた状態で、 大きさsの袋に品物をきっちり詰めたときの 「詰めた品物の価格の最大値」を求める問題です。
たとえば、次の表のような5種類の品物を適当にまぜて、 ある大きさの袋にきっちり詰めたときに、 合計価格の最大値がいくらになるかを考える問題が相当します。 ただし、品物1種類につき使える個数に制限がある場合と、 制限がない(同じものを何個でも詰めていい)場合があります。
品物 | |||
---|---|---|---|
品番 | 名前 | 大きさ | 価格 |
0 | orange | 2 | 2 |
1 | apple | 3 | 4 |
2 | grape | 5 | 7 |
3 | peach | 7 | 11 |
4 | melon | 9 | 14 |
どの品物も最大1個までしか使えない場合を考えてみましょう。
この場合はどの品物に関しても「使う or 使わない」のどちらかを 選ぶしかないわけですから、n個の品物があるときは 2n通りの組合せが考えられます。 Java風のアルゴリズムで表すと次のようになります。
ナップザック問題をバックトラックアルゴリズム(Java風)で書いた場合 |
// int[] aは品物の配列 // int s は袋のサイズ // int vmax は価格の最大値 for (x0=0; x0 <= 1; x0++) { // 品物A1 int s1 = s - a[0].size * x0; for (x1=0; x1 <= 1; x1++) { // 品物A2 int s2 = s1 - a[1].size * x1; ... for(xn-1=0; xn-1 <= 1; xn-1++) { // 品物An-1 if (sn-1 - a[n-1].size * xn-1 == 0) { // 袋にいっぱいならば int v = a[0].value * x0 + a[1].value * x1 + ... + a[n-1].value * xn-1; // 価格を計算して if (vmax < v) vmax = v; // 最大価格の更新 } } } } |
バックトラックで求めようとすると、 品物Ai(,ただし1≦i≦n)に関して、 「Aiの大きさ」を Ai.size 「Aiの価格」を Ai.value とすると
n s = Σ Ai.size × xi i=1となる Aviを繰り返しによって求めて、そのときの
n s = Σ Avi.value × xi i=1を計算して最大値を求めることになります。 Java風のアルゴリズムで表すと次のようになります。
ナップザック問題をバックトラックアルゴリズム(Java風)で書いた場合 |
// int[] aは品物の配列 // int s は袋のサイズ // int vmax は価格の最大値 for (x0=0; x0 <= s /a[0].size; x0++) { // 品物A1 int s1 = s - a[0].size * x0; for (x1=0; x1 <= s1 / a[1].size; x1++) { // 品物A2 int s2 = s1 - a[1].size * x1; ... for(xn-1=0; xn-1 <= sn-1 / a[n-1].size; xn-1++) { // 品物An-1 if (sn-1 - a[n-1].size * sn-1 == 0) { // 袋にいっぱいならば int v = a[0].value * x0 + a[1].value * x1 + ... + a[n].value * xn; // 価格を計算して if (vmax < v) vmax = v; // 最大価格の更新 } } } } |
品物の種類を n, 袋の大きさをmとすると、バックトラック法では 計算は各品物について(m / 品物のサイズ)回必要で、 これをバックトラックしながらn段に渡って再帰呼び出ししますから 全体で(m / 品物のサイズ)n回の計算が必要です。 計算量は O (mn) となります。
下の実行例では m=500で 0.84秒、m=1000で10.44秒, m=2000で158秒と急激に実行時間が増えていることがわかります。
さらに、n (品物の種類)を増やした場合は指数オーダーで計算時間は増加するので、 たとえば n=20, m=500 程度でもとても現実に計算するのは不可能な計算 となってしまいます。
RunKnapsackBT.javaの実行例 |
|
knapsack01.txt |
|
knapsack02.txt |
|
動的計画法は、
品物が1個ずつしか使えないナップザック問題を考えます。
以下では、 品物0〜iだけを使って大きさsの袋にきっちりと詰め込むナップザック問題の 解を、袋の大きさsの関数として fi(s) と記述しています。
まず、使える品物が何もない初期状態を考えます。 すなわち、袋の大きさ=0で価値=0です。
これを使って、品物0が使えるようになった場合の計算を行います。 大きさsの袋に品物をきっちり詰め込めた時の最大価値 f0(s) を表にします。
次に、品物1が新たに使えるようになったとして、f0(s) を参照しながらf1(s)を求めます。
品物0〜1が使える場合のf1(s)が求まった状態で、 新たに品物2が使えるようになった場合の計算を表したのが 次の図です。 大きさs=8の袋に品物2を詰め込む場合は、f1(3) すなわち f1(s - 品物2の大きさ) を参照して計算しています。
すなわち、品物i+1の大きさをsizei+1、価値をValuei+1 とすると
fi(s - sizei+1) + Valuei+1 と fi(s)を比較して、大きい方をfi+1(s)とすればよいのです。
実際にプログラミングするときは次のようにするとよいでしょう。
品物を0種類から始めて、品物の種類を1つずつ増やしていき、 品物全部について表を完成させると次のようになります。
品物が何個でも使えるナップザック問題を考えます。
f1(s)を参照しながら、品物2も使える場合の計算を している様子が次の図です。 大きさs=13の袋に品物2を詰め込む場合は、f1(8) すなわち f1(s - 品物2の大きさ) を参照して計算しています。 ただし、この場合は品物2を複数個使ってもいいわけですから 「品物2も使った場合の最大値」である f2(8)も 参照する必要があります。
式で表すと次のように説明できます。
すなわち、同じ品物が何個も使える問題では
fi+1(s - sizei+1) + Valuei+1 と fi(s)を比較して、大きい方をfi+1(s)とすればよいのです。
品物を0種類から始めて、品物の種類を1つずつ増やしていき、 品物全部について表を完成させると次のようになります。 「その結果を得るために最後に加えた品物」も別に管理しておけば、 その結果を得るために必要な各品物を求めることができます。
また、「その結果を得るために最後に加えた品物」も記憶しておけば、 その最適解を得るために必要な要素(品物)もわかります。
品物の種類を n, 袋の大きさをmとすると、 動的計画法では計算は各品物についてm回必要で(表を小さい方から大きい方へ)、 それを品物の種類nだけ繰り返しますから、計算量は O (m×n) となります。
下の実行例では m=500で 0.09秒、m=1000でもm=2000でも 0.11秒と、 もともと高速であり、しかも mを増やしても実行時間はゆっくりと しか増えないことがわかります。
品物の種類 n を増やしたときでさえ、計算時間はゆっくりとしか増えていきません。
RunKnapsack.javaの実行例 |
|
選んだ品物を表示するように変更してみましょう。
「ある袋の大きさに対して、最大の価値を更新する場合は、 最後に追加した品物を記憶しておく」ことで追加した品物を たどることができます。
KnapsackChoice.javaの変更点 |
|
RunKnapsackChoice.javaの変更点 |
|
RunKnapsackChoice.javaの実行例 |
|
今回解説したのは、「袋にきっちり詰めるナップザック」の問題ですが、 「袋に空間があっても構わない」問題も同様な方法で解くことができます。
「袋にきっちり詰めるナップザック」の問題における 「袋の大きさsの時の解」を f(s) と書くことにすると 「袋に空間があっても構わない」問題では「1≦i≦sにおけるf(i)の最大値」 が解となります。
作成したプログラムが正しく動作することを確認したら、それぞれの 提出先に提出しなさい。
提出した後は、正しく提出されていることを http://ynitta.com/class/algoC/local/handin/ で必ず確認しておいて下さい。
課題提出〆切は次回の講義の開始時刻です。
提出ファイル | KnapsackChoice.java |
---|---|
コメント欄: | knapsack02.txtを入力としたとき、袋の大きさ112でのRunKnapsackChoice.javaの出力 |
提出先: | 「宿題提出Web:アルゴリズムc:課題7a」 http://ynitta.com/class/algoC/local/handin/up.php?id=kadai7a |
RunKnapsackChoice.javaを動作させて下さい。
提出ファイル | DPSum.java |
---|---|
コメント欄: | dpsum02.txtを入力としたとき、RunDPSum.javaの出力 |
提出先: | 「宿題提出Web:アルゴリズムc:課題7b」 http://ynitta.com/class/algoC/local/handin/up.php?id=kadai7b |
棒が何本か与えられた時、その棒の中からいくつかを選んで継ぎ足して (継ぎ足しに必要な長さは0とする)「指定された長さ」ちょうどにする問題を 考えよう。 あなたの仕事は、この問題を解いて何本の棒で「指定された長さ」にできるかを 答えるプログラムを書くことである。 答が何通りかある場合は、そのうちの最小の本数を答えること。 もし、与えられた棒を使って「指定された長さ」ちょうどにできない場合は -1と答えなさい。
入力データは複数のデータセットから成る。 データセットの終わりは2つの 0 で表される。
Dataset1 Dataset2 ... Dataset2 0 0
各データセットは次のような正の整数から成る。
M N D1 D2 ... DN
Mは「指定された長さ」である(1≦M≦10000)。 Nは棒の総数(1≦N≦1000)で、 Di は各棒の長さを表す(1≦Di≦1000)。
各データセットに対して、答えとなる整数1個を含んだ1行を 出力すること。
サンプル入力 |
|
サンプル出力 |
|
dpsum02.txt |
|
main()メソッドは次のように書くことができるでしょう。 DPSum.java を自分で作って下さい。 「個数制限ありのナップザック問題」となります。
提出ファイル | Pollock.java |
---|---|
コメント欄: | C1_in.txtを入力としたとき、Pollock.javaの出力 |
提出先: | 「宿題提出Web:アルゴリズムc:課題7c」 http://ynitta.com/class/algoC/local/handin/up.php?id=kadai7c |
「ACM ICPC2010 東京大会予選問題C」より改題。
n 番目の正三角形数は,最初の n 個の正整数の和と定義される.
n 番目の正四面体数は,最初の n 個の正三角形数の和と定義される.
簡単に示せるように,n 番目の正四面体数は n(n+1)(n+2) ⁄ 6 に等しい.
たとえば,5番目の正四面体数は 1+(1+2)+(1+2+3)+(1+2+3+4)+(1+2+3+4+5)
= 5×6×7 ⁄ 6
= 35 である.
最初の5個の正三角形数
1, 3, 6, 10, 15
最初の5個の正四面体数
1, 4, 10, 20, 35
1850年,職業数学者ではなく英国の法律家でありトーリー党(現在の保守党)の政治家でもあった初代准男爵フレデリック・ポロック卿が,すべての正整数は5個以内の正四面体数の和として表現できると予想した. ただし,和の中で同じ正四面体数が複数回出現してよく,その場合,それぞれの出現を別々に数えるものとする. この予想は一世紀半以上も未解決なままである.
あなたの任務は, 個別の整数に対してポロック予想が成り立つことを確認するプログラムを書くことである. あなたのプログラムは, 入力された整数おのおのについて, それを正四面体数の和として表すための正四面体数の個数の最小値を計算しなくてはならない.
たとえば,40自体は正四面体数ではないが、 40は2個の正四面体数の和 4×5×6 ⁄ 6 + 4×5×6 ⁄ 6 として表すことができる. したがって,あなたのプログラムに40が与えられると, 2と答えなくてはならない.
入力は行の並びで,
おのおのの行には 106 より小さい正整数がちょうど一つだけ含まれている.
入力の終わりは,一文字の
入力された正整数おのおのについて, 入力された整数を正四面体数の和として表すために必要な正四面体数の個数の 最小値を含んだ行を出力せよ. 出力に余分な文字が含まれてはならない.
40 14 5 165 120 103 106 139 0
2 2 2 1 1 5 4 3