EDPC解説 M~T

EDPC解説目次へ戻る

カラカルよ。M問題~T問題を解説するわ。
このあたりから計算量を気にして高速化をしないといけない問題が増えてきて大変だけど、ついてこれるかしら?

M問題 Candies

N人の子供にちょうどK個の飴を配る方法は何通りあるか\mod 10^9+7で求めよ。
ただしi番目の子供にはa[i]個以下しか渡すことができない。
制約:N\leq 10^2K\leq 10^5

解説

dp[i][j]=(i番目までの子供たちにj個の飴を配る場合の数)
ってDPをやるだけね。

dp[0][0]=1;
rep(i,1,N+1)rep(j,0,K+1){
	rep(k,0,a[i]+1)if(j-k>=0)dp[i][j]=(dp[i][j]+dp[i-1][j-k])%MOD;
}

……とはいかないのよねぇ。
これだと計算量がO(NK^2)になって全然間に合わないわ。
そこで状態遷移のところをよくみてみるわ。数式で書くと、これってこういうことよね。
\displaystyle{dp[i][j]=\sum_{k=\max(0,j-a[i])}^j dp[i-1][k]}
これはdp[i-1][*]区間の和だから、累積和を使えば高速化することができるわね。

dp[0][0]=1;
rep(i,1,N+1){
	cum[0]=0;
	rep(j,1,K+1+1)cum[j]=(cum[j-1]+dp[i-1][j-1])%MOD;
	//cum[j]=dp[i-1][0]+...+dp[i-1][j-1]
	rep(j,0,K+1)dp[i][j]=(cum[j+1]-cum[max(0,j-a[i])]+MOD)%MOD;
}
ans=dp[N][K];

計算量はO(NK)よ。

N問題 Slimes

N匹のスライムが1列に並んでいる。i番目のスライムの大きさはa[i]である。
「隣り合う2匹のスライムを合体させる」という操作を繰り返して1匹のスライムにまとめることを考える。
合体後のスライムの大きさの和と等しいコストがかかる。最小コストを求めよ。
制約:N\leq 400

解説

この問題は逆から考えると簡単ね。つまり問題全体をこう読み替えるわけ。

1匹のスライムがいる。大きさは\sum a[i]である。
「スライムを2匹に分解する」という操作を繰り返して、大きさが端から順にa[i]であるようなN匹のスライムを作ることを考える。
分解には分解前のスライムの大きさと等しいコストがかかる。最小コストを求めよ。

切るべき箇所は、最終的なスライムの切れ目にあたるN-1箇所のどれかね。
どれが最小になるか分かればいいから
dp[l][r]=(区間[l,r]に相当するスライムが1匹にまとまっているとき、それを分解するために必要な最小コスト)
が分かればいいわね。実装はメモ化再帰が簡単だと思うわ。

int f(int l,int r){
	if(flag[l][r])return dp[l][r];
	flag[l][r]=1;
	if(l==r)return 0;
	//どこで切るか全通り試す
	fans=INF;
	rep(m,l,r)fans=min(fans,f(l,m)+f(m+1,r));
	return dp[l][r]=fans+(a[l]~a[r]の和);//予め累積和を計算しておく
}
//ans=f(1,N);

状態数がO(N^2)、遷移がO(N)だから計算量はO(N^3)ね。

O問題 Matching

男女がN人ずついる。男女からなるペアをN組つくりたい。
男性iと女性jの相性a[i][j]が0か1で与えられ、相性が0であるような2人はペアになることができない。
ペアの作り方は何通りあるか?\mod 10^9+7で求めよ。
制約:N\leq 21

解説

bitDPというテクニックを使う問題ね。
bitDPというのは、集合を数に変換して状態(=添字)として持つようなDPのことよ。
例えば\{0,2,3\}という集合は2^0+2^2+2^3=13という数に変換して、「数iが集合に属しているか」と「i bit目が1か」を対応させるわ。
こうすると、「集合としてS\subset T」⇒「数としてS\leq T」になるから

rep(S,0,1<<N){
	/*処理*/
}

というふうに、DPの遷移が単純なループで書けることが多いのよね。
この問題もこのことを知っていれば簡単よ。
dp[i][S]=(男性はi人目までで、既にペアになった女性の集合がSであるような場合の数)
としてDPするわ。
今まではずっと1-indexedでやってきたけど、bitDPは0-indexedの方が都合がいいから、入力は0-indexedで与えられているものとするわ。

#define bit(n,k) ((n>>k)&1) /*nのk bit目*/
dp[0][0]=1;
rep(i,1,N+1)rep(S,0,1<<N){
	rep(j,0,N)if(bit(S,j)==1&&a[i-1][j]==1){
		dp[i][S]=(dp[i][S]+dp[i-1][S^(1<<j)])%MOD;
	}
}
ans=dp[N][(1<<N)-1];

