動的計画法解説 [SRM 468 Div2 250/Div1 500]

目次


問題1

王様は長い間出張していたので、できるだけ早く女王のもとに帰りたいと思っています。王様は都市 0 におり、女王は都市 N (1 <= N <= 50)に居ます。全ての都市 i (0 <= i <= N - 1) に対して陸路と空路があります。配列 roadTime, flightTime が与えられ、それぞれの i 番目の要素は都市 i から都市 i + 1 までの陸路、空路の所要時間 (1 以上 1000 以下) を表わします。しかし王国の科学力は低く、空路による移動は墜落の危険を伴います。そのため、王女は王様に空路は K (0 <= K <= N) 回までに留めるように言いました。王様が王女に会うための最短時間を求めなさい。

出題元 SRM 468 Div2 250

~~全探索~~

かがみ黒井先生からこんな宿題が出ていたわ。
こなたって言うかさあ。
かがみ何よ。
こなた全部空路で行けば良くね?
かがみまあ、最初はそう思うわね。でもサンプルを見ると空路の方が時間が掛かるケースもあるから特に空路<陸路という条件は無いみたいよ。
こなたでも常識で考えておかしいよね。
かがみあーもー空路がト○ロで陸路がネ○バスなんじゃないの?
こなたおお、なるほど。
かがみ納得するのか。。。まあいいわ。問題に取り掛かりましょう。
こなたとりあえずどっから取り掛かったら良いかわかんないな。
かがみ難しい問題を解く場合は簡単な問題に変えてみるという手が有効よ。例えば K の制限を無くしてみたらどう?
こなたそれなら簡単。ビットが立っている場合を空路とみなして全探索で書くとこんな感じかな。

public int minTime(int N, int[] roadTime, int[] flightTime, int K) {
	int sum, min = Integer.MAX_VALUE;
	for (int i = 0; i < (1 << N); i++) {
		sum = 0;
		for (int j = 0; j < N; j++) {
			if ((i & (1 << j)) != 0) {
				sum += flightTime[j];
			} else {
				sum += roadTime[j];
			}
		}
		if (min > sum)
			min = sum;
	}
	return min;
}
かがみそうね。これに K の制限を加えるとどうなる?
こなた空路が K 回までってことはビットの数が K 以下ってことだから、こうかな?

public int minTime(int N, int[] roadTime, int[] flightTime, int K) {
	int sum, min = Integer.MAX_VALUE;
	for (int i = 0; i < (1 << N); i++) {
		if (bitCount(i) > K)
			continue;
		sum = 0;
		for (int j = 0; j < N; j++) {
			if ((i & (1 << j)) != 0) {
				sum += flightTime[j];
			} else {
				sum += roadTime[j];
			}
		}
		if (min > sum)
			min = sum;
	}
	return min;
}

private int bitCount(int val) {
	int ret = 0;
	for (int i = 0; i < 32; i++) {
		if ((val & (1 << i)) != 0) {
			ret++;
		}
	}
	return ret;
}
かがみまあそんなとこね。
こなたよし!サンプル通った!あ、でも System test で TLE (*1) する。。。

(*1) TLE: 時間超過 (Time Limit Exceeded) の略。 TopCoder においては 2 秒以内にプログラムが終了しなければならない。

かがみ外側の i のループは N = 50 の場合は何回周るのよ?
こなた2^50 だから 1000 兆回以上か。そりゃ TLE するわな。かがみん先生!もう何も思い付きません!

~~再帰と漸化式~~

かがみじゃあヒント。再帰を使って書いてみるのよ。
こなた再帰苦手なんだよね。。。書いてる途中であれ?あれ?ってなるし。
かがみいきなり再帰のプログラムを書こうとすると途中で混乱することが多いわね。そんな時は漸化式を書いて、それを元にプログラムを書くと良いわよ。
こなたざんかしき?
かがみ「ぜんかしき」よ。例えば等差数列を表わすときに使う a[i] = a[i - 1] + 2 のようなやつよ。
こなたそれと再帰とどう関係があるの?
かがみ漸化式をプログラムで表現する場合に、再帰を使うと自然に表現できるのよ。
こなたさっきの a[i] = a[i - 1] + 2 も表現できるの?
かがみできるわよ。最初だから例を見せるわ。 a[0] = 1, a[i] = a[i - 1] + 2 の例よ。

