SlideShare a Scribd company logo
1 of 180
Download to read offline
NPCA夏合宿講義
● 合宿初参加の身なのに講義を頼まれました 
● なにが講義だえらそうに 
● 許して
今日するお話 
● 全探索 
– DFS(深さ優先探索) 
– BFS(幅優先探索) 
● 動的計画法(DP) 
– メモ化再帰 
– DP
   全探索
全探索 
● 物事には状態がある 
– 年齢、身長、etc... 
● 状態がどう変化していくか調べたい
全探索 
● (例) 
1,2,3から一つずつ数字をとっていく時とる順番 
は何通りありますか? 
– 3! = 6通り
状態の変化 
1 
2 
2 
3 
3 
1 
3 
1 
2 
はじめ 
3 
2 
3 
1 
2 
1
状態の変化 
1 
2 
2 
3 
3 
1 
3 
1 
2 
はじめ 
3 
2 
3 
1 
2 
1 
それぞれが1通り 
のとり方に対応
状態の変化 
1 
2 
2 
3 
3 
1 
3 
1 
2 
はじめ 
3 
2 
3 
1 
2 
1 
それぞれが1通り 
のとり方に対応
状態の変化 
1 
2 
2 
3 
3 
1 
3 
1 
2 
はじめ 
3 
2 
3 
1 
2 
1 
それぞれが1通り 
のとり方に対応
状態の変化 
1 
2 
2 
3 
3 
1 
3 
1 
2 
はじめ 
3 
2 
3 
1 
2 
1 
それぞれが1通り 
のとり方に対応
状態の変化 
1 
2 
2 
3 
3 
1 
3 
1 
2 
はじめ 
3 
2 
3 
1 
2 
1 
それぞれが1通り 
のとり方に対応
状態の変化 
1 
2 
2 
3 
3 
1 
3 
1 
2 
はじめ 
3 
2 
3 
1 
2 
1 
それぞれが1通り 
のとり方に対応
全探索 
● こうした状態の変化をすべて調べつくしたい 
● 2つの方法があります
全探索 
● こうした状態の変化をすべて調べつくしたい 
● 2つの方法があります 
– DFS(深さ優先探索) 
– BFS(幅優先探索)
全探索 
● こうした状態の変化をすべて調べつくしたい 
● 2つの方法があります 
– DFS(深さ優先探索) 
– BFS(幅優先探索) 
● 2つの違い 
– 探索する順序
DFS 
● こういう状態の遷移があるとする
DFS 
1
DFS 
1 
2
DFS 
1 
3 
2
DFS 
1 
3 
4 
2
DFS 
1 
3 
4 
2
DFS 
1 
3 
4 5 
2
DFS 
1 
3 
4 5 
2
DFS 
1 
3 
2 
4 5 6
DFS 
1 
3 
2 
4 5 6
DFS 
1 
3 
2 
4 5 6
DFS 
1 
3 
2 
4 5 6
DFS 
1 
3 
4 
7 
2 
5 6
DFS 
1 
3 
4 
7 
8 
2 
5 6
DFS 
1 
3 
4 
7 
8 
2 
5 6 9
DFS 
1 
3 
4 
7 
8 
2 
5 6 9
DFS 
1 
3 
4 
7 
8 
2 
5 6 9
DFS 
1 
3 
4 
7 
8 
9 
10 
2 
5 6
DFS 
1 
3 
4 
7 
8 
10 
9 11 
2 
5 6
DFS 
1 
3 
4 
7 
8 
10 
9 11 
2 
5 6
DFS 
1 
3 
4 
7 
8 
10 
9 11 
2 
5 6
DFS 
1 
3 
4 
7 
8 
10 
9 11 
2 
5 6
DFS 
1 
3 
4 
7 
8 
12 
10 
9 11 
2 
5 6
DFS 
1 
2 
3 13 
4 
7 
8 
12 
10 
9 11 
5 6
DFS 
1 
2 
3 13 
14 
4 
7 
8 
12 
10 
9 11 
5 6
DFS 
1 
2 
3 13 
14 
4 
7 
8 
12 
10 
9 11 
5 6
DFS 
1 
2 
3 13 
14 
4 
7 
8 
12 
10 
9 11 15 
5 6
DFS 
1 
2 
3 13 
14 
4 
7 
8 
12 
10 
9 11 15 
5 6
DFS 
1 
2 
3 13 
14 
4 
7 
8 
12 
10 
9 11 15 
5 6
DFS 
1 
2 
3 13 
14 
4 
7 
8 
12 
10 
9 11 15 
5 6
DFS 
1 
2 
3 13 
14 
4 
7 
8 
12 
10 
9 11 15 
5 6
DFS 
● 行けるところまで行く 
– それ以上いけなくなった戻る 
● どうやって実装するの? 
– スタックを用いる 
– 関数の再帰を用いる
スタックってなんやねん 
● データ構造の一つ 
● pushとpopという操作がある 
– push ‥‥ スタックの一番上に積む 
– pop ‥‥ スタックの一番上から取り出す 
● できることはこれだけ
スタック(stack)
DFS 
1 
1 
  スタック 