……とすると、これはO(2^N N^2)でTLEなのよねえ。
少し考えると「既にペアになった女性の集合がS」なら「bitpopcount(S)人目までをチェックした」ってことになるから、iを状態として持つ必要は無いことがわかるわ。

#define bit(n,k) ((n>>k)&1) /*nのk bit目*/
dp[0]=1;
rep(S,1,1<<N){
	i=__builtin_popcount(S);
	rep(j,0,N)if(bit(S,j)==1&&a[i-1][j]==1)dp[S]=(dp[S]+dp[S^(1<<j)])%MOD;
}
ans=dp[(1<<N)-1];

計算量はO(2^N N)よ。

詳しくは説明しないけど、この問題を配るDPで解くときは、次のコードで実はO(2^N N)になるわ。枝刈りが本質的に計算量を改善する例ね。

#define bit(n,k) ((n>>k)&1) /*nのk bit目*/
dp[0][0]=1;
//一見するとO(2^N N^2)だけど……
rep(i,0,N)rep(S,0,1<<N)if(dp[i][S]){//<-ここのif文の枝刈りが効いてO(2^N N)に落ちる
	rep(j,0,N)if(bit(S,j)==0&&a[i][j]==1){
		dp[i+1][S^(1<<j)]=(dp[i][S^(1<<j)]+dp[i][S])%MOD;
	}
}
ans=dp[N][(1<<N)-1];

P問題 Independent Set

N頂点の木が与えられる。頂点を白か黒で塗る。
隣接する頂点どうしをともに黒で塗ることはできない。
塗り方は何通りあるか?\mod 10^9+7で求めよ。
制約:N\leq 10^5

解説

木DPというやつね。部分木に関する情報を集めて元の木に関する問題に答えるようなDPよ。
まず適当な頂点を1つ選んで根とするわ。
各頂点は子が何色かによって塗れる色が決まるから、各部分木について、根が黒/白の塗り方の数がわかればいいわね。
dp[i][j]=(頂点iを(j?黒く:白く)塗ったとき、iを親とする部分木の塗り方の場合の数)
とすると、
\displaystyle{dp[i][0]=\prod_{jはiの子} (dp[j][0]+dp[j][1])}
\displaystyle{dp[i][1]=\prod_{jはiの子} dp[j][0]}
となるわ。
実装は根からたどれるメモ化再帰が簡単じゃないかしら。

void f(int i){
	if(flag[i])return;
	flag[i]=1;
	dp[i][0]=1;
	dp[i][1]=1;
	for(iの子jについて){
		f(j);
		dp[i][0]=dp[i][0]*(dp[j][0]+dp[j][1])%MOD;
		dp[i][1]=dp[i][1]*dp[j][0];
	}
}
//頂点1を根とすると
//f(1);
//ans=(dp[1][0]+dp[1][1])%MOD;

計算量はO(N)ね。

Q問題 Flowers

N本の花が一列に並んでいる。花iは高さh[i]、美しさa[i]である。
左から見て単調増加になるように花を選ぶ時、美しさの最大値を求めよ。
制約:N\leq 2*10^5h[i]1Nで相異なる

解説

i番目までの選び方を決めた時、次の決め方に影響するのは最後に選んだ花だけだから
dp[i][j]=(i番目まで使って最後の高さがjであるようなものの美しさの最大値)
というのを考えるのが自然よね。

dp[i][j]= \begin{cases}
\max\{dp[i-1][k]|0\leq k< j\}+a[i] & (h[i]==j\text{のとき})\\
dp[i-1][j] & (\text{それ以外})
\end{cases}
1回あたり更新するのは1箇所だから、iを状態にもつ必要はなくて、配列を使い回すことができるわ。
あとは、区間maxが高速に計算できればいいから、セグメント木を使えばできるわね。
セグメント木の説明は省略するけど、区間に対する操作が高速にできるようなデータ構造のことよ。

rep(i,1,N+1){
	temp=getmax(0,h[i]);//dp[0],...,dp[h[i]-1]のmax
	setvalue(h[i],temp+a[i]);//dp[h[i]]をtemp+a[i]にする
}
ans=getmax(0,N+1);

セグメント木は区間maxの計算と値の変更をどちらもO(\log N)でできるから、計算量はO(N\log N)になるわね。

もしこの問題で全てのa[i]が1なら、これは最長増加部分列と全く同じ問題ね。
つまり最長増加部分列問題もこの解き方で解けるわ。

R問題 Walk