public int rec(int N) {
	if (N == 0)
		return 1;
	return rec(N - 1) + 2;
}
こなた確かに漸化式の通りだ。今まで再帰のプログラムを見て、そのまま理解しようとしてわけわかんなくなってたけど、漸化式を見てから読むとすぐに理解できるね。
かがみそ、そう。良かったわね。
こなた何か目覚めた!何か覚醒した!私にも見える!こ、これが俺の力?
かがみじゃ、じゃあ K の制限なしの場合の問題をやってみて。
こなたうあああ、わからない。
かがみ順番に行くわよ。漸化式を作るためには「添字」と「値」の両方が必要なの。
こなたどういうこと?
かがみ例えばさっきの a[n] の例だと、添字である n を決めれば値が決まるわよね。
こなたそれ当たり前なんじゃないの?
かがみそうだけど!問題に応じて何を「添字」にして何を「値」にするのかを選択する必要があるってことよ。条件に応じて選択を変える場合もあるの。この問題の場合はそれぞれ何を使えば良いと思う?
こなたまず最短時間を求める問題だから「値」の方は最短時間で良いと思う。あと添字はそれぞれの都市を表わす i かな?
かがみそれで漸化式を立ててみて。
こなたうう。まだできない。
かがみそういう時は具体的にやってみるの。 roadTime = {1, 2, 6}, flightTime = {4, 3, 5} の場合を考えてみて。漸化式は b[i] として。
こなたb[i] は都市 i - 1 から i までの最短時間てことだね。まず i = 0 の場合は、まだどこにも行ってないから b[0] = 0 。 i = 1 の場合は陸路だと 1 、空路だと 4 かかるから b[1] = 1。 i = 2 の場合は陸路で b[2] = 2。 i = 3 の場合は空路で b[3] = 5。
かがみそう来たか。。。その方法だとそれぞれの区間ごとに最短時間が求まるけど、最終的に求めたいものは何だっけ?
こなた全部の合計。あ、だから b[0] から b[n] まで足したものが答えだね。
かがみそうなんだが。例えばさっきの数列の例だと a[n] を計算しさえすれば a[0] から a[n] まで足さなくても答えを表わしていたよね。 b[i] もそんなふうに書けないかな。
こなたつまり b[i] は都市を i 個通った時点の合計最短時間てことだね。 b[0] = 0, b[1] = 1 + b[0] = 1, b[2] = 2 + b[1] = 3, b[3] = 5 + b[2] = 8。
かがみそうね。漸化式で表わすとどうなる?
こなたえっと b[i] = (都市 i - 1 から都市 i への陸路と空路のうち短い方) + b[i - 1]。
かがみ日本語の部分を書き直すと?
こなたb[0] = 0, b[i] = min(roadTime[i - 1], flightTime[i - 1]) + b[i - 1] (i > 0)
かがみこれで漸化式はできたわね。プログラムに書き直して。
こなたできた!漸化式を見ながらだと再帰を書くとき混乱しなくて済むね。

private int[] r, f;
public int minTime(int N, int[] roadTime, int[] flightTime, int K) {
	r = roadTime;
	f = flightTime;
	return b(N);
}

private int b(int i) {
	if (i == 0)
		return 0;
	return Math.min(b(i - 1) + r[i - 1], b(i - 1) + f[i - 1]);
}
こなたいやー長かった。ようやく終わったよ。
かがみまだ続くけどね。ちょっと休憩しましょう。

~~休憩中~~

