質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.47%
再帰

情報工学における再帰とは、プログラムのあるメソッドの処理上で自身のメソッドが再び呼び出されている処理の事をいいます。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

1回答

434閲覧

AtcoderのEducational DP Contestの問題 I - Coins を動的計画で解いた場合とメモ化再帰で解いた場合の実行時間の違いについて

MONOSPEC

総合スコア1

再帰

情報工学における再帰とは、プログラムのあるメソッドの処理上で自身のメソッドが再び呼び出されている処理の事をいいます。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

0グッド

0クリップ

投稿2023/06/01 12:43

編集2023/06/01 13:49

実現したいこと

  • ある問題を動的計画で解いた場合とメモ化再帰で解いた場合とで、実行時間にどの程度差が生じるのか知りたい。

前提

私は動的計画法について勉強中で、atcoder内の以下の問題に取り組んでいます。
URL: https://atcoder.jp/contests/dp/tasks/dp_i

実行時間制限: 2 sec / メモリ制限: 1024 MB **問題文** N を正の奇数とします。 N 枚のコインがあります。 コインには 1,2,…,N と番号が振られています。 各 i (1≤i≤N) について、コイン i を投げると、確率 p_iで表が出て、確率 1−p_i で裏が出ます。 太郎君は N 枚のコインをすべて投げました。 このとき、表の個数が裏の個数を上回る確率を求めてください。 **制約** - N は奇数である。 - 1 ≤ N ≤ 2999 - p_i は実数であり、小数第 2 位まで与えられる。 - 0 < p_i <1 **入力** 入力は以下の形式で標準入力から与えられる。 N p_1 ​p_2 … p_N ​ **出力** 表の個数が裏の個数を上回る確率を出力せよ。 絶対誤差が 10^(−9) 以下ならば正解となる。

質問

この問題を動的計画とメモ化再帰で解く2つのプログラムをそれぞれ作成したのですが、動的計画では任意の入力に対して100 msec以内で終了するのに対して、メモ化再帰の場合は制限時間の2 sec を超過してしまいます。

動的計画とメモ化再帰の違いに関して、

  • ボトムアップかトップダウンどちらで最終的な解答を得ようとするかの違いはあるものの、内部の計算は似たもの
  • メモ化再帰では関数を繰り返し呼び出すことによるオーバヘッドが生じるため、動的計画よりは遅くなる可能性がある

と理解しているのですが、それだけで実行時間にここまで大きな差が生まれるものなのでしょうか?
それとも、私のメモ化再帰の実装が悪いために遅くなってしまっているのでしょうか?

ぜひ、教えていただければ幸いです。

動的計画にもとづくプログラム

C++

1#include <iomanip> 2#include <iostream> 3#include <vector> 4 5int main() { 6 int N; 7 std::cin >> N; 8 std::vector<double> p(N, 0.0); 9 for (int i = 0; i < N; i++) { 10 std::cin >> p[i]; 11 } 12 13 std::vector<std::vector<double>> dp(N + 1, std::vector<double>(N + 1, 0.0)); 14 dp[0][0] = 1.0; 15 for (int i = 0; i < N; i++) { 16 for (int j = 0; j <= i; j++) { 17 dp[i + 1][j] += dp[i][j] * (1.0 - p[i]); 18 dp[i + 1][j + 1] += dp[i][j] * p[i]; 19 } 20 } 21 22 double res = 0.0; 23 for (int i = N / 2 + 1; i <= N; i++) { 24 res += dp[N][i]; 25 } 26 27 std::cout << std::fixed << std::setprecision(10) << res << std::endl; 28 29 return 0; 30}

メモ化再帰にもとづくプログラム

C++

1#include <iomanip> 2#include <iostream> 3#include <vector> 4 5int N; 6std::vector<double> p(10000, 0.0); 7std::vector<std::vector<double>> dp(10000, std::vector<double>(10000, 0.0)); 8 9double rec(int i, int j) { 10 if (dp[i][j] != 0.0) { 11 return dp[i][j]; 12 } 13 if (i == 0 and j == 0) { 14 return 1.0; 15 } 16 if (i < j) { 17 return 0.0; 18 } 19 if (i > 0 and j == 0) { 20 return dp[i][j] += rec(i - 1, j) * (1.0 - p[i - 1]); 21 } 22 return dp[i][j] += rec(i - 1, j) * (1.0 - p[i - 1]) + rec(i - 1, j - 1) * p[i - 1]; 23} 24 25int main() { 26 std::cin >> N; 27 for (int i = 0; i < N; i++) { 28 std::cin >> p[i]; 29 } 30 31 double result = 0.0; 32 for (int j = N / 2 + 1; j <= N; j++) { 33 result += rec(N, j); 34 } 35 36 std::cout << std::fixed << std::setprecision(10) << result << std::endl; 37 38 return 0; 39}

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

計算済みかをif (dp[i][j] != 0.0)で判定しているのが間違いです。
この判定だと、計算結果が0.0になった場合と区別ができません。

例えば、入力として、N=2999、p1~pN=0.5 を与えた場合、dp[2999][2507]あたりでdoubleで表現できる限界を下回り、計算結果が0.0と等しくなってしまいます。そうなると、計算済みなのに未計算とみなされ、毎回計算が行われてしまいます。

計算済みかの判定は、計算結果として表れることが絶対にない数字で行うようにしましょう。今回なら、マイナスの確率はありえないので、初期値に-1.0を入れておいて、マイナスだったら未計算と判定するなど。

c++

1#include <iomanip> 2#include <iostream> 3#include <vector> 4 5int N; 6std::vector<double> p(10000, 0.0); 7std::vector<std::vector<double>> dp(10000, std::vector<double>(10000, -1.0)); 8 9double rec(int i, int j) { 10 if (dp[i][j] >= 0.0) { 11 return dp[i][j]; 12 } 13 if (i == 0 and j == 0) { 14 return 1.0; 15 } 16 if (i < j) { 17 return 0.0; 18 } 19 if (i > 0 and j == 0) { 20 return dp[i][j] = rec(i - 1, j) * (1.0 - p[i - 1]); 21 } 22 return dp[i][j] = rec(i - 1, j) * (1.0 - p[i - 1]) + rec(i - 1, j - 1) * p[i - 1]; 23} 24 25int main() { 26 std::cin >> N; 27 for (int i = 0; i < N; i++) { 28 std::cin >> p[i]; 29 } 30 31 double result = 0.0; 32 for (int j = N / 2 + 1; j <= N; j++) { 33 result += rec(N, j); 34 } 35 36 std::cout << std::fixed << std::setprecision(10) << result << std::endl; 37 38 return 0; 39}

投稿2023/06/01 14:30

actorbug

総合スコア2224

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

MONOSPEC

2023/06/02 13:35

回答ありがとうございます! ご指摘の通りで、メモする計算結果が0.0に等しくなってしまう可能性があることを見落としていたようです。メモの初期値を-1.0とすることで解決しました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.47%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問