的計画法入門
目次
 グラフの基礎知識
 無向グラフ、有向グラフ
 閉路、DAG、トポロジカル順序
 グラフの表現方法
 DAGの問題の解き方
 最短経路、最長経路
 最大/最小の重みの合計
 連結しているか
 動的計画法とDAG
 DAGを使った動的計画法
 問題解説
 DAGへの落とし方
グラフの基礎知識
グラフとは
 丸が線で繋がれた図

 丸に情報をいれ、線で情報のつながりを表す
 丸に「駅名」、線で「電車でつながっている駅」を表す、
など
グラフとは
エッジ

ノード

エッジ

ノード

エッジ
ノード

エッジ

ノード

エッジ

ノード

エッジ

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

 線を「エッジ」や「辺」という
 あるノードから別のノードに行く行き方を「パス」や

「経路」という
 エッジにも情報を持たせることがある。(重み)
有向グラフ
 グラフの中で、一方通行なエッジがあるもの
 一方通行のエッジがないものは「無向グラフ」

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

 ノードに入っていくエッジを「入力辺」
 仕事順などを表せる

両方向通行可
の場合
閉路
 違うノードを通り、最初のノードに戻ることができる

とき、その経路を「閉路」という。
 つまり、一周回って戻ってこれる、ということ。

閉路あり

閉路あり

閉路なし!
DAG
 閉路を持たない有向グラフのこと
 動的計画法のキモです

これもDAG

閉路はない!
つまりDAG
これは違う
トポロジカル順序
 DAGにおいて、「ノードAからノードBにエッジがあ

るとき、必ずAがBより前にあるように並び替えた順
序」のこと

A

B

C

 トポロジカル順序は、
 「A → B → C → D → E」
 「A → B → D → C → E」

D

E
トポロジカル順序
 トポロジカル順序は複数存在するときもある
 DAGを仕事の順序を表したグラフと考えると、
「仕事が成立する順序」とも考えられる。

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

A

B

C

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

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];

 色々なグラフを表現可能。しかし若干めんどくさい。
グラフの表現(配列)
 配列での表現
int nodes[100];

int nodes[10][20];

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

現できる
 エッジはプログラミングでどうにか表現する
 nodes[i] = nodes[i-3] + 2; // node iとnode i-3を関連付け

 動的計画法の問題に適している
 ノードの番号(座標)も情報として活用できる

 複雑なグラフは再現しにくい
グラフの表現(配列)例
 node i と node i-3 を関連付けした場合

i: 0

1

2

3

4

5

6

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

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

が2のDAGが完成
DAGの問題の
解き方
グラフを使った問題
 グラフの問題には、例えば以下があります。
 ノードAからノードBに行くとき、
 通るエッジ数(ノード数)の最大/最小
 通るエッジの重みの合計の最大/最小
 総経路数(行き方の場合の数)

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

 実は、グラフが配列を使ったDAGの時は、これらの問

題は簡単に解くことができます
配列DAGの問題を解く(1)
 ノードAからノードBまでのなんちゃら系は、まずト

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

入力辺
調べる対象
のノード
これらは、もう調べられている
つまり、値が確定している
配列DAGの問題を解く(2)
 次に、調べ終わっているノードの値から、調べている

ノードの値を確定します。
 通るエッジの重みの合計の最大/最小なら
 あるノードに保存する情報は、「ノードAから、そのノー
ドに至るまでの重みの合計の最大/最小」とする
 あるノードに至るまでの重みの最大/最小は、その入力
ノードの情報+エッジの重みの中で、最大/最小のもの
4
2
8

3
10
1

←4+3、2+10、8+1の中で
最大/最小の値を書き込む
配列DAGの問題を解く(3)
 最大/最小を求めるときは、以下の式が便利です。
node[i] = max(node[i],node[j]+edge_val);
or
node[i] = min(node[i],node[j]+edge_val);

 この式をiの全部の入力ノードjに対して実行します
 iは固定でjが変わっていき、node[i]を更新するイメージ

 例えば、あるノードが、それより6つ前までのノード

全てとつながっている時は(範囲チェックは割愛)
for(int k=1;k<=6;k++) {
node[i] = min(node[i],node[i-k]+edge_val);
}

 最大/最小のエッジ数なら、重みを1で固定します
配列DAGの問題を解く(4)
 ノードAからノードBまでの総経路数の問題ならもっ

と話は簡単です。
 ノードに保存する情報は「ノードAからそのノードま
での総経路数」とします
 ノードをトポロジカル順序で調べていきます
 調べる内容は、ただ入力ノードの値を足すだけです。
 「確率」も同様にして求められます。
 ノードに確率を保存し、そのp倍を出力先ノードに足す
4

0.4

確率1/4

0.6

確率1/4

2

8

↑4+2+8通り

↑0.4*1/4 + 0.6*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)
 グラフ
i:

0

1

2

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

3

4

5
配列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に行くときの、最小エッジ

数を求めよ」と同義
 先ほどの問題と全く一緒
i:

0

1

2

3

4

5
問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増えるのでノードを一階

層ずらしてつなぎます。

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

k

i
問2:解説2
 「30ターンちょうどで」→

「30階層目で」
 求めるものは、「ノード(0,0,0)
j
から(9,9,35)に行く総経路数
を求めよ」と同義
k
 これはDAGで、トポロジカル
順序はkの昇順

i
問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 + 1/3*0.6

0.4

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

0.6

←1/3*0.4 + 1/3*0.6
皆確率1/3の時
j

問3:解説(2)

0

1

i

j:

2

3

4

5

6

7
問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)
j
i

ビー玉の
重さ2

ビー玉の
重さ1
問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/132350209
2
 DAGと動的計画法の関連についてわかりやすく、詳しく書いて
あります。今回扱っていない高度な話題もたくさんあります。
 「最速最強アルゴリズマー養成講座」
 http://www.itmedia.co.jp/enterprise/articles/1005/15/n
ews002_2.html
 今回の問題の出典元です。

 「プログラミングコンテスト チャレンジブック」
 通称蟻本です。動的計画法含め、様々なアルゴリズムについて
幅広く解説されています。
おわり
 ご清聴、ありがとうございました。

動的計画法入門(An introduction to Dynamic Programming)