N頂点単純有向グラフGの隣接グラフAが与えられる。
長さKのパスがいくつあるか\mod 10^9+7で求めよ。
ただし同じ辺を複数回通るものも含める。
制約:N\leq 50、K\leq 10^{18}

解法

長さKのパスを考えるのだから、とりあえず始点と終点を状態として
dp[n][i][j]=(頂点iから頂点jへ行く長さnのパスの個数)
というのを考えてみるわ。状態遷移は
dp[n][i][j]=\sum_k dp[n-1][i][k]*a[k][j]
となるわね。ところで、この積の形、よくみると行列の積と同じね!
dp[n][i][j](i,j)成分とする行列をDP_nと書くことにすると、さっきの漸化式は
DP_n=DP_{n-1} A
になるわ。
DP_0は明らかに単位行列だから、DP_n=A^nね。
求める答えは\sum_{i,j} dp[K][i][j]だから、A^Kを計算すれば求められるわよ。

matrixpow(A,K,MOD);//Aをmod MODでK乗する
rep(i,1,N+1)rep(j,1,N+1)ans=(ans+A[i][j])%MOD;

行列の積は1回あたりO(N^3)、繰り返し二乗法を使えば積を計算する回数はO(\log K)だから、O(N^3\log K)で求められるわね。

「行列の積と同じ式になってると気づくのは無理じゃない?」
そうね、こればっかりは知ってるかどうかになりそうね。
行列累乗で解く問題は、次の2つのポイントを抑えておくといいわ。どっちも決め手になるほどじゃないけど……
・ポイント1:制約をみる
N\leq 50くらい、K10^9とか10^{18}とかの大きな値、みたいな制約の問題は、サイズN\times Nの行列をK乗するかも。
・ポイント2:繰り返し
今回の状態遷移の式を見ると、回数nが遷移に影響しないわね。こういうふうに同じ状態遷移を何度も繰り返すものは、行列累乗かも。

S問題 Digit Sum

1以上K以下の数のうち、十進表記での各桁の数字の和がDの倍数であるものはいくつあるか?
\mod 10^9+7で求めよ。
制約:K\leq10^{10^4}D\leq 10^2

解説

桁DPね。
桁DPっていうのは本質的に1種類しかなくて、基本的にこの方針で実装することになるわ。
dp[i][f][*]=(上からi桁目まで決めて、Kより小さいことが確定して(f?いて:いなくて)、条件*を満たすようなもの)
場合によっては下の桁から決めることもあるけど、まあ似たようなものね。
今回は桁和がDの倍数であるものの個数を調べたいから、「既に決まっている部分の桁和の\mod D」という状態を追加で持てばいいわよ。
桁DPについてはこのサイトの説明がわかりやすいと思うわ。
桁DP入門 - ペケンペイのブログ
桁DPはもらうDPよりも配るDPの方が圧倒的に書きやすいのよね。実はいままでA問題からR問題までは全部もらうDPで説明してたんだけど、気付いてたかしら?

//K[i]でKの先頭からi文字目の数が得られるとする
dp[0][0][0]=1;
rep(i,0,N)rep(j,0,D){
	rep(dig,0,10)dp[i+1][1][(j+dig)%D]=(dp[i+1][1][(j+dig)%D]+dp[i][1][j])%MOD;
	rep(dig,0,K[i+1])dp[i+1][1][(j+dig)%D]=(dp[i+1][1][(j+dig)%D]+dp[i][0][j])%MOD;
	dp[i+1][0][(j+K[i+1])%D]=(dp[i+1][0][(j+K[i+1])%D]+dp[i][0][j])%MOD;
}
ans=(dp[n][0][0]+dp[n][1][0]-1+MOD)%MOD;//答えに0が含まれるのでそれを除くために-1

計算量は、基数をBとしてO(DB\log_B K)よ。

T問題 Permutation

不等号'<''>'のいずれかからなる長さN-1の文字列sが与えられる。
1Nの並び替えpであって、次の条件を満たすものは何個あるか?
条件:pの隣接する要素を比較し、順に不等号を書いたとき、それがsと一致する
制約:N\leq 3*10^3

解説

dp[i][j][S]=(i番目まで決めたとき、最後の数がjで、使用済みの数の集合がSであるような場合の数)
というのはすぐ思いつくけど、N\leq 3000だからこれは当然無理ね……。
よく考えると、次に決める数は、前に決める数との大小関係さえわかっていればいいから
dp[i][j]=(i番目まで決めたとき、i番目の数より大きいものがj個残っているような場合の数)
というのを考えればうまくいきそうね。