かがみ再開するわよ。
こなた思ったんだけどさ。
かがみ何よ。
こなた王様は女王に早く会いたいと思ってるかもしんないけど女王はぶっちゃけ会いたくないんじゃね?だから空路は K 以下にしろとか言ってるんじゃね?
かがみ問題批判はもういいから。さっきは K の制限を無くしていたけど今度は制限ありの場合の漸化式を書いてみて。
こなたえーと、 K が入るんだよね。漸化式にできるのかな。
かがみヒントは b[i] の形ではなく b[i, j] の形よ。
こなたi は都市の添字で b[i, j] 自体は合計最短経路というのは変わらないと思う。だから j で K を表わすのかな。
こなたi 個の都市を通った後で j 回の空路が残っている場合の合計最短経路を b[i, j] とおいたら良いんじゃないかな。
かがみそれでやってみて。
こなたj = 0 の場合は空路が無いから b[i, j] = b[i - 1, j] + r[i - 1] だね。それから i = 0 の場合はどこにも行ってないから b[i, j] = 0 だね。
かがみそうね。
こなたさっきの K 無しの場合と違って空路を使うと j が一つ減るから b[i, j] = min(b[i - 1, j] + roadTime[i - 1], b[i - 1, j - 1] + flightTime[i - 1]) (j > 0) だね。
かがみ漸化式は完成ね。プログラムにして。

private int[] r, f;
public int minTime(int N, int[] roadTime, int[] flightTime, int K) {
	r = roadTime;
	f = flightTime;
	return b(N, K);
}

private int b(int i, int j) {
	if (i == 0)
		return 0;
	int road = b(i - 1, j) + r[i - 1];
	if (j > 0)
		return Math.min(road, b(i - 1, j - 1) + f[i - 1]);
	else
		return road;
}
こなたよし!サンプル通った!あ、でもまた TLE。
かがみそうね O(2^N) なのは変わらないからね。
こなたじゃあなぜやらせた。

~~メモ化再帰~~

かがみ次の準備のためよ。なぜ TLE するか説明するために roadTime = {1, 2, 6}, flightTime = {4, 3, 5}, K = 2 の場合を図にしてみましょう。
TLE
こなたb[1, 1] の場合を2回計算してるね。
かがみそうね。条件より 0 <= i, j <= 50 だから b[i, j] は多くとも 2500 通りしかないの。それなのに時間が掛かってしまうのは無駄な計算をたくさんやっているからよ。
こなた無駄をなくせば TLE しなくなる?
かがみそうよ。無駄をなくすために、既に計算済みのものは配列に格納しておくように変えてみましょう。これを「メモ化再帰」と言うわ。
こなたできた。

private int[] r, f;
private int memo[][];
public int minTime(int N, int[] roadTime, int[] flightTime, int K) {
	r = roadTime;
	f = flightTime;
	memo = new int[N + 1][K + 1];
	for (int i = 0; i <= N; i++)
		for (int j = 0; j <= K; j++)
			memo[i][j] = -1;
	return b(N, K);
}

private int b(int i, int j) {
	if (memo[i][j] != -1)
		return memo[i][j];
	if (i == 0) {
		memo[i][j] = 0;
	} else {
		int road = b(i - 1, j) + r[i - 1];
		if (j > 0)
			memo[i][j] = Math.min(road, b(i - 1, j - 1) + f[i - 1]);
 		else
 			memo[i][j] = road;
	}
	return memo[i][j];
}
かがみ早いわね。
こなたこーゆーのは得意。あ、 TLE しなくなった。全部通ったー!今度こそ終わりだね!
かがみここまでは全て準備。
こなたぐっ。

~~動的計画法~~

かがみさあいよいよ本題の動的計画法の説明に入るわよ。
こなたで、でもさっきテスト通ったから黒井先生からの宿題は終わったんじゃ。
かがみ動的計画法を使えってホームルームで言ってたわよ。
こなたそ、そんな宿題を出す高校なんてないよね。。。
かがみつべこべ言わず行くわよ。
こなたほーい。
かがみ動的計画法を一言で言うと、さっきのメモ化再帰で使ったメモを直接求めてしまう方法よ。ちなみにメモ化再帰のことも動的計画法って呼ぶ場合があるわ。
こなたじゃあ終わりでいいじゃん。
かがみ黙れ。さっきの配列 memo[i][j] には何が入ってる?
こなたb[i, j] と同じものが入っているんだから memo[i][j] == b[i, j] でしょ?
かがみその通り。じゃあ memo[i][j] はどんな規則性で並んでいる?
こなたb[i, j] と同じ漸化式が成り立つんでしょ?つまりこう。

