動的計画法入門(an introduction to dynamic programming)

55
的計画法入門

Upload: kakira9618

Post on 20-Jul-2015

1.629 views

Category:

Technology


0 download

TRANSCRIPT

的計画法入門

目次

グラフの基礎知識

無向グラフ、有向グラフ

閉路、DAG、トポロジカル順序

グラフの表現方法

DAGの問題の解き方

最短経路、最長経路

最大/最小の重みの合計

連結しているか

動的計画法とDAG

DAGを使った動的計画法

問題解説

DAGへの落とし方

グラフの基礎知識

グラフとは

丸が線で繋がれた図

丸に情報をいれ、線で情報のつながりを表す

丸に「駅名」、線で「電車でつながっている駅」を表す、など

グラフとは

丸を「ノード」や「頂点」

線を「エッジ」や「辺」という

あるノードから別のノードに行く行き方を「パス」や「経路」という

エッジにも情報を持たせることがある。(重み)

エッジ

エッジ エッジ エッジ

エッジ エッジ

ノード ノード ノード ノード ノード

有向グラフ

グラフの中で、一方通行なエッジがあるもの

一方通行のエッジがないものは「無向グラフ」

ノードから出て行くエッジを「出力辺」

ノードに入っていくエッジを「入力辺」

仕事順などを表せる

両方向通行可 の場合

閉路

違うノードを通り、最初のノードに戻ることができるとき、その経路を「閉路」という。

つまり、一周回って戻ってこれる、ということ。

閉路あり 閉路あり 閉路なし!

DAG

閉路を持たない有向グラフのこと

動的計画法のキモです

閉路はない! つまりDAG

これもDAG

これは違う

トポロジカル順序

DAGにおいて、「ノードAからノードBにエッジがあるとき、必ずAがBより前にあるように並び替えた順序」のこと

トポロジカル順序は、

「A → B → C → D → E」

「A → B → D → C → E」

A B C D E

トポロジカル順序

トポロジカル順序は複数存在するときもある

DAGを仕事の順序を表したグラフと考えると、「仕事が成立する順序」とも考えられる。

トポロジカル順序にノードを並び替えることを「トポロジカルソート」という。

A B C D E

A → B → C → D → E または A → B → D → C → E なら 仕事が成立する。

グラフの表現(構造体)

構造体の配列での表現

色々なグラフを表現可能。しかし若干めんどくさい。