1をpush
DFS 
1 
2 
1 
  スタック 
2をpush 
2
DFS 
1 
3 
2 
3 
1 
  スタック 
3をpush 
2
DFS 
1 
3 
4 
2 
4 
3 
1 
  スタック 
4をpush 
2
DFS 
1 
3 
4 
2 
4 
3 
1 
  スタック 
もうこれ以上進めない 
2
DFS 
1 
3 
4 
2 
3 
1 
  スタック 
4をpop 
2 
スタックの一番上が 
今見ている状態
DFS 
1 
3 
4 5 
2 
3 
1 
  スタック 
5をpush 
2 
スタックの一番上が 
今見ている状態 
5
● というようにスタックが空になるまで続ける 
● 関数の再帰を使えばもっと簡単に実装できる 
– 関数の再帰はスタックを用いて実現されている 
● 進めなくなってpopするのが関数でreturnするのに相当
関数が再帰する様子 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
int main(){ 
dfs(1); 
return 0; 
}
関数が再帰する様子 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
retrun; 
}
関数が再帰する様子 
dfs(2) 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
retrun; 
}
関数が再帰する様子 
dfs(2) 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(3)
関数が再帰する様子 
dfs(2) 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(3) 
dfs(4)
関数が再帰する様子 
dfs(2) 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(3) 
dfs(4)
関数が再帰する様子 
dfs(2) 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(3) 
dfs(5)
関数が再帰する様子 
dfs(2) 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(3) 
dfs(5)
関数が再帰する様子 
dfs(2) 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(3)
関数が再帰する様子 
dfs(2) 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(4)
関数が再帰する様子 
dfs(2) 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(4)
関数が再帰する様子 
dfs(2) 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
}
関数が再帰する様子 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
}
関数が再帰する様子 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(3)
関数が再帰する様子 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(3) 
dfs(4)
関数が再帰する様子 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(3)
関数が再帰する様子 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(3) 
dfs(5)
関数が再帰する様子 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
} 
dfs(3)
関数が再帰する様子 
dfs(1) 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
}
関数が再帰する様子 
● void dfs(int x){ 
if(x>=4)return; 
dfs(x+1); 
dfs(x+2); 
return; 
}
関数が再帰する様子 
dfs(2) 
dfs(3) 
dfs(3) 
dfs(4) 
dfs(5) 
dfs(4) 
dfs(1) 
dfs(4) 
dfs(5)
DFS 
● 関数の再帰でDFSが実現されている様子が 
わかりましたか? 
● AさんとBさんで数字を言い合うゲームをする 
– 最初の人が1を言う 
– 交互に前の人が言った数字+1または+2を言う 
– 4以上を言ったほうが負け
DFS 
dfs(2) 
dfs(3) 
dfs(3) 
dfs(4) 
dfs(5) 
dfs(4) 
dfs(1) 
dfs(4) 
dfs(5) 
Bさんが3を 
言えば必勝 
A B A B
DFS 
● 全探索するといろんなことがわかります 
● 自分がある手を選択したときの勝率など
DFS 
● dfs(今の状態){ 
for each(今の状態から行ける状態):dfs(次の状態) 
return 
} 
● 状態を引数に与えてやる 
● 戻り値は調べたい事柄によって様々
実際にDFSしてみよう! 
● 問題 
品物がN個あり、値段はそれぞれC[i]円です。 
NPCA君はなるべくM円に近い買い物をしたいです。 
M円との差額は何円に抑えられるでしょう。 
制約 1≦ N ≦20 
1≦ C[i] ≦10^3 
1≦ M ≦ 10^5
状態の表し方 
● まずは状態の表現の仕方を考えてみる 
– 表し方が複雑だと計算量が増えることもある 
● なるべくシンプルにかつすべての場合を尽くせるように 
– 引数の個数が多くならないように 
– 区別の必要な複数の状態が同じように表されてはだめ
区別が必要 
● 品物を1~Nと番号付ける 
● 品物1,2を買うのと、品物2,1を買うのは区別が必要か? 
– 今回注目しているのは合計金額なので 
– 買う順番には興味がない 
→ 1番の品物から順に買うかどうか決めていく事にする
必要な情報 
● では買った品物のリストをもっておけばいい? 
→ 情報の持ちすぎ、合計金額に興味があるので 
  それだけをもっていればいい 