memo[i][0] = memo[i - 1][0] + r[i - 1]
memo[0][j] = 0
memo[i][j] = min(memo[i - 1][j] + r[i - 1], memo[i - 1][j - 1] + f[i - 1]) (j > 0)
かがみこれを再帰を使わずに求められるかしら。
こなたmemo[i][0], memo[0][j] = 0 はすぐできる。memo[i][j] は、ええと。
かがみroadTime = {1, 2, 6}, flightTime = {4, 3, 5}, K = 2 の場合をもう一度考えましょうか。
こなたまず memo[0][j] = 0 だから memo[0][0] = 0, memo[0][1] = 0, memo[0][2] = 0 だね。それから memo[i][0] = memo[i - 1][0] + r[i]。残りの memo[i][j] も全部計算して表にするとこんな感じ。
memo
かがみじゃあプログラムにしてみましょうか。動的計画法は英語で Dynamic Programming というので配列の名前は dp にすることが多いみたいよ。
こなた書けた。

public int minTime(int N, int[] roadTime, int[] flightTime, int K) {
	int dp[][] = new int[N + 1][K + 1];
	for (int j = 0; j <= K; j++)
		dp[0][j] = 0;
	int road;
	for (int i = 1; i <= N; i++) {
		for (int j = 0; j <= K; j++) {
			road = dp[i - 1][j] + roadTime[i - 1];
			if (j > 0)
				dp[i][j] = Math.min(road,
					dp[i - 1][j - 1] + flightTime[i - 1]);
			else
				dp[i][j] = road;
		}
	}
	return dp[N][K];
}
かがみこれなら明らかに O(NK) よね。
こなたSystem test 通った。ようやく終わりだ。長かった。
かがみ結構がんばったわね。あんたにしては。
こなたああ、自分をほめてあげたいよ。。。
こなたあと、最初の全探索の例はもしかして要らなかった?
かがみ初心者の場合は「全探索で問題を把握」 -> 「漸化式を作る」 -> 「再帰を書く」 -> 「動的計画法を書く」の順にやると混乱が少なくて済むからよ。いきなり漸化式から始めてたらこなたは挫折してたでしょ。
こなたうん。。。
かがみところで今回はテーマが動的計画法なので言わなかったけど、以下のコードでも解けるわ。

public int minTime(int N, int[] roadTime, int[] flightTime, int K) {
	int[] delta = new int[roadTime.length];
	int total = 0;
	for (int i = 0; i < roadTime.length; i++) {
		delta[i] = roadTime[i] - flightTime[i];
		total += roadTime[i];
	}

	Arrays.sort(delta);
	for (int i = delta.length - 1; i >=0 && K > 0 && delta[i] > 0;
		i--) {
		total -= delta[i];
		K--;
	}
	return total;
}
こなたそうか。陸路と空路の差が大きいものから順に選べば良いのか。動的計画法が無くても解けるじゃん。なんで黒井先生はこの問題を選んだんだろう。
かがみ動的計画法が必須の問題だとそれなりに難しい問題になっちゃうから入門用ってことでこれにしたんじゃない?
こなたつまりスラ○ムを倒すためにイオ○ズンを使ったってこと?やれやれ、どうせならアークデー○ンあたりの手ごたえのある問題が欲しかったよ。
かがみ(コ、コイツ終わったのをいいことに心にも無いことを。。。)
黒井よう言うた!お前がそんなに勉学に興味を持つようになるとは!教師生活 25 年!こんな嬉しい事は無いでー!
こなたい、いや、あのそういうわけでは。あと先生は 27 歳では。。。
黒井そんなお前にプレゼントや!

問題2