rep(j,0,N)dp[1][j]=1;
rep(i,2,N+1){
	if(s[i-1]=='<'){
		rep(j,0,N-i+1)rep(k,j+1,N-i+2)dp[i][j]=(dp[i][j]+dp[i-1][k])%MOD;
	}else{
		rep(j,0,N-i+1)rep(k,0,j+1)dp[i][j]=(dp[i][j]+dp[i-1][k])%MOD;
	}
}
ans=dp[N][0]

…………と言いたいところだけど、これもまだO(N^3)なのよね。
M問題と同じように最後の部分で累積和を使うとO(N^2)に出来るわ

rep(j,0,N)dp[1][j]=1;
rep(i,2,N+1){
	cum[0]=0;
	rep(j,1,N-i+3)cum[j]=(cum[j-1]+dp[i-1][j])%MOD;
	if(s[i-1]=='<'){
		rep(j,0,N-i+1)dp[i][j]=(cum[N-i+2]-cum[j+1]+MOD)%MOD;
	}else{
		rep(j,0,N-i+1)dp[i][j]=cum[j+1];
	}
}
ans=dp[N][0];


ふぅ、これで私の担当範囲はおしまいね。最後の6問はフェネックが解説してくれるわ。
……もしかしてフェネックが何かしてくれるなんて珍しいことなんじゃないかしら?

EDPC解説目次へ戻る

EDPC解説 F~L

EDPC解説目次へ戻る

はいはーい、サーバルだよ!
F問題~L問題の解説をしていくよ! よろしくね。

F問題 LCS

2つの文字列s,tが与えられる。
s,tの共通部分列であって、最長のものを1つ求めよ。
制約:|s|,|t|\leq 3*10^3

解説

いきなり難しい問題だね……。
「求める文字列の長さ」だけを先に求めておいて、実際の文字列はそれを手がかりに構成していくよ。
dp[i][j]=(si文字目、tj文字目までみた時の最長共通部分列の長さ)
とすると、
s[i]==t[j]のときこの文字は使ったほうがいいからdp[i-1][j-1]+1
そうじゃないときは、どっちかの最後の1文字は使わないから\max(dp[i-1][j],dp[i][j-1])
になるね。