● 必要な状態は 
(今何番目の品物まで見たか,今までに買った金額)
DFS 
int ans=INF; 
void dfs(int x,int sum){ 
if(x==N+1){ 
if(ans>abs(M-sum))ans=abs(M-sum); 
return; 
} 
dfs(x+1,sum+C[x+1]); 
dfs(x+1,sum); 
return; 
} 
int main(){ 
dfs(1,0); 
return 0; 
}
DFS 
int ans=INF; // INFはとても大きな値(具体的には10^9くらい) 
void dfs(int x,int sum){ // x 何番目か sum 今まで買った合計金額 
if(x==N+1){   //最後のN番目の品物まで見終わった 
if(ans>abs(M-sum))ans=abs(M-sum); //absは絶対値 
return; 
}  この部分を終了条件という 
dfs(x+1,sum+C[x]); //品物 x を買う場合 
dfs(x+1,sum);    //買わない場合 
return; 
}
DFS 
int main(){ 
dfs(1,0); 
return 0; 
} 
● Main関数の方から呼び出すときは、 
1番目を見る、まだ何も買っていないので合計金額は0 
だからdfs(1,0);
DFS 
● 終了条件を書かないと再帰がいつまでも続いて、 
配列外参照、スタックオーバーフローなどを起こす 
● 今回はN個目まですべて買うか買わないか決めたあと、 
x=N+1となったところで終了させた
実際に解いてみよう 
● PKU 3628 Bookshelf 2 
● 問題概要 
● N匹の牛と高さBの本棚がある 
● 各牛の高さはH[i] 
● 牛たちは積み上がって本棚の高さ以上に届きたい 
● あまり高すぎるとあぶないのでなるべく低いほうがいい 
● そのような時の本棚の高さとの差はいくらか
解けましたか? 
● さっきの問題とかなり似てますね 
● この問題でも牛の順序はどうでもいいので 
● (何番目の牛まで見たか,今まで積んだ合計の高さ) 
● があればいいです 
● 終了条件は(何番目の牛まで見たか=N+1)ですね
実際に解いてみよう 
● NPCA Judge #99 講義用問題2
解けましたか? 
● まず状態を表すのに必要な要素を考えてみよう 
● 欲しい情報 
– 順番は決まっているので左から見ていくことにする 
– 今の所連続して表を向いているところの和 
(前に切れてしまったところはどうでもいい) 
– 裏がえす枚数に制限がある 
● (今何番目まで見たか,今連続して表になってる所の和,裏返した枚数)
● 終了条件以外にも、こういう状態になったらもうダメ 
というのがあればそこで探索を打ち切ってしまうと 
高速化につながります 
– 枝刈りといいます 
● 再帰関数の挙動はわかりにくいかもしれませんが 
問題を解いていくと慣れると思います
BFS 
1
BFS 
1 
2
BFS 
1 
2 3
BFS 
1 
2 3 4
BFS 
1 
5 
2 3 4
BFS 
1 
5 
2 4 
3 
6
BFS 
1 
5 
3 
6 
4 
7 
2
BFS 
1 
3 
6 
4 
5 7 
8 
2
BFS 
1 
5 8 
9 
3 
6 
4 
7 
2
BFS 
1 
5 8 
9 
3 
6 
4 
7 
10 
2
BFS 
1 
2 
5 8 
9 
3 
6 
4 
7 
10 11
BFS 
1 
2 
5 8 
9 
3 
6 
4 
12 
7 
10 11
BFS 
1 
2 
5 8 
9 
3 
6 
4 
7 
12 13 
10 11
BFS 
1 
2 
5 8 
14 
9 
3 
6 
4 
7 
12 13 
10 11
BFS 
1 
2 
5 8 
14 
9 
3 
6 
4 
7 
12 13 15 
10 11
BFS 
● 近い所から順に探索 
● どうやって実装するの? 
– キューを用いる
キューってなんやねん 
● データ構造の一つ 
● pushとpopという操作がある 
– push ‥‥ キューの一番上に積む 
– pop ‥‥ キューの一番下から取り出す 
● できることはこれだけ
キュー(queue) 
push 5 push 11 pop push 2 push6 pop pop 
11 
5 5 
11 
5 
2 
11 
6 
2 
11 
6 
2 
11 
6 
2
キュー 
● 今回は再帰みたいな代わりがないからキューを実装する 
必要がある 
● どないすんねん
キュー 
● ほとんどの言語にはstack,queueなどのライブラリが存在 
する(はず) 
● C++ならstackヘッダ、queueヘッダに入っている
キュー 
● キューの使い方(C++) 
– queue<T> hoge;   Tは型 
– hoge.push(x); hogeにxをpush 
– hoge.pop();     hogeからpop 
– hoge.front();     hogeから次にpopされる値
BFS 
1 
1 
  キュー 