王様は長い間出張していたので、できるだけ早く女王のもとに帰りたいと思っています。王様は都市 0 におり、女王は都市 N (1 <= N <= 400000)に居ます。全ての都市 i (0 <= i <= N - 1) に対して陸路と空路があります。配列 roadTime, flightTime が与えられ、それぞれの i 番目の要素は都市 i から都市 i + 1 までの陸路、空路の所要時間 (1 以上 100000 以下) を表わします。roadTime は以下の式で生成されます。

	roadTime[0] = roadFirst mod roadMod;
	for i = 1 to N-1
		roadTime[i] = (roadTime[i-1]*roadProd + roadAdd) mod roadMod;
flightTime は flightFirst, flightProd, flightAdd, flightMod を使って同じように生成されます。しかし王国の科学力は低く、空路による移動は離陸に失敗する危険を伴います。そのため、王女は王様に空路は K (0 <= K <= 40 かつ 0 <= K <= N) 回までに留めるように言いました。危険なのは離陸のみなので、複数の都市を連続して空路で移動する場合は一回と数えます。王様が王女に会うための最短時間を求めなさい。

出題元 SRM 468 Div1 500

~~全探索~~

こなたい、いえ結構です。
黒井そうか!嬉しいか!ワイも嬉しいでー!ほながんばりやー。
こなたい、行ってしまった。。。か、かがみん、あの。。。
かがみ自業自得ね。私は帰るから自力で頑張りなさい。
こなたそ、そんな。待っ。。。本当に帰っちゃった。くそー。帰ってアニメを観なければいけないというのに!
こなたラスボスを倒したと思ったらそれは全部夢で2周目がようやく本番だったような気分だ。しかも2周目は格段に難しいし。
こなたとりあえずやってみるか。落ち着け、落ち着くんだ。素数を数えて落ち着くんだッ。。。
こなたよく見るとさっきの問題を難しくしただけみたいだ。
こなたまずは問題を理解するために全探索の例を書いてみるか。
こなた陸路と空路の列を全部求めておいて、それに対して全探索と。
こなたさっきと違う点は、連続した空路は一つにまとめられるということ。だから、直前が空路かどうかを prevIsFlight というフラグで表わして書けば良いか。
こなたはあ、フラグという言葉はギャルゲだけで使いたかったなあ。
こなたとりあえず書いてみた。

private int[] roadTime, flightTime;
public long minTime(int N, int roadFirst, int roadProd, int roadAdd, int roadMod,
	int flightFirst, int flightProd, int flightAdd, int flightMod, int K) {
	roadTime = new int[N];
	roadTime[0] = roadFirst % roadMod;
	for (int i = 1; i < N; i++)
		roadTime[i] = (int)(((long)roadTime[i - 1] * roadProd + roadAdd) % roadMod);
	flightTime = new int[N];
	flightTime[0] = flightFirst % flightMod;
	for (int i = 1; i < N; i++)
		flightTime[i] = (int)(((long)flightTime[i - 1] * flightProd + flightAdd) % flightMod);

	long sum, min = Integer.MAX_VALUE, flight;
	boolean prevIsFlight;
	for (int i = 0; i < (1 << N); i++) {
		sum = 0;
		flight = 0;
		prevIsFlight = false;
		for (int j = 0; j < N; j++) {
			if ((i & (1 << j)) != 0) {
				if (!prevIsFlight) {
					flight++;
					prevIsFlight = true;
				}
				sum += flightTime[j];
			} else {
				sum += roadTime[j];
				prevIsFlight = false;
			}
		}
		if (flight <= K && min > sum)
			min = sum;
	}
	return min;
}
こなたお、サンプル通った。でもやっぱ System test は TLE か。 N=400000じゃ絶対無理だよなあ。まあ、問題文の理解は合っていることがわかったので次に行こう。

~~再帰と漸化式~~

