npca summer 2014
DESCRIPTION
NPCA summer 2014TRANSCRIPT
NPCA夏合宿講義
● 合宿初参加の身なのに講義を頼まれました
● なにが講義だえらそうに
● 許して
今日するお話
● 全探索
– DFS(深さ優先探索)
– BFS(幅優先探索)
● 動的計画法(DP)
– メモ化再帰
– DP
全探索
全探索
● 物事には状態がある
– 年齢、身長、etc...
● 状態がどう変化していくか調べたい
全探索
● (例)
1,2,3から一つずつ数字をとっていく時とる順番
は何通りありますか?
– 3! = 6通り
状態の変化
1
2
3
2
3
1
3
1
2
はじめ
3
2
3
1
2
1
状態の変化
1
2
3
2
3
1
3
1
2
はじめ
3
2
3
1
2
1
それぞれが1通りのとり方に対応
状態の変化
1
2
3
2
3
1
3
1
2
はじめ
3
2
3
1
2
1
それぞれが1通りのとり方に対応
状態の変化
1
2
3
2
3
1
3
1
2
はじめ
3
2
3
1
2
1
それぞれが1通りのとり方に対応
状態の変化
1
2
3
2
3
1
3
1
2
はじめ
3
2
3
1
2
1
それぞれが1通りのとり方に対応
状態の変化
1
2
3
2
3
1
3
1
2
はじめ
3
2
3
1
2
1
それぞれが1通りのとり方に対応
状態の変化
1
2
3
2
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
4 65
2
DFS
1
3
4 65
2
DFS
1
3
4 65
2
DFS
1
3
4 65
2
DFS
1
3
4
7
65
2
DFS
1
3
4
7
8
65
2
DFS
1
3
4
7
8
965
2
DFS
1
3
4
7
8
965
2
DFS
1
3
4
7
8
965
2
DFS
1
3
4
7
8
9
10
65
2
DFS
1
3
4
7
8
119
10
65
2
DFS
1
3
4
7
8
119
10
65
2
DFS
1
3
4
7
8
119
10
65
2
DFS
1
3
4
7
8
119
10
65
2
DFS
1
3
4
7
8
12
119
10
65
2
DFS
1
133
4
7
8
12
119
10
65
2
DFS
1
14
133
4
7
8
12
119
10
65
2
DFS
1
14
133
4
7
8
12
119
10
65
2
DFS
1
14
133
4
7
8
12
15119
10
65
2
DFS
1
14
133
4
7
8
12
15119
10
65
2
DFS
1
14
133
4
7
8
12
15119
10
65
2
DFS
1
14
133
4
7
8
12
15119
10
65
2
DFS
1
14
133
4
7
8
12
15119
10
65
2
DFS
● 行けるところまで行く
– それ以上いけなくなった戻る● どうやって実装するの?
– スタックを用いる
– 関数の再帰を用いる
スタックってなんやねん
● データ構造の一つ● pushとpopという操作がある
– push ‥‥ スタックの一番上に積む
– pop ‥‥ スタックの一番上から取り出す● できることはこれだけ
スタック(stack)
DFS
1
1
スタック
1をpush
DFS
1
2
1
スタック
2をpush
2
DFS
1
3
2
1
スタック
3をpush
2
3
DFS
1
3
4
2
1
スタック
4をpush
2
3
4
DFS
1
3
4
2
1
スタック
もうこれ以上進めない
2
3
4
DFS
1
3
4
2
1
スタック
4をpop
2
3
スタックの一番上が今見ている状態
DFS
1
3
4 5
2
1
スタック
5をpush
2
3
スタックの一番上が今見ている状態
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(3)
dfs(2)
dfs(3)
dfs(4)
dfs(5)
dfs(4)
dfs(1)
dfs(4)
dfs(5)
DFS
● 関数の再帰でDFSが実現されている様子が
わかりましたか?
● AさんとBさんで数字を言い合うゲームをする
– 最初の人が1を言う
– 交互に前の人が言った数字+1または+2を言う
– 4以上を言ったほうが負け
DFS
dfs(3)
dfs(2)
dfs(3)
dfs(4)
dfs(5)
dfs(4)
dfs(1)
dfs(4)
dfs(5)
A B A B
Bさんが3を言えば必勝
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
32
BFS
1
3 42
BFS
1
5
3 42
BFS
1
5
3
6
42
BFS
1
5
3
6
4
7
2
BFS
1
85
3
6
4
7
2
BFS
1
85
9
3
6
4
7
2
BFS
1
85
9
3
6
4
7
10
2
BFS
1
85
9
3
6
4
7
1110
2
BFS
1
85
9
3
6
4
12
7
1110
2
BFS
1
85
9
3
6
4
1312
7
1110
2
BFS
1
14
85
9
3
6
4
1312
7
1110
2
BFS
1
14
85
9
3
6
4
151312
7
1110
2
BFS
● 近い所から順に探索● どうやって実装するの?
– キューを用いる
キューってなんやねん
● データ構造の一つ● pushとpopという操作がある
– push ‥‥キューの一番上に積む
– pop ‥‥キューの一番下から取り出す● できることはこれだけ
キュー(queue)
55
11
push 5 push 11 pop push 2 push6 pop pop
11
5
1111
2
11
2
6
2
6
11
2
6
5 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
32
2
キュー
3をpush
3
BFS
1
3 42
2
キュー
4をpush
3
4
BFS
1
3 42
2
キュー
ここまでが1をpopしたあとの処理
3
4
BFS
1
3 42
3
キュー
2をpop
4
BFS
1
5
3 42
3
キュー
5をpush
4
5
BFS
1
5
3 42
3
キュー
ここまでが2をpopした後の処理
4
5
BFS
1
5
3 42
4
キュー
3をpop
5
BFS
1
5
3
6
42
4
キュー
6をpush
5
6
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(“%d\n”,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(“%d\n”,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]);
}
} 添字が負になる時に注意
お疲れ様でした