最初に1をpushしておく
BFS 
1 
  キュー 
1をpop
BFS 
1 
2 
2 
  キュー 
2をpush
BFS 
1 
2 3 
2 
  キュー 
3をpush 
3
BFS 
1 
2 3 4 
4 
2 
  キュー 
4をpush 
3
BFS 
1 
2 3 4 
4 
2 
  キュー 
ここまでが1をpopした 
あとの処理 
3
BFS 
1 
2 3 4 
3 
  キュー 
2をpop 
4
BFS 
1 
5 
2 3 4 
5 
3 
  キュー 
5をpush 
4
BFS 
1 
5 
2 3 4 
5 
3 
  キュー 
ここまでが2をpopした 
後の処理 
4
BFS 
1 
5 
2 3 4 
4 
  キュー 
3をpop 
5
BFS 
1 
5 
2 3 
4 
6 
6 
4 
  キュー 
6をpush 
5
BFS 
● こんな感じでキューが空になるまでやる 
– キューが空になったときすべて調べつくされている 
● 実装どないすんねん
BFS 
● void bfs(){ 
queue<状態の型> q; 
q.push(はじめの状態); 
while(!q.empty()){ 
(状態) cur = q.front(); 
q.pop(); 
for each(今の状態から行ける状態){ 
if(今まで訪れてない)q.push(次の状態); 
    } 
} 
return; 
}
DFS,BFSにおける注意 
● どちらでも同じところをなんども探索するのは無駄 
– 配列にその状態を訪れたかどうか記憶しておく 
– 前のソースコードでは状態をマークするのは 
省略しているので注意 
● 配列に値を記憶しながらDFS → メモ化再帰
全探索まとめ 
● 大きく分けてDFS,BFSの2種類がある 
● それぞれstack,queueを用いて実装できる 
● 状態の表し方はなるべくシンプルに 
– 興味のない情報は要らない 
● あとは慣れ
  DP