かがみまーだやってんの。
こなたあ!かがみん!口では帰ると言っておきながら気になって見に来てくれるなんて!あんたツンデレの鑑だよ。。。
かがみ相変わらず目線がおかしいわね。で、どこまで行ったのよ。
こなた全探索まで書いた。。。
かがみ次は漸化式ね。私もこの問題初見だから考えながらやるわ。
こなたってかがみん、お菓子食べ過ぎなんじゃ。。。
かがみいいのよ放課後なんだし。で、漸化式は。
こなた直前が空路かどうかを条件に入れないといけない。
かがみそうねえ。状態を増やしたくないけどとりあえず入れてみようか。 b[i, j, true|false] の形ね。
こなたb[0, j, false] = 0, b[0, j, true] = 0 まではわかる。
かがみまあ、出発地に居るだけだからな。 b[i, j, false] を考えなさいよ。
こなたうぐ。ええとお。直前が空路じゃないから b[i, j, false] = min(b[i - 1, j, true] + roadTime[i - 1], b[i - 1, j, false] + roadTime[i - 1])
かがみb[i, j, true] は直前が空路だから j > 0 の場合はずっと空路なのか、直前から空路なのかで比較が必要ね。 よって b[i, j, true] = min(b[i - 1, j - 1, false] + flightTime[i - 1], b[i - 1, j, true] + flightTime[i - 1])
こなたあと j = 0 の場合は b[i - 1, j, true] + flightTime[i - 1] だね。
かがみそれから最終状態は、 b[N, K, false] の b[N, K, true] のどちらか小さい方を選ばなきゃいけないってもの忘れずにね。じゃあ書いてみて。

private int[] roadTime, flightTime;
public long minTime(int N, int roadFirst, int roadProd, int roadAdd, int roadMod,
	int flightFirst, int flightProd, int flightAdd, int flightMod, int K) {
	roadTime = new int[N];
	roadTime[0] = roadFirst % roadMod;
	for (int i = 1; i < N; i++)
		roadTime[i] = (int)(((long)roadTime[i - 1] * roadProd + roadAdd) % roadMod);
	flightTime = new int[N];
	flightTime[0] = flightFirst % flightMod;
	for (int i = 1; i < N; i++)
		flightTime[i] = (int)(((long)flightTime[i - 1] * flightProd + flightAdd) % flightMod);

	return Math.min(b(N, K, false), b(N, K, true));
}

private long b(int i, int j, boolean prevIsFlight) {
	if (i == 0)
		return 0;
	if (prevIsFlight) {
		if (j > 0) {
			return Math.min(
				b(i - 1, j - 1, false) + flightTime[i - 1],
				b(i - 1, j, true) + flightTime[i - 1]);
		} else {
			return b(i - 1, j, true) + flightTime[i - 1];
		}
	} else {
		return Math.min(b(i - 1, j, true) + roadTime[i - 1],
			b(i - 1, j, false) + roadTime[i - 1]);
	}
}
こなたあれ?サンプルが通らない。そうか。初めて選ぶ場合には、前回が空路ってことはありえないから b[0, j, true] = 0 ではなく b[0, j, true] = [ありえない] だね。ありえないってどうやって表現するんだろ。
かがみ候補として選択されないようにするってことだから充分大きい値を入れれば良いわね。 N が 400000 以下で roadMod, flightMod が 100000 以下だから最大でも 4 * 10^10 ね。だから Long.MAX_VALUE / 2 を入れておけば充分よ。
こなた何で 2 で割るの?
かがみLong.MAX_VALUE に 1 を足すと負の値になっちゃうからよ。

private int[] roadTime, flightTime;
public long minTime(int N, int roadFirst, int roadProd, int roadAdd, int roadMod,
	int flightFirst, int flightProd, int flightAdd, int flightMod, int K) {
	roadTime = new int[N];
	roadTime[0] = roadFirst % roadMod;
	for (int i = 1; i < N; i++)
		roadTime[i] = (int)(((long)roadTime[i - 1] * roadProd + roadAdd) % roadMod);
	flightTime = new int[N];
	flightTime[0] = flightFirst % flightMod;
	for (int i = 1; i < N; i++)
		flightTime[i] = (int)(((long)flightTime[i - 1] * flightProd + flightAdd) % flightMod);

	return Math.min(b(N, K, false), b(N, K, true));
}