typedef struct _node {

int data; //ノードの情報 } Node;

typedef truct _edge {

Node *pNode1;

Node *pNode2;

int weight; //重み } Edge;

Node nodes[100];

Edge edges[10000];

グラフの表現(配列)

配列での表現

ノードが線、長方形、直方体状に並んだグラフを簡単に再現できる

エッジはプログラミングでどうにか表現する

nodes[i] = nodes[i-3] + 2; // node iとnode i-3を関連付け

動的計画法の問題に適している

ノードの番号(座標)も情報として活用できる

複雑なグラフは再現しにくい

int nodes[100]; int nodes[10][20];

グラフの表現(配列)例

node i と node i-3 を関連付けした場合

このグラフはDAGです(トポロジカル順序はiの昇順)

nodes[i] = nodes[i-3] + 2 とすれば、エッジの重みが2のDAGが完成

i : 0 1 2 3 4 5 6

DAGの問題の 解き方

グラフを使った問題

グラフの問題には、例えば以下があります。

ノードAからノードBに行くとき、

通るエッジ数(ノード数)の最大/最小

通るエッジの重みの合計の最大/最小

総経路数(行き方の場合の数)

ノードAとノードBがつながっているかどうか

実は、グラフが配列を使ったDAGの時は、これらの問題は簡単に解くことができます

配列DAGの問題を解く(1)

ノードAからノードBまでのなんちゃら系は、まずトポロジカル順序で調べていきます。

トポロジカル順序で調べているので、あるノードを調べている時、そのノードにつながっている入力辺の先のノード(入力ノード)はすでに調べているはずです。

入力辺

調べる対象 のノード

これらは、もう調べられている つまり、値が確定している

配列DAGの問題を解く(2)

次に、調べ終わっているノードの値から、調べているノードの値を確定します。

通るエッジの重みの合計の最大/最小なら

あるノードに保存する情報は、「ノードAから、そのノードに至るまでの重みの合計の最大/最小」とする

あるノードに至るまでの重みの最大/最小は、その入力ノードの情報+エッジの重みの中で、最大/最小のもの

4

2

8 1

10

3

←4+3、2+10、8+1の中で 最大/最小の値を書き込む

配列DAGの問題を解く(3)

最大/最小を求めるときは、以下の式が便利です。

この式をiの全部の入力ノードjに対して実行します

iは固定でjが変わっていき、node[i]を更新するイメージ

例えば、あるノードが、それより6つ前までのノード全てとつながっている時は(範囲チェックは割愛)

最大/最小のエッジ数なら、重みを1で固定します

node[i] = max(node[i],node[j]+edge_val);

node[i] = min(node[i],node[j]+edge_val);

or

for(int k=1;k<=6;k++) {

node[i] = min(node[i],node[i-k]+edge_val);

}

配列DAGの問題を解く(4)

ノードAからノードBまでの総経路数の問題ならもっと話は簡単です。

ノードに保存する情報は「ノードAからそのノードまでの総経路数」とします

ノードをトポロジカル順序で調べていきます

調べる内容は、ただ入力ノードの値を足すだけです。

「確率」も同様にして求められます。

ノードに確率を保存し、そのp倍を出力先ノードに足す

4

2

8 ↑4+2+8通り

0.4

0.6 ↑0.4*1/4 + 0.6*1/4 確率1/4

確率1/4

配列DAGの問題を解く(5)

「ノードAとノードBが連結しているか」も同じように考えます。

面倒臭かったら、経路数が0かどうかを調べても良いです

ノードに保存する情報は、ノードAとそのノードがつながっているなら1、そうでないなら0とします

ノードAはノードAと連結していると考えます。

ノードAの値は1、それ以外はとりあえず0

トポロジカル順序で考えた時、全ての入力ノードの内、一つでも1があれば1、一つもなければ0としていきます

若干効率は悪いですが… 今回はこれで行きましょう

1

0

0

←1があるので、 1を書き込みます

配列DAGの問題を解く(6)

初期条件には注意が必要です。

最大を求める問題なら、最初から最大値が決定しているノード以外のノードの値は、-∞で初期化します

実際には-∞は入れられないので、-INTMAXで我慢します

最小ならば、+∞で初期化します

経路数なら、明らかなところを1で、そうでないところを0で初期化します。

連結しているかどうかは、連結しているかどうかの対象ノードの値を1に、それ以外を0に初期化します。

配列DAGの問題を解く(7)

実際に考えてみましょう

線状に並んだ101個のノードが数直線上にある。これらのノードは「素数個」後のノードと繋がっている

2個後、3個後、5個後、7個後、11個後…と隣接

範囲外のノードは繋がっていないと考える。

この時1個目のノードから101個目のノードに行く

行き方は何通りあるか

最小のエッジ数は何個か

配列DAGの問題を解く(8)

0 1 2 3 4 5 i :

グラフ

トポロジカル順序はiの昇順

配列DAGの問題を解く(9)

int nodes[101] = {0}; //行き方を格納

int primes[25] =

{2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71

,73,79,83,89,97};

nodes[0] = 1; //0番に行く行き方だけは1

//トポロジカルソート順に調べる

for(int i = 0; i < 101; i++) {

for(int j = 0; j < 25; j++) {

if (i – primes[j] >= 0) { //範囲チェック

nodes[i] += node[i - primes[j]];

}

}

}

std::cout << nodes[100] << std::endl;

行き方

配列DAGの問題を解く(10)

int nodes[101]; //最小のエッジ数を格納

int primes[25] =

{2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71

,73,79,83,89,97};

nodes[0] = 0; //0番は0個のエッジ数で到達できる

for(int i = 1; i < 101; i++) nodes[i] = 10000000; //+∞

//トポロジカルソート順に調べる

for(int i = 0; i < 101; i++) {

for(int j = 0; j < 25; j++) {

if (i – primes[j] >= 0) { //範囲チェック

nodes[i] = min(nodes[i],

nodes[i - primes[j]] + 1);

}

}

}

std::cout << nodes[100] << std::endl;

最小のエッジ数

動的計画法とDAG

動的計画法とDAG

すでに分かっている答えを組み合わせて、新しい答えを作っていく方法

具体的には、制約が簡単な問題の答えから、より難しい制約の問題を解いていく。

DP(Dynamic Programming)と言われる

通常の調べ上げより処理が圧倒的に軽い(𝑎𝑛 → 𝑛𝑎)

動的計画法の問題は、基本的にDAGに落とせる

DAGに落とせれば、先程の解法が使える

配列を使ったDAGに落とすことが多いが、その時の添字も情報として活用する

例題

100を素数の和で表すとき、その最小項数を求めよ

例:8 = 2+2+2+2 = 2+3+3 = 5+3 だから、最小項数は2

例題:ヒント

100を素数の和で表すとき、その最小項数を求めよ

ターゲットの自然数をいきなり分解しようとすると難しい。

とりあえず、DAGに落とせないか考えよう

dp[i]に「iを素数の和で表すときの、最小項数」を入れてみる。

なぜこの配列を考えようと思ったのかは、後解説します

dp[i]とdp[i-素数]の関係を考えると…

例題:解説

dp[i] := iを素数の和で表すときの、最小項数

足した素数分だけ右に行き、使ったエッジ数が1増える

「ノード0からノード100に行くときの、最小エッジ数を求めよ」と同義

先ほどの問題と全く一緒

0 1 2 3 4 5 i :

問1

100万円以下の金額が与えられる。10種類の小切手(𝑖種類目の小切手の金額は𝐴[𝑖](> 0))で支払いをする時、支払う合計枚数の最小値を出力しなさい。

出典:『最速最強アルゴリズマー養成講座』(編)

問1:解説

dp[i] := i万円払うときの小切手の最小枚数

1枚使うごとに枚数が増える。iはA[i]だけ増える

「枚数」は「使ったエッジ数」に置き換えられる

例題と同じ!

問2

将棋の「銀」の動きをするコマが、10*10の盤面の一番左下にある。35ターンちょうどで、一番右上に行く進み方は何通りあるか。

問2:ヒント

地形がある問題は、その地形の形でノードを置くと良いです。

dp[i][j]など。

その上でノードのつながりを考えます。今回は銀が移動するので、左上、上、右上、右下、左下のノードと繋がっていると考えられます

しかしこれだとDAGになりません。こういう時はもう一次元何かを増やすとうまくいくことが多いです。何かもう一つ制約は無かったでしょうか?

銀の動きをグラフに してみたがDAGにならない→

問2:解説(1)

今回は、ターン数でもう一次元拡張します。

dp[i][j][k] :=kターンで座標(i,j)に行く行き方

一回移動すると、ターンが1増えるのでノードを一階層ずらしてつなぎます。

i

j

k 全てのノードが一階層下の 左上、上、右上、右下、左下 と繋がっている

問2:解説2

「30ターンちょうどで」→

「30階層目で」

求めるものは、「ノード(0,0,0)

から(9,9,35)に行く総経路数

を求めよ」と同義

これはDAGで、トポロジカル

順序はkの昇順

i

j

k

問2:解説(3) int dp[10][10][36] = {0}; //kターンで(i,j)に行く進み方の数

int dirX[5] = {-1,0,1,-1,1}; //銀の進む方向(x)

int dirY[5] = {1,1,1,-1,-1}; //銀の進む方向(y)

dp[0][0][0] = 1;

for(int k = 1; k <= 35; k++) { //トポロジカル順序はkの昇順

for(int i = 0; i < 10; i++) {

for(int j = 0; j < 10; j++) {

for(int l=0;l<5;l++) {

int px = i - dirX[l];

int py = j - dirY[l];

if(isIN(px,py)) { //範囲チェック

dp[i][j][k]+=dp[px][py][k-1];

}

}

}

}

}

std::cout << dp[9][9][35] << std::endl;

問3

さいころを振って進むすごろくがある。ただし、ちょうど6𝑛 マス(𝑛 > 0)進むと、スタートに戻される。20回さいころを振ったとき、スタート地点から30マス以上進んでいる確率を求めよ。

出典:『最速最強アルゴリズマー養成講座』(編)

問3:ヒント

一応、すごろくなので、地形があります。

地形と言っても「何マス進んだか」の一直線ですが…

サイコロを振って進むので、各ノードは6つ先までのノード全てとつながっていると言えます

ただし、 6𝑛個目のノードにはつなぐときは、スタートにつなぎ変えます

しかし、これではDAGになりません。何か制約を加えてDAGにしましょう。

問3:解説(1)

「サイコロを振った回数」を加えて次元を拡張します

dp[i][j] := i回サイコロを振った時jマス進んでいる確率

i階層目の各ノードは、i+1階層目の6つ先までのノードと繋がっている

6𝑛個目の場合は、i+1階層目のスタートにつなぎ替える。

ある一つのエッジを進む確率は、エッジの出処のノードの値の1/6

この値をエッジの先のノードに足していく。

皆確率1/3の時

0.4

0.6

←1/3*0.4 + 1/3*0.6

←1/3*0.4 + 1/3*0.6

←1/3*0.4 + 1/3*0.6

問3:解説(2)

j : 0 1 2 3 4 5 6 7

j

i

問3:解説(3) double dp[21][121]={0}; //i回サイコロを振ってjマス進む確率

dp[0][0] = 1.0; //スタートマスの確率は1

for(int i = 1; i <= 20; i++) { //トポロジカル順序はiの昇順

dp[i][0] = 1.0 / 6.0;

for(int j = 1; j < 121; j++) {

if (j % 6 != 0) {

for(int k = 1; k <= 6; k++) {

if (j – k >= 0) { //範囲チェック

dp[i][j] += dp[i-1][j-k]/6.0;

}

}

}

}

}

double ans=0.0;

for(int j = 30; j < 121; j++) ans += dp[20][j];

std::cout << ans << std::endl;

問4

ある大小バラバラの1000個以下のビー玉が与えられる。ビー玉の重さが1~10の整数だとして、これを2人で分けるとき、合計の重さを均等にすることが可能か求めなさい

出典:『最速最強アルゴリズマー養成講座』

問4:ヒント

ビー玉の合計の重さをSとすると、Sが偶数でない場合不可能。偶数の時、「何個かのビー玉を選んで、合計がS/2となる選び方が存在するか」と言い換えられる。

「~を選んで」の問題の場合、「~をi番目まで考慮した時の」と機械的に1次元増やすとうまくいく。

これと、合計を配列の添字にすると…

問4:解説(1)

dp[i][j] := i個目のビー玉まで考慮した時、選んだビー玉の重さの合計がjとなるか(1:可 0:不可)

「選択」を「 i個目の~まで考慮した時」とした時は、選択肢の数だけ、エッジが出ます

今回は、選ぶか選ばないかの2つのエッジが出ます

選んだ場合は、i番目のビー玉の重さだけ右にズレて1階層下のノードとつながります。選ばない場合は直下のノードとつながります。

後は、(0,0)のノードと(1000,S/2)のノードが連結しているかという問題を解くだけです。

もう少し効率のよい方法もあるらしい

問4:解説(2)

ビー玉の重さ2

ビー玉の重さ1

j

i

問4:解説(3) int dp[1001][5001] = {0}; // i個目のビー玉まで考慮した時、選んだ

ビー玉の重さの合計がjとなるか dp[0][0] = 1;

int weight[1000]; //ビー玉の重さ

int sum = 0; //ビー玉の重さの合計

for(int i = 0; i < 1000; i++) sum += weight[i];

if (sum % 2 == 0) {

for(int i = 1; i <= 1000; i++) { //トポロジカル順序

for(int j = 0; j < 5001; j++) {

if(j - A[i - 1] >= 0) { //範囲チェック

dp[i][j] |= dp[i-1][j-A[i-1]];

}

dp[i][j] |= dp[i-1][j];

}

}

}

if (dp[1000][sum/2]) std::cout << “possible” << std::endl;

else std::cout << “impossible” << std::endl;

DAGへの落とし方(1)

まず、問題に関わる制約を全て書き出します。

その制約を「dp配列の添字または値にするもの」と「グラフのつながりを考えるために使用するもの」にグループ分けします

大まかには

範囲指定されている制約は配列の添字になりやすい

求めるものは、配列の値になりやすい

その他、問題を解いていく途中に値がコロコロ変わるものは配列の添字/値になりやすい

選択肢、地形も配列の添字になる

それ以外はグラフのつながりを表すものになる

DAGへの落とし方(2)

特に配列の添字になりやすいもの

ターン数、日数、手数、(試行)回数

合計

地形

選択(「i番目まで考慮した時の~」に変更して添字に)

特にグラフのつながり方を表すものになりやすいもの

移動規則(広い意味での「座標」を移動させるルール)

恒等的に同じ中身の配列

問1

100万円以下の金額が与えられる。10種類の小切手(𝑖種類目の小切手の金額は𝐴[𝑖](> 0))で支払いをする時、支払う合計枚数の最小値を出力しなさい。

dpの添字または値になるもの 金額…問題を解いていく途中にコロコロ変わる。また、範囲指

定がされていて、金額の合計と言い換えられる。

枚数…求めるものだから、dp配列の値になりやすい。コロコロ変わる。

グラフのつながりを表すもの 小切手の金額の配列…問題文中は常に一定。金額の合計を移動

させるものと考えることができる

結果:dp[i] := i円払うのにかかる合計枚数の最小値

問2

将棋の「銀」の動きをするコマが、10*10の盤面の一番左下にある。30ターンちょうどで、一番右上に行く進み方は何通りあるか。

dpの添字または値になるもの 盤面…立派な地形。添字になりやすい。

ターン…特に添字になりやすいものの代表。解いている途中にコロコロかわり、30ターン以内という制限もある

通り…求めるものなので値になりやすい。

グラフのつながりを表すもの コマの動き…移動手段。地形とセットでよく出てくる。

結果:dp[i][j][k] := kターンで(i,j)に行く進み方の数

問3 さいころを振って進むすごろくがある。ただし、ちょうど6𝑛 マ

ス(𝑛 > 0)進むと、スタートに戻される。20回さいころを振ったとき、スタート地点から30マス以上進んでいる確率を求めよ。

dpの添字または値になるもの

回数…添字になりやすい典型です

マス…基本的に、出た目の合計と考えられます。問題を解いている途中にコロコロ変わり、範囲指定もあります。

確率…求めるものです。値になりやすいです。

グラフのつながりを表すもの

さいころ…移動規則です。進んだマスを増やすものと考えられます。問題文中は一定です。

戻る…さいころと同じで、移動規則です。

結果:dp[i][j] := i回さいころを振った時jマス進んでいる確率

問4

ある大小バラバラの1000個以下のビー玉が与えられる。ビー玉の重さが1~10の整数だとして、これを2人で分けるとき、合計の重さを均等にすることが可能か求めなさい

dpの添字または値になるもの ビー玉の個数…二人の内どちらかに振り分けると考えれば、選

択と考えられます。範囲指定もあります。添字です。

合計の重さ…「合計」です。添字になります。

グラフのつながりを表すもの ビー玉の重さ…問題を解く間一定です。合計の重さを移動させ

るものと考えることができます

結果:dp[i][j] := i個目のビー玉まで考慮した時、合計の重さがjとなるか

参考文献等

「DPの話」

http://d.hatena.ne.jp/Tayama/20111210/1323502092

DAGと動的計画法の関連についてわかりやすく、詳しく書いてあります。今回扱っていない高度な話題もたくさんあります。

「最速最強アルゴリズマー養成講座」

http://www.itmedia.co.jp/enterprise/articles/1005/15/news002_2.html

今回の問題の出典元です。

「プログラミングコンテスト チャレンジブック」

通称蟻本です。動的計画法含め、様々なアルゴリズムについて幅広く解説されています。

おわり

ご清聴、ありがとうございました。