DPってなんやねん 
● 動的計画法(Dynamic Programming)の略 
● だから動的計画法ってなんやねん
DPってなんやねん 
● 現時点では全探索の無駄を省いたものというような 
認識でOK 
● 百聞は一見に如かずじゃ
● 問題 
品物がN個あります。 
各品物には重さW[i]kgと価値V[i]円があります。 
NPCAくんは重いものを運ぶのが苦手なのでM[kg]までし 
か持ちたくないです。 
なるべく価値の総和が高くなるように品物を選ぶ時価値 
の総和はいくらになるでしょう。 
● 制約 1≦N≦100 1≦W,V≦100 1≦M≦10000
● ナップサック問題と呼ばれる超有名問題 
● DPの紹介のときに必ずといってもいいほど登場する 
● JOIでは食事のときに手で解かないといけない
● とりあえずさっきやったDFSで全探索してみよう 
● 状態の表し方を考えてみる 
● この場合も品物を選ぶ順番は関係ない 
● 制約があるのは重さなので今までに選んだ品物の重さの 
総和が必要 
● 価値の最大値を戻り値で返すことにする 
● (何番目の品物まで見たか,選んだ品物の重さの総和)
● int dfs(int x,int sum){ 
if(x==N+1)return 0; 
if(sum+W[x]>M)return dfs(x+1,sum); 
return max(dfs(x+1,sum),dfs(x+1,sum+W[x])+V[x]); 
} 
int main(){ 
printf(“%dn”,dfs(1,0)); 
return 0; 
}
● 解けた、やったね 
● N≦20程度なら通る 
– 2^Nの状態を調べている 
● しかしN≦100 
● どこに無駄があるんだろう?
● (例) 
● N=5,M=10 
● (W,V)=(1,1),(2,4),(3,2),(3,5),(4,7)
● (例) 
● N=5,M=10 
● (W,V)=(1,1),(2,4),(3,2),(3,5),(4,7) 
● 品物1,2をとって今4番目の品物を見ている時dfs(4,3)
● (例) 
● N=5,M=10 
● (W,V)=(1,1),(2,4),(3,2),(3,5),(4,7) 
● 品物1,2をとって今4番目の品物を見ている時 dfs(4,3) 
● 品物3をとって今4番目の品物を見ている時 dfs(4,3) 
● 同じものが複数回呼ばれている。
● 関数は引数が同じであれば帰ってくる値はもちろん同じ 
● 1回呼んだら配列に記憶しておく 
– 2回目以降再帰せずにO(1)で値が帰ってくる 
● 引数のパターンはxが1~N+1,sumが0~1000 
– (Nの上限)*1000=10^5程度
改良版DFS 
● int memo[100][1010]; 
int dfs(int x,int sum){ 
if(memo[x][sum]!=-1)return memo[x][sum]; 
if(x==N+1)return 0; 
if(sum+W[x]>M)return memo[x][sum]=dfs(x+1,sum); 
return memo[x][sum]=max(dfs(x+1,sum),dfs(x+1,sum+W[x])+V[x]); 
} 
int main(){ 
memset(memo,-1,sizeof(memo));//memoの全要素を-1で初期化 
printf(“%dn”,dfs(1,0)); 
return 0; 
}
● このように配列に値を記憶しておいて無駄な再帰を防ぐ 
のをメモ化再帰という 
● DPは再帰を行わずにこれを解く
● 突然だが次のような配列を考える 
● dp[i][j]:=i番目までの品物から重さj以下になるように品物 
を選んだ時の価値の総和の最大値 
● 今求めたいのはdp[N][M] 
● DPではこの配列を埋めていくことで答えを求める 
● ではどのように埋めていくのか?
● とりあえず確定する場所がある 
– dp[0][0]=0 
– 0番目の品物というのは存在しないがとりあえず何も 
品物を選んでない状態。当然価値,重さの合計は0。 
● ここから、配列dpの要素同士の関係を利用して配列を 
埋めていく 
● 逆に、関係性がなければDPはできない。
● では今回の場合どのような関係があるのか? 
● dp[i][j]:=i番目までの品物から重さj以下になるように品物 
を選んだ時の価値の総和の最大値 
● i番目の品物を選んだ場合、選ばなかった場合のどちらか 
– 排中律
● i番目の品物を選んだ場合が最大の時 
dp[ i ][ j ] = dp[ i-1 ][ j - W[i] ]+V[i] 
– i-1番目までの品物からj-W[i]以下の重さの品物を選んだ 
時の価値の総和の最大値にi番目の品物の価値が加わる 
● i番目の品物を選ばない場合が最大の時 
dp[ i ][ j ] = dp[ i-1 ][ j ] 
– i-1番目までの品物からj以下の重さの品物を選んだ時の 
価値の総和の最大値と同じ
● dp[i][j]はこれらのどちらか大きい方 
● dp[i][j]=max(dp[i-1][j],dp[i-1][j-W[i]]+V[i]) 
● これが関係式 
● この式をみてわかるようにi,jが小さいものから順に 
確定していく 
● dp[0][0]は0だとわかっている
● int dp[105][1010]; 
int main(){ 
for(int i=1;i<=N;i++){ 
for(int j=0;j<=M;j++){ 
if(j-W[i]<0)dp[i][j]=dp[i-1][j]; 
else dp[i][j]=max(dp[i-1][j],dp[i-1][j-W[i]]+V[i]); 
} 
} 
return 0; 
}
● int dp[105][1010]; 
int main(){ 
for(int i=1;i<=N;i++){ 
for(int j=0;j<=M;j++){ 
if(j-W[i]<0)dp[i][j]=dp[i-1][j]; 
else dp[i][j]=max(dp[i-1][j],dp[i-1][j-W[i]]+V[i]); 
} 
} 
return 0; 
}
● このように添字が負にならないように注意 
● この場合は j < W[i]となる場合を考えなくてよい 
–重さW[i]の品物を買って重さの合計がjとなることはない 
● もし添字が負になるような状況も考えなければならない 
なら適宜げたをはかせて添字を非負にしてやる 
– std::mapとかでやってもいいかも
メモリ節約 
● さっきのソースコードを見てわかるとおり 
dp[i][ ]はdp[i-1][ ]の影響しか受けない 
● 2つ以上前は覚えておく必要がないので捨てていく
● int dp[2][1010]; 
int main(){ 
for(int i=1;i<=N;i++){ 
for(int j=0;j<=M;j++){ 
if(j-W[i]<0)dp[i%2][j]=dp[(i-1)%2][j]; 
else dp[i%2][j]=max(dp[(i-1)%2][j],dp[(i-1)%2][j-W[i]]+V[i]); 
} 
} 
return 0; 
} 
● 今回は必要なかったが必ず必要なときもある
● 自分で解く時どうするの? 
● 今日は突然 
● dp[i][j]:=i番目までの品物から重さj以下になるように品物 
を選んだ時の価値の総和の最大値 
● が与えられたからできた 
● 自分で思いつくしかない 
● どないしたらええねん
● メモ化再帰の時と同様に、状態を表すのに必要な最小限 
の要素を考える
● メモ化再帰の時と同様に、状態を表すのに必要な最小限 
の要素を考える 
● 最初から無理しなくてよい。徐々に要らない情報を省い 
ていくと解けることは多い 
● 関係式の作り方によっては計算量が大きく変わる
● あとは慣れなので問題を解くのが大事 
– 何事も精進だね、うん 
● よく出てくる形がいくつかある
よくあるかたち 
● i番目までの〜から〜 
– 今回のナップサック問題も。非常に多い 
● 長さ/大きさ i の〜をつくる時の〜(残せる)〜の最小/最大
● それっぽいdpテーブルを思いついても 
関係式(漸化式)がわからないととけない 
● これもやっぱり経験 
– 何事も精進だね、うん 
● こちらもよく出てくる形がある
よくあるかたち 
● dp[i]とdp[i-1]の関係に注目するもの 
– かなり多い 
● min(dp[j],dp[j- ◯]+▲,dp[j-◯*2]+▲*2‥‥dp[j-◯*k]+▲*k) 
– 比較的よくある 
– この形の場合計算量を落とすテクがある 
– 今日は話しませんが興味があったら蟻本を読むか 
私に聞いてください
実際に解いてみよう 
● PKU 2385 Apple Catching 
● 2本の林檎の木がある 
● 初めBessieは木1にいる 
● 一定の間隔でT回どちらか 
から林檎が落ちてくる 
● W回以下しか移動したくない 
● 最大でいくつの林檎を取れるか 
● 制約 1≦T≦1000 1≦ W ≦ 30
解けましたか? 
● dp[i][j]:=i回目の林檎の落下までで移動回数j回以下で 
取れる最大の個数 
● どちらの木にいるのかという情報がない? 
– 移動回数からすぐにわかる 
– 移動回数が奇数なら木2,偶数なら木1
● 漸化式を考えてみよう 
● i回目の落下が今居る木の時 dp[i][j]=dp[i-1][j]+1 
● i回目の落下が今居る木でない時 
– 取りに行く時 dp[i-1][j-1]+1 
– 取りに行かない時 dp[i-1][j] 
– dp[i][j]=max(dp[i-1][j-1]+1,dp[i-1][j]) 
● 初期条件 dp[0][0]=0
● int dp[1010][35]; 
int main(){ 
for(int i=1;i<=T;i++){ 
for(int j=0;j<=W;j++){ 
if((j%2==0&&fall[i]==1)||(j%2==1&&fall[i]==2))dp[i][j]=dp[i-1][j]+1; 
else dp[i][j]=max(dp[i-1][j],dp[i-1][j-1]+1); 
} 
} 
return 0; 
}
メモ化再帰でも解けます 
● int rec(int x,int mov){ 
if(memo[x][mov]!=-1)return memo[x][mov]; 
if((mov%2==1&&fall[i]==1)||(mov%2==0&&fall[i]==2)){ 
return memo[x][mov]=rec(x-1,mov)+1; 
} 
return memo[x][mov]=max(rec(x-1,mov-1)+1,rec(x-1,mov)); 
}
DP 
● DPの雰囲気はつかんでいただけたでしょうか 
● DPは本当に解いた量がものをいうと思います 
– つらい 
● がんばろう
DPの勉強方法 
● 何もなしでいきなり解くのは難しいかもしれない 
● 蟻本を読もう 
– 持ってなくても部室に2冊あります 
● 蟻本で感覚をつかんだらAOJやPKUの問題を解こう
DPの大切さ 
● 競技プログラミングではなくても探索やDPなどが必要に 
なることは多々あると思います 
● JOIの本選,春合宿へ行けるかどうかに関わります 
– (去年本選で私と部長はDPをこじらせて死んだ) 
● 上のようにdpテーブル,漸化式があってても意外とバグる 
– 初期化を適切にするのは意外と難しい 
● 結局経験なんです 
– 何事も精進だね、うん
DPの大切さ 
● JOI予選突破を目指すならばDPが重要 
– 予選4番はほぼDP,ボーダーは300〜400点 
● 頑張ろうな 
● この後のプロコンはJOI予選対策という位置づけなので 
DPを一問以上入れてます。 
● 今練習して解けるようになっちゃいましょう!
Let's 実装
~問題演習~ 
● AOJ 0573 Night Market 
● PKU 2229 Sumsets 
● PKU 2465 Adventures in Moving - Part IV 
● PKU 3046 Ant Counting 
● PKU 3616 Milking Time 
● PKU 3280 Cheapest Palindrome 
● AOJ 0550 Dividing Snacks 
● 0573だけ解説するので暇な人は後のもやっといて 
● 5分ごとくらいにヒント開示します
● ヒント1 
夜店を訪れる順番は訪れる店を決めると一意に定まる 
→ i番目までの~から
● ヒント2 
状態の表し方 
(今何番目の店まで見たか,今の時刻) 
→ dp[i][j]:=i番目の店までで時刻jまでに得られる楽しさのMax
● ヒント3 
漸化式 
とりあえず任意のi,jでdp[i][j]はdp[i][j-1]以上 
店iで遊ぶ時,花火の時間とかぶっていれば遊べない 
j-B[i] ~ jまで遊ぶのでj-B[i]<S&&S<jならば 
i-1番目までの店で時刻jまでのMaxと同じ 
dp[i][j]=max(dp[i][j-1],dp[i-1][j])
● ヒント4 
それ以外の時は遊ぶか遊ばないか選べる 
遊ぶ時 i-1番目の店まででj-B[i]まで遊んで, 
j-B[i]~jの間店iで遊ぶ → dp[i-1][j-B[i]]+A[i] 
遊ばない時 i-1番目の店まででjまで遊ぶ 
→ dp[i-1][j] 
これらの大きい方 
dp[i][j]=max(dp[i][j-1],dp[i-1][j],dp[i-1][j-B[i]]+A[i])
● 答え 
for(int i=1;i<=N;i++){ 
for(int j=0;j<=M;j++){ 
if(j-B[i]<0)dp[i][j]=max(dp[i-1][j],dp[i][j-1]); 
if(j-B[i]<S&&S<j)dp[i][j]=max(dp[i-1][j],dp[i][j-1]); 
else dp[i][j]=max(dp[i][j-1],dp[i-1][j],dp[i-1][j-B[i]]+A[i]); 
} 
} 添字が負になる時に注意
お疲れ様でした

NPCA summer 2014