private long b(int i, int j, boolean prevIsFlight) {
	if (i == 0)
		return prevIsFlight ? Long.MAX_VALUE / 2 : 0;
	if (prevIsFlight) {
		if (j > 0) {
			return Math.min(
				b(i - 1, j - 1, false) + flightTime[i - 1],
				b(i - 1, j, true) + flightTime[i - 1]);
		} else {
			return b(i - 1, j, true) + flightTime[i - 1];
		}
	} else {
		return Math.min(b(i - 1, j, true) + roadTime[i - 1],
			b(i - 1, j, false) + roadTime[i - 1]);
	}
}
こなたやっとサンプル通った。。。
かがみコレ大変ね。。。
こなたうん。。。これでもまだ TLE するしね。。。
ひより(ガラガラ)はッ!
こなたあっ。
ひよりこ、これは!すいません!すいません!夕暮れの教室で二人っきりで超絶百合展開いえ仲むつまじくされているところをお邪魔するなんて!
こなたち、違、これは。
ひよりこ、この件は私の心の中に留めておいて誰にも言いません!ただ新作のネタにはします!
こなたいや、そーゆんじゃないんだって。
ひよりで、できればそのまま続けて下さい。私はお邪魔にならないように見守らせていただきます。
こなただ、だから違うんだよー。
ひよりえ。誤解?うおー!また私ってば先輩をネタに腐った妄想をー!
ひよりしかし!これが私!これぞ生きている証!創作意欲が湧いて来た!早速帰ってネームにします!
こなたい、行っちゃった。
かがみあの子あんたにそっくりね。
こなたぐは!反論したいが!反論できないッ!
かがみ今日はもう疲れたから続きは明日にしましょう。

~~動的計画法~~

~翌日~

こなたうう。。。
かがみどうしたの?
こなた夢の中に動的計画法が出てきてずっとうなされてた。
かがみ私もよ。。。何なのかしらね。コレ。
こなたぷよぷよやった後にぷよぷよの夢を見るのと同じかな。
かがみまあ似たような感覚ね。
こなたギャルゲやった後に仮想と現実の区別がつかなくなるのも同じかな。
かがみそれはあんただけよ!昨日の再帰を動的計画法に書き直すわよ。
こなた意外に簡単に書き直せた。

public long minTime(int N, int roadFirst, int roadProd, int roadAdd, int roadMod,
	int flightFirst, int flightProd, int flightAdd, int flightMod, int K) {
	int[] roadTime = new int[N];
	roadTime[0] = roadFirst % roadMod;
	for (int i = 1; i < N; i++)
		roadTime[i] = (int)(((long)roadTime[i - 1] * roadProd + roadAdd) % roadMod);
	int[] flightTime = new int[N];
	flightTime[0] = flightFirst % flightMod;
	for (int i = 1; i < N; i++)
		flightTime[i] = (int)(((long)flightTime[i - 1] * flightProd + flightAdd) % flightMod);

	long dp[][][] = new long[N + 1][K + 1][2];
	for (int j = 0; j <= K; j++) {
		dp[0][j][0] = 0;
		dp[0][j][1] = Long.MAX_VALUE / 2;
	}
	long flight;
	for (int i = 1; i <= N; i++) {
		for (int j = 0; j <= K; j++) {
			flight = dp[i - 1][j][1] + flightTime[i - 1];
			if (j > 0)
				dp[i][j][1] = Math.min(
					dp[i - 1][j - 1][0] + flightTime[i - 1], flight);
			else
				dp[i][j][1] = flight;
			dp[i][j][0] = Math.min(
				dp[i - 1][j][1] + roadTime[i - 1],
				dp[i - 1][j][0] + roadTime[i - 1]);
		}
	}
	return Math.min(dp[N][K][0], dp[N][K][1]);
}
こなたサンプル通ったけど System test は java.lang.OutOfMemoryError: Java heap space が出るね。動的計画法にしたのに。
かがみN = 400000, K = 40 のケースだとメモリが足りないわね。
こなたroadTime と flightTime は配列で持っておく必要無いんじゃないの?
かがみ確かにそうだけどそこを削っても焼け石に水よ。 dp[ ][ ][ ] を何とかしないと。
こなたN の 400000 は多いよね。
かがみ漸化式を見ると、直前の i の配列しか使っていないから i に関しては 2 つだけ用意すれば良いわね。これでメモリサイズのオーダーを O(NK) から O(K) に減らせるわ。

