NPCA summer 2014
- 3. 今日するお話
● 全探索
– DFS(深さ優先探索)
– BFS(幅優先探索)
● 動的計画法(DP)
– メモ化再帰
– DP
- 6. 全探索
● (例)
1,2,3から一つずつ数字をとっていく時とる順番
は何通りありますか?
– 3! = 6通り
- 8. 状態の変化
1
2
2
3
3
1
3
1
2
はじめ
3
2
3
1
2
1
それぞれが1通り
のとり方に対応
- 9. 状態の変化
1
2
2
3
3
1
3
1
2
はじめ
3
2
3
1
2
1
それぞれが1通り
のとり方に対応
- 10. 状態の変化
1
2
2
3
3
1
3
1
2
はじめ
3
2
3
1
2
1
それぞれが1通り
のとり方に対応
- 11. 状態の変化
1
2
2
3
3
1
3
1
2
はじめ
3
2
3
1
2
1
それぞれが1通り
のとり方に対応
- 12. 状態の変化
1
2
2
3
3
1
3
1
2
はじめ
3
2
3
1
2
1
それぞれが1通り
のとり方に対応
- 13. 状態の変化
1
2
2
3
3
1
3
1
2
はじめ
3
2
3
1
2
1
それぞれが1通り
のとり方に対応
- 40. DFS
1
2
3 13
4
7
8
12
10
9 11
5 6
- 41. DFS
1
2
3 13
14
4
7
8
12
10
9 11
5 6
- 42. DFS
1
2
3 13
14
4
7
8
12
10
9 11
5 6
- 43. DFS
1
2
3 13
14
4
7
8
12
10
9 11 15
5 6
- 44. DFS
1
2
3 13
14
4
7
8
12
10
9 11 15
5 6
- 45. DFS
1
2
3 13
14
4
7
8
12
10
9 11 15
5 6
- 46. DFS
1
2
3 13
14
4
7
8
12
10
9 11 15
5 6
- 47. DFS
1
2
3 13
14
4
7
8
12
10
9 11 15
5 6
- 54. DFS
1
3
4
2
4
3
1
スタック
4をpush
2
- 55. DFS
1
3
4
2
4
3
1
スタック
もうこれ以上進めない
2
- 56. DFS
1
3
4
2
3
1
スタック
4をpop
2
スタックの一番上が
今見ている状態
- 57. DFS
1
3
4 5
2
3
1
スタック
5をpush
2
スタックの一番上が
今見ている状態
5
- 59. 関数が再帰する様子
● void dfs(int x){
if(x>=4)return;
dfs(x+1);
dfs(x+2);
return;
}
int main(){
dfs(1);
return 0;
}
- 72. 関数が再帰する様子
dfs(1)
● void dfs(int x){
if(x>=4)return;
dfs(x+1);
dfs(x+2);
return;
}
dfs(3)
- 73. 関数が再帰する様子
dfs(1)
● void dfs(int x){
if(x>=4)return;
dfs(x+1);
dfs(x+2);
return;
}
dfs(3)
dfs(4)
- 74. 関数が再帰する様子
dfs(1)
● void dfs(int x){
if(x>=4)return;
dfs(x+1);
dfs(x+2);
return;
}
dfs(3)
- 75. 関数が再帰する様子
dfs(1)
● void dfs(int x){
if(x>=4)return;
dfs(x+1);
dfs(x+2);
return;
}
dfs(3)
dfs(5)
- 76. 関数が再帰する様子
dfs(1)
● void dfs(int x){
if(x>=4)return;
dfs(x+1);
dfs(x+2);
return;
}
dfs(3)
- 81. 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
- 83. DFS
● dfs(今の状態){
for each(今の状態から行ける状態):dfs(次の状態)
return
}
● 状態を引数に与えてやる
● 戻り値は調べたい事柄によって様々
- 84. 実際にDFSしてみよう!
● 問題
品物がN個あり、値段はそれぞれC[i]円です。
NPCA君はなるべくM円に近い買い物をしたいです。
M円との差額は何円に抑えられるでしょう。
制約 1≦ N ≦20
1≦ C[i] ≦10^3
1≦ M ≦ 10^5
- 86. 区別が必要
● 品物を1~Nと番号付ける
● 品物1,2を買うのと、品物2,1を買うのは区別が必要か?
– 今回注目しているのは合計金額なので
– 買う順番には興味がない
→ 1番の品物から順に買うかどうか決めていく事にする
- 88. 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;
}
- 89. 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;
}
- 90. DFS
int main(){
dfs(1,0);
return 0;
}
● Main関数の方から呼び出すときは、
1番目を見る、まだ何も買っていないので合計金額は0
だからdfs(1,0);
- 92. 実際に解いてみよう
● PKU 3628 Bookshelf 2
● 問題概要
● N匹の牛と高さBの本棚がある
● 各牛の高さはH[i]
● 牛たちは積み上がって本棚の高さ以上に届きたい
● あまり高すぎるとあぶないのでなるべく低いほうがいい
● そのような時の本棚の高さとの差はいくらか
- 95. 解けましたか?
● まず状態を表すのに必要な要素を考えてみよう
● 欲しい情報
– 順番は決まっているので左から見ていくことにする
– 今の所連続して表を向いているところの和
(前に切れてしまったところはどうでもいい)
– 裏がえす枚数に制限がある
● (今何番目まで見たか,今連続して表になってる所の和,裏返した枚数)
- 108. BFS
1
2
5 8
9
3
6
4
12
7
10 11
- 109. BFS
1
2
5 8
9
3
6
4
7
12 13
10 11
- 110. BFS
1
2
5 8
14
9
3
6
4
7
12 13
10 11
- 111. BFS
1
2
5 8
14
9
3
6
4
7
12 13 15
10 11
- 114. キュー(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
- 117. キュー
● キューの使い方(C++)
– queue<T> hoge; Tは型
– hoge.push(x); hogeにxをpush
– hoge.pop(); hogeからpop
– hoge.front(); hogeから次にpopされる値
- 123. BFS
1
2 3 4
4
2
キュー
ここまでが1をpopした
あとの処理
3
- 125. BFS
1
5
2 3 4
5
3
キュー
5をpush
4
- 126. BFS
1
5
2 3 4
5
3
キュー
ここまでが2をpopした
後の処理
4
- 128. BFS
1
5
2 3
4
6
6
4
キュー
6をpush
5
- 130. BFS
● void bfs(){
queue<状態の型> q;
q.push(はじめの状態);
while(!q.empty()){
(状態) cur = q.front();
q.pop();
for each(今の状態から行ける状態){
if(今まで訪れてない)q.push(次の状態);
}
}
return;
}
- 136. ● 問題
品物がN個あります。
各品物には重さW[i]kgと価値V[i]円があります。
NPCAくんは重いものを運ぶのが苦手なのでM[kg]までし
か持ちたくないです。
なるべく価値の総和が高くなるように品物を選ぶ時価値
の総和はいくらになるでしょう。
● 制約 1≦N≦100 1≦W,V≦100 1≦M≦10000
- 138. ● とりあえずさっきやったDFSで全探索してみよう
● 状態の表し方を考えてみる
● この場合も品物を選ぶ順番は関係ない
● 制約があるのは重さなので今までに選んだ品物の重さの
総和が必要
● 価値の最大値を戻り値で返すことにする
● (何番目の品物まで見たか,選んだ品物の重さの総和)
- 139. ● 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;
}
- 140. ● 解けた、やったね
● N≦20程度なら通る
– 2^Nの状態を調べている
● しかしN≦100
● どこに無駄があるんだろう?
- 141. ● (例)
● N=5,M=10
● (W,V)=(1,1),(2,4),(3,2),(3,5),(4,7)
- 142. ● (例)
● N=5,M=10
● (W,V)=(1,1),(2,4),(3,2),(3,5),(4,7)
● 品物1,2をとって今4番目の品物を見ている時dfs(4,3)
- 143. ● (例)
● 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)
● 同じものが複数回呼ばれている。
- 145. 改良版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;
}
- 147. ● 突然だが次のような配列を考える
● dp[i][j]:=i番目までの品物から重さj以下になるように品物
を選んだ時の価値の総和の最大値
● 今求めたいのはdp[N][M]
● DPではこの配列を埋めていくことで答えを求める
● ではどのように埋めていくのか?
- 148. ● とりあえず確定する場所がある
– dp[0][0]=0
– 0番目の品物というのは存在しないがとりあえず何も
品物を選んでない状態。当然価値,重さの合計は0。
● ここから、配列dpの要素同士の関係を利用して配列を
埋めていく
● 逆に、関係性がなければDPはできない。
- 150. ● 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以下の重さの品物を選んだ時の
価値の総和の最大値と同じ
- 151. ● 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だとわかっている
- 152. ● 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;
}
- 153. ● 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;
}
- 154. ● このように添字が負にならないように注意
● この場合は j < W[i]となる場合を考えなくてよい
–重さW[i]の品物を買って重さの合計がjとなることはない
● もし添字が負になるような状況も考えなければならない
なら適宜げたをはかせて添字を非負にしてやる
– std::mapとかでやってもいいかも
- 156. ● 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;
}
● 今回は必要なかったが必ず必要なときもある
- 157. ● 自分で解く時どうするの?
● 今日は突然
● dp[i][j]:=i番目までの品物から重さj以下になるように品物
を選んだ時の価値の総和の最大値
● が与えられたからできた
● 自分で思いつくしかない
● どないしたらええねん
- 163. よくあるかたち
● dp[i]とdp[i-1]の関係に注目するもの
– かなり多い
● min(dp[j],dp[j- ◯]+▲,dp[j-◯*2]+▲*2‥‥dp[j-◯*k]+▲*k)
– 比較的よくある
– この形の場合計算量を落とすテクがある
– 今日は話しませんが興味があったら蟻本を読むか
私に聞いてください
- 164. 実際に解いてみよう
● PKU 2385 Apple Catching
● 2本の林檎の木がある
● 初めBessieは木1にいる
● 一定の間隔でT回どちらか
から林檎が落ちてくる
● W回以下しか移動したくない
● 最大でいくつの林檎を取れるか
● 制約 1≦T≦1000 1≦ W ≦ 30
- 166. ● 漸化式を考えてみよう
● 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
- 167. ● 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;
}
- 168. メモ化再帰でも解けます
● 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));
}
- 171. DPの大切さ
● 競技プログラミングではなくても探索やDPなどが必要に
なることは多々あると思います
● JOIの本選,春合宿へ行けるかどうかに関わります
– (去年本選で私と部長はDPをこじらせて死んだ)
● 上のようにdpテーブル,漸化式があってても意外とバグる
– 初期化を適切にするのは意外と難しい
● 結局経験なんです
– 何事も精進だね、うん
- 174. ~問題演習~
● 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分ごとくらいにヒント開示します
- 176. ● ヒント2
状態の表し方
(今何番目の店まで見たか,今の時刻)
→ dp[i][j]:=i番目の店までで時刻jまでに得られる楽しさのMax
- 177. ● ヒント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])
- 178. ● ヒント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])
- 179. ● 答え
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]);
}
} 添字が負になる時に注意