//slenはSの長さ、tlenはTの長さとする
rep(i,1,slen+1)rep(j,1,tlen+1){
	if(s[i]==t[j])dp[i][j]=dp[i-1][j-1]+1;
	else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
//dp[slen][tlen]が求める長さ

具体的に文字列を作るのはDP配列を逆にたどればできるよ。
もしs[i]==t[j]なら、求める最長共通部分列の最後の文字はs[i]で、それより前の部分はdp[i-1][j-1]の結果からわかって、
そうじゃないならどっちかの最後の文字は使わないから、dpの値が減らない方に移動すればいいね。

len=dp[slen][tlen];
i=slen;
j=teln;
while(len>0){
	if(s[i]==t[j]){
		ans[len]=s[i];
		i--;
		j--;
		len--;
	}else if(dp[i][j]==dp[i-1][j]){
		i--;
	}else{
		j--;
	}
}

計算量はO(|s||t|)だよ。
イメージとしてはこんな感じ!

G問題 Longest Path

有向閉路を持たないN頂点M辺の有向グラフGが与えられる。
Gの有向パスの最長経路を求めよ。
制約:N\leq 10^5M\leq 10^5

解説

これをもらうDP/配るDPで書くのはちょっと難しいね。トポロジカルソートっていうのが必要になるのかな?
こういうときはメモ化再帰で書くといいよ!
f(x)=(xを始点とする最長経路)とすると

 f(x) = \begin{cases}
    0 & (x\text{が葉のとき}) \\
    \max\{f(y)+1|x\text{から}y\text{への辺があるような}y\} \ \ \ \ \ \ \ \ \ \ \ \ \ \ & (\text{そうでないとき})
  \end{cases}
として再帰で計算できるよ。答えは\max\{f(x)\}だね。
再帰の深さがO(N)くらいになるけど、スタックオーバーフローはしないみたい。

int f(int x){
	if(flag[x])return dp[x];
	flag[x]=1;
	fans=0;
	for(xからyへの辺があるようなy){
		fans=max(fans,f(y)+1);
	}
	return dp[x]=fans;
}
//ans=0;
//rep(i,1,N+1)ans=max(ans,f(i));

計算量はO(N+M)だよ。

H問題 Grid 1

2次元のマスを移動する。(i,j)からは(i+1,j)(i,j+1)に移動できる。
ただしa[i][j]が'#'であるマスには移動できない。
(1,1)から(H,W)へ移動する場合の数を\mod 10^9+7で求めよ。
制約:H,W \leq 10^3

解説

(H,W)へ行く場合の数は、(H-1,W),(H,W-1)へ行く場合の数の和だね。だから
dp[i][j]=((1,1)から(i,j)へ移動する場合の数の\mod 10^9+7)
とすれば解けそうだよ。

dp[1][1]=1;
rep(i,1,H+1)rep(j,1,W+1)if(!(i==1&&j==1)){
	if(a[i][j]=='#')dp[i][j]=0;
	else dp[i][j]=(dp[i-1][j]+dp[i][j-1])%MOD;
}
ans=dp[H][W];

計算量はO(HW)だよ。

I問題 Coins

N枚のコインがあり、コインiは確率p[i]で表、1-p[i]で裏が出る。
全てのコインを投げたとき、表が出たコインの数の方が多い確率を求めよ。
制約:N\leq 2999Nは奇数

解説

dp[i]=(i枚目のコインまで投げたとき、表が出たコインの数の方が多い確率)
だと、次に裏が出たときでもまだ表のほうが多いかどうかがわからないからうまくいかないね。
表が出た枚数がわかってれば大丈夫そうだから
dp[i][j]=(i枚目のコインまで投げたとき、表がj枚でる確率)
で出来そうだよ。
dp[i][j]= \\
(i-1\text{枚目のコインまで投げたとき、表が}j\text{枚でる確率})\times(i\text{枚目のコインが裏である確率})\\
 +(i-1\text{枚目のコインまで投げたとき、表が}j-1\text{枚でる確率})\times(i\text{枚目のコインが表である確率})
でちゃんと計算できるね。
答えは\sum_{j=(N+1)/2}^{N}dp[N][j]だよ。

dp[0][0]=1;
rep(i,1,N+1)rep(j,0,i+1){
	if(j>0)dp[i][j]=dp[i-1][j]*(1-p[i])+dp[i-1][j-1]*p[i];
	else dp[i][j]=dp[i-1][j]*(1-p[i]);
}
ans=0;
rep(j,N/2+1,N+1)ans+=dp[N][j];

計算量はO(N^2)だね。

J問題 Sushi

N個の皿があり、皿iには寿司がa[i]個乗っている。
全ての寿司を食べるまで次の操作を繰り返すとき、操作の回数の期待値を求めよ。
操作:皿をランダムに1つ選ぶ。選んだ皿に寿司が乗っていれば、そのうち1つを食べる。
制約:N\leq 3001\leq a[i]\leq 3

解説

まずは状態をどうやって持つか考えよう。
普通にやるとdp[a0][a1][a2][a3]\ldotsみたいに添字がN個欲しくなっちゃう。
でも例えば「皿1に寿司が2個、皿2に寿司が1個」という状態と「皿1に寿司が1個、皿2に寿司が2個」という状態だと、操作回数の期待値は同じになるよね。
よく考えると、「どの皿に何個あるか」っていうところまでわかる必要はなくて、「1個の皿がいくつ、2個の皿がいくつ、3個の皿がいくつ」だけで期待値は決まることがわかるよ。
そこで
dp[c1][c2][c3]=(1個の皿がc1枚、2個の皿がc2枚、3個の皿がc3枚あるとき、全て食べるまでの操作回数の期待値)
というのを考えるよ。
えーっと、これはループで書くのが難しいからメモ化再帰で書く方がいいね。


dp[c1][c2][c3]=\\
1\\
 +dp[c1-1][c2][c3]*(\text{1個の皿が選ばれる確率})\\
 +dp[c1+1][c2-1][c3]*(\text{2個の皿が選ばれる確率})\\
 +dp[c1][c2+1][c3-1]*(\text{3個の皿が選ばれる確率})\\
 +dp[c1][c2][c3]*(\text{0個の皿が選ばれる確率})

再帰なのに同じ状態に戻ってきちゃう!
けど、これはdp[c1][c2][c3]の項を移項すれば解消できるね。


dp[c1][c2][c3]=\\
1/(1-(0個の皿が選ばれる確率))\\
 +dp[c1-1][c2][c3]*(\text{1個の皿が選ばれる確率})/(1-(0個の皿が選ばれる確率))\\
 +dp[c1+1][c2-1][c3]*(\text{2個の皿が選ばれる確率})/(1-(0個の皿が選ばれる確率))\\
 +dp[c1][c2+1][c3-1]*(\text{3個の皿が選ばれる確率})/(1-(0個の皿が選ばれる確率))

double f(int c1,int c2,int c3){
	if(c1==0&&c2==0&&c3==0)return 0;
	if(flag[c1][c2][c3])return dp[c1][c2][c3];
	flag[c1][c2][c3]=1;
	fans=1/(1-(double)(N-c1-c2-c3)/N);//doubleにキャストしないと整数除算になっちゃう
	if(c1>0)fans+=f(c1-1,c2,c3)*c1/N/(1-(double)(N-c1-c2-c3)/N);
	if(c2>0)fans+=f(c1+1,c2-1,c3)*c2/N/(1-(double)(N-c1-c2-c3)/N);
	if(c3>0)fans+=f(c1,c2+1,c3-1)*c3/N/(1-(double)(N-c1-c2-c3)/N);
	return dp[c1][c2][c3]=fans;
}
//rep(i,1,N+1)c[a[i]]++;
//ans=f(c[1],c[2],c[3]);

計算量はO(N^3)だよ。

K問題 Stones

N個の元をもつ数の集合Aと、石の山があり、二人でゲームをする。
x\in Aを選び、石の山からちょうどx個の石を取り除く」という操作を交互にして、先に操作できなくなったほうが負けとなる。
最初に石がK個あるとき、どちらが勝つか答えよ。
制約:K\leq 10^5N\leq 10^2

解説

ゲームの基本的な問題だよ!
石の個数によって勝敗が決まるから、
dp[i]=(石の個数がi個のとき直後の手番の人が勝てるか?)
を考えるよ。これがYesになる状態を「勝ち状態」、Noになる状態を「負け状態」と呼ぶことにするね。
自分の手番の後で残る石の個数としてありえるものについて考えて、どれか1つでも「負け状態」になるなら、それを相手に渡せば良くて、全部「勝ち状態」ならどうやっても相手に「勝ち状態」を渡すことになるから自分は負けだよ。

rep(i,1,K+1){
	rep(j,1,N+1)if(i-a[j]>=0&&dp[i-a[j]]==0)dp[i]=1;
}
//ans=dp[K];

計算量はO(NK)だね。

L問題 Deque

長さNの数列aが与えられ、二人でゲームをする。
aの先頭か末尾の数を取り除く。取り除いた数と同じ点数を得る」というルールである。
ゲーム終了時の先手の総得点をX、後手の総得点をYとする。 先手はX−Yを最大化しようとし、後手はX−Yを最小化しようとする。二人が最適に行動するとき、X−Yを求めよ。
制約:N\leq 3*10^3

解説

太字で書いてあるところは要するに「(自分の点数)-(相手の点数)を最大化しようとする」だね!
前から取ったほうがいいか後ろから取ったほうがいいかは、残った数を使ってのゲームの結果が分かれば判断できるから、
dp[i][j]=(区間[i,j]が残ってるときの「次の手番の人の得点-そうじゃない方の人の得点」)
とすればよさそうだね。実装はメモ化再帰が簡単かな。

int f(int l,int r){
	if(flag[l][r])return dp[l][r];
	flag[l][r]=1;
	if(l==r)return dp[l][r]=a[l];
	return dp[l][r]=max(a[l]-f(l+1,r),a[r]-f(l,r-1));
}
//ans=f(1,N);

計算量はO(N^2)だね。


ふー、大変だった! これで私の担当はおしまい。
次はカラカルがM問題からを解説してくれるよ!

EDPC解説目次へ戻る

EDPC解説 A~E

EDPC解説目次へ戻る

アライさんなのだ!
EDPCのA問題~E問題を解説するのだ!

A問題 Frog 1

1Nの番号がついた足場がある。
足場iからは足場i+1i+2に移動でき、足場iから足場jに移動するコストは|h[i]-h[j]|である。
足場1から足場Nに移動するまでの最小コストを求めよ。
制約:N\leq 10^5

解説

足場Nにたどり着くには足場N-1から来るか足場N-2から来るしかないのだ!
だから答えは
「足場N-1にたどり着くまでの最小コスト+|h[N-1]-h[N]|」と
「足場N-2にたどり着くまでの最小コスト+|h[N-2]-h[N]|」の小さい方になるのだ!
ということは
dp[i]=(足場iにたどり着くまでの最小コスト)
がわかってれば良さそうなのだ!

rep(i,2,N+1){
	if(i-2>=1)dp[i]=min(dp[i-1]+abs(h[i-1]-h[i]),dp[i-2]+abs(h[i-2]-h[i]));
	else dp[i]=dp[i-1]+abs(h[i-1]-h[i]);//i-2<1のときi-2の足場はないのだ!
}
ans=dp[N];

N回ループだから計算量O(N)なのだ。

B問題 Frog 2

1Nの番号がついた足場がある。
足場iからは足場i+1,i+2,\ldots,i+Kのいずれかに移動でき、足場iから足場jに移動するコストは|h[i]-h[j]|である。
足場1から足場Nに移動するまでの最小コストを求めよ。
制約:N\leq 10^5K\leq 10^2

解説

A問題と全く同じで答えは
「足場N-1にたどり着くまでの最小コスト+|h[N-1]-h[N]|
「足場N-2にたどり着くまでの最小コスト+|h[N-2]-h[N]|
……
「足場N-Kにたどり着くまでの最小コスト+|h[N-K]-h[N]|」の最小のものなのだ!
だからやっぱり
dp[i]=(足場iにたどり着くまでの最小コスト)
がわかれば良さそうなのだ!

rep(i,2,N+1){
	dp[i]=dp[i-1]+abs(h[i-1]-h[i]);
	rep(j,2,K+1)if(i-j>=1)dp[i]=min(dp[i],dp[i-j]+abs(h[i-j]-h[i]));
}
ans=dp[N];

外側のループがN回、内側のループがK回だから計算量はO(NK)なのだ。

C問題 Vacation

N日間のそれぞれの日についてA,B,Cの3つの行動のうち1つを選んで行う。
i日目にAをすると幸福度a[i]得る。同様にB,Cをするとb[i],c[i]を得る。
同じ行動を2日以上連続して選ぶことが出来ないとき、幸福度の和の最大値を求めよ。
制約:N\leq 10^5

解説

さっきまでの問題と同じように
dp[i]=(i日目までに得られる幸福度の和の最大値)
としたいけど、それじゃダメなのだ。例えばa[N]=1,b[N]=1,c[N]=1000だったときには、最終日にはCを選んだ方が良さそうだけど、前の日にCを選んでたら困るのだ!
「2日連続で選べない」という条件は、前の日に何をしたかわかってればどうにかできるから
dp[i][j]=(i日目に行動jをして、i日目までに得られる幸福度の和の最大値)
を考えれば良さそうなのだ!
i日目に行動Aをするには、前日の行動はBCじゃないとダメだから
dp[i][A]=\max(dp[i-1][B]+a[i],dp[i-1][C]+a[i])
になるのだ!dp[i][B]dp[i][C]も同様なのだ。
最終的な答えは、最終日の行動がどれでもいいから
\max(dp[N][A],dp[N][B],dp[N][C])
なのだ!
実際には文字を添え字に持つのは大変だから、A→0、B→1、C→2としてるのだ。

rep(i,1,N+1){
	dp[i][0]=max(dp[i-1][1]+a[i],dp[i-1][2]+a[i]);
	dp[i][1]=max(dp[i-1][0]+b[i],dp[i-1][2]+b[i]);
	dp[i][2]=max(dp[i-1][0]+c[i],dp[i-1][1]+c[i]);
}
ans=max(dp[N][0],max(dp[N][1],dp[N][2]));

A[i][0]=a[i],\ A[i][1]=b[i],\ A[i][2]=c[i]としてa[i],b[i],c[i]を1つの2次元配列にまとめて持つとループで書けて簡単なのだ!

rep(i,1,N+1)rep(j,0,3){
	rep(k,0,3)if(j!=k)dp[i][j]=max(dp[i][j],dp[i-1][k]+A[i][j]);
}
ans=max(dp[N][0],max(dp[N][1],dp[N][2]));

N回ループだから計算量O(N)なのだ。

D問題 Knapsack 1

N個の品物があり、品物iは重さw[i]、価値v[i]である。
重さの総和がW以下になるように品物を選ぶとき、選んだ品物の価値の総和の最大値を求めよ。
制約:N\leq10^2W\leq10^5v[i]\leq 10^9

解説

ナップサックDPなのだ!
dp[i]=(i番目までの品物を、重さがW以下になるように選んだときの価値の最大値)
じゃやっぱりダメで、次の品物を使っていいかどうかがわからないのだ。
次の品物を使っていいかどうかは、前までの重さがわかってればわかりそうな気がするから
dp[i][j]=(i番目までの品物を、重さがj以下になるように選んだときの価値の最大値)
を考えるのだ。するとdp[i][j]は、
i番目の品物を使うとき「i-1番目までで重さがj-w[i]以下の価値の最大値+v[i]
使わないとき「i-1番目までの重さがj以下の価値の最大値」
という2つの場合の大きい方になるのだ

rep(i,1,N+1)rep(j,0,W+1){
	if(j-w[i]>=0)dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
	else dp[i][j]=dp[i-1][j];//j-w[i]<0のときはi番目の品物は使えないのだ!
}
ans=dp[N][W];

外側のループがN回、内側のループがW回だから計算量はO(NW)なのだ。

E問題 Knapsack 2

N個の品物があり、品物iは重さw[i]、価値v[i]である。
重さの総和がW以下になるように品物を選ぶとき、選んだ品物の価値の総和の最大値を求めよ。
制約:N\leq10^2W\leq10^9v[i]\leq 10^3

解説

D問題と全く同じなのだ。…………なのだ?
W\leq10^9になってるから、全く同じじゃダメなのだ。
D問題とは添字と値を逆にして
dp[i][j]=(i番目までの品物を、価値がjになるように選んだときの最小重さ)
とすると解けるのだ。
最後にdp[N][j]jが大きいところから見ていって、初めてW以下になったときのjが答えなのだ!

rep(i,0,N+1)rep(j,0,100001)dp[i][j]=INF;
dp[0][0]=0;
rep(i,1,N+1)rep(j,0,100001){
	if(j-v[i]>=0)dp[i][j]=min(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
	else dp[i][j]=dp[i-1][j];//j-v[i]<0のときはi番目の品物は使えないのだ!
}
ans=100000;
while(dp[N][ans]>W)ans--;

jのループをどこまでする必要があるかといえば、全部の品物を選んだときの\sum v[i]\leq 10^5なのだ。だから計算量はO(N\sum v[i])なのだ。

……「これを思いつくのは無理」? 確かに難しいけど、次のどれかの発想を使って思いつくのだ!
・発想1:制約を見る
この問題で考えるべきものは「何番目の品物まで使ったか」「価値はいくらか」「重さはいくらか」の3つなのだ。制約を考えると、状態として持てるのは多くても前の2つだけなのだ。だから
dp[ 何番目][価値]=(i番目までの品物を、価値に関するなんらかの条件を満たすように選んだときの重さに関するなにか)
という形にするしかないのだ。あとは頑張って考えるのだ!
・発想2:boolのDP
まずはさっき言った「考えるべきもの3つ」を全部状態に持つDPを考えてみるのだ。
dp[i][j][k]=(i番目までの品物を、価値がj、重さがkとなるように選べるか?)
ここで「『はい』か『いいえ』を値に持つようなDPは、添字を1つ選んで「~が『はい』になる〇〇の最小値(最大値)」という形に書き直せることが多い」というテクニックを使うと、思いつけるのだ!
・発想3:添字の入れ替え
「~となる〇〇の最大値」を値に持つDPは、適当な添字と入れ替えて「~となる××の最小値」というDPにできることが多いのだ!
D問題の解法にこれを使えば思いつけるのだ!
これは発想2の考え方「~となる○○の最大値」→「~とできるか?」→「~となる××の最小値」の間を飛ばしたものとも言えるのだ。


これでアライさんの解説はおしまいなのだ。
アライさんにかかればこんなの朝飯前なのだ!
次のF問題からはサーバルが解説してくれるのだ。

EDPC解説目次へ戻る

DPまとめコンテスト(EDPC)解説 目次

みなさん、こんにちは。パークガイドのミライです。
先日行われた『DPまとめコンテスト/Educational DP Contest』、通称EDPCの解説を競プロフレンズのみなさんに書いていただきました。
個別の解説に入るまえに、解説の中で登場するコードについて注意をいくつかしておきます。
・解説で使われるコードはC言語で書かれたものです。
・入出力は省略しています。入力で与えられる配列は入力例にならい1-indexedです
・DP配列は0で初期化されています。
・次の5つのマクロが定義されています。

#define rep(i,l,r)for(int i=(l);i<(r);i++)
#define max(p,q)((p)>(q)?(p):(q))
#define min(p,q)((p)<(q)?(p):(q))
#define INF ((1LL<<62)-(1LL<<31)) /*オーバーフローしない程度に大きい数*/
#define MOD 1000000007

repが半開区間になっていること以外は自然なものだと思いますので、混乱はないと思いますが注意してください。


それでは解説をどうぞ。
EDPC解説 A~E (入門編) アライグマさん
EDPC解説 F~L (基本編) サーバルさん
EDPC解説 M~T (応用編) カラカルさん
EDPC解説 U~Z (発展編) フェネックさん

さて、せっかくなので私も「DPとはなにか」について話してみましょうか。
これにはさまざまな宗派がありますが、私は「ある問題を部分問題に分解し、それらの解を用いて元の問題に答える手法」だと考えています。
簡単にいうと「困難は分割せよ」ですね。この考え方は実装するうえではもらうDPやメモ化再帰との相性がよいです。
「何を状態として持つか」が「どのような部分問題に分解するか」と対応し、「状態遷移がどのようになっているか」が「部分問題の解から元の問題の解をどうやって得るか」に対応しています。
この考え方だけで基本的なDPの問題は多くが解けるようになるので是非覚えてくださいね。