public long minTime(int N, int roadFirst, int roadProd, int roadAdd, int roadMod,
	int flightFirst, int flightProd, int flightAdd, int flightMod, int K) {
	int[] roadTime = new int[N];
	roadTime[0] = roadFirst % roadMod;
	for (int i = 1; i < N; i++)
		roadTime[i] = (int)(((long)roadTime[i - 1] * roadProd + roadAdd) % roadMod);
	int[] flightTime = new int[N];
	flightTime[0] = flightFirst % flightMod;
	for (int i = 1; i < N; i++)
		flightTime[i] = (int)(((long)flightTime[i - 1] * flightProd + flightAdd) % flightMod);

	long dp[][][] = new long[2][K + 1][2];
	for (int j = 0; j <= K; j++) {
		dp[0][j][0] = 0;
		dp[0][j][1] = Long.MAX_VALUE / 2;
	}
	long flight;
	for (int i = 1; i <= N; i++) {
		for (int j = 0; j <= K; j++) {
			flight = dp[(i - 1) & 1][j][1] + flightTime[i - 1];
			if (j > 0)
				dp[i & 1][j][1] = Math.min(
					dp[(i - 1) & 1][j - 1][0] + flightTime[i - 1],
					flight);
			else
				dp[i & 1][j][1] = flight;
			dp[i & 1][j][0] = Math.min(
				dp[(i - 1) & 1][j][1] + roadTime[i - 1],
				dp[(i - 1) & 1][j][0] + roadTime[i - 1]);
		}
	}
	return Math.min(dp[N & 1][K][0], dp[N & 1][K][1]);
}
こなた(i & 1) って何?
かがみ数字の下位 1 bit を取り出しているの。つまり奇数なら 1 偶数なら 0 を返すようして、これを使って配列を切り替えているのよ。
こなたなるほど。
かがみよし! System test 通った!
こなたせっかくだから roadTime と flightTime の配列を消してみる。

public long minTime(int N, int roadFirst, int roadProd, int roadAdd, int roadMod,
	int flightFirst, int flightProd, int flightAdd, int flightMod, int K) {
	long dp[][][] = new long[2][K + 1][2];
	for (int j = 0; j <= K; j++) {
		dp[0][j][0] = 0;
		dp[0][j][1] = Long.MAX_VALUE / 2;
	}
	long roadTime = roadFirst % roadMod;
	long flightTime = flightFirst % flightMod;
	long flight;
	for (int i = 1; i <= N; i++) {
		for (int j = 0; j <= K; j++) {
			flight = dp[(i - 1) & 1][j][1] + flightTime;
			if (j > 0)
				dp[i & 1][j][1] = Math.min(
					dp[(i - 1) & 1][j - 1][0] + flightTime, flight);
			else
				dp[i & 1][j][1] = flight;
			dp[i & 1][j][0] = Math.min(
				dp[(i - 1) & 1][j][1] + roadTime,
				dp[(i - 1) & 1][j][0] + roadTime);
		}
		roadTime = (roadTime * roadProd + roadAdd) % roadMod;
		flightTime = (flightTime * flightProd + flightAdd) % flightMod;
	}
	return Math.min(dp[N & 1][K][0], dp[N & 1][K][1]);
}
こなたSystem test 通った!
かがみようやくこれで終わりね。
こなた。。。
かがみあら静かね。
こなたまた黒井先生が現れないように黙っている。
黒井お。終わったらしいなー。ようやったようやった。
こなたい、いつの間に背後に。
黒井じゃあ次の問題行くで。
こなた「こなたはにげだした!」
黒井「しかしまわりこまれてしまった!」

こちらのらき☆すたアイコンを使わせていただきました

参考文献: iwi さんの資料

戻る