web-dev-qa-db-ja.com

二重再帰関数の実行時間を決定するにはどうすればよいですか?

任意の二重再帰関数がある場合、実行時間をどのように計算しますか?

例(疑似コード):

int a(int x){
  if (x < = 0)
    return 1010;
  else
    return b(x-1) + a(x-1);
}
int b(int y){
  if (y <= -5)
    return -2;
  else
    return b(a(y-1));
}

またはそれらの線に沿って何か。

このようなものを決定するために使用できる方法または使用すべき方法は何ですか?

15

あなたはあなたの機能を変え続けます。しかし、変換せずに永遠に実行されるものを選び続けます。

再帰は複雑で高速になります。提案された二重再帰関数を分析する最初のステップは、いくつかのサンプル値でそれをトレースして、それが何をするかを確認することです。計算が無限ループに入った場合、関数は適切に定義されていません。計算がより大きな数値を取得し続けるスパイラルに入る場合(非常に簡単に発生します)、それはおそらく十分に定義されていません。

トレースして回答が得られた場合は、回答間のパターンまたは反復関係を考え出そうとします。それができたら、その実行時間を把握することができます。それを理解することは非常に非常に複雑になる可能性がありますが、多くの場合に答えを理解させる 修士定理 などの結果があります。

単一の再帰でさえ、ランタイムが計算方法がわからない関数を簡単に思い付くことに注意してください。たとえば、次のことを考慮してください。

def recursive (n):
    if 0 == n%2:
        return 1 + recursive(n/2)
    Elif 1 == n:
        return 0
    else:
        return recursive(3*n + 1)

現在は不明 この関数が常に適切に定義されているかどうか、さらにはそのランタイムが何であるかはもちろんです。

11
btilly

その特定の関数のペアの実行時間は、どちらも他方を呼び出さずに戻るため、無限です。 aの戻り値はalwaysbへの呼び出しの戻り値に依存しますalwaysaを呼び出します...そして、それはinfinite recursionとして知られています。

5
jimreed

明白な方法は、関数を実行し、その所要時間を測定することです。ただし、これは特定の入力にかかる時間を示すだけです。そして、関数が終了することを事前に知らなければ、難しいです。関数が終了するかどうかを判断する機械的な方法はありません。それが 停止の問題 であり、決定できません。

関数の実行時間を見つけることは、 ライスの定理 によって同様に決定できません。実際、ライスの定理は、関数がO(f(n))時間で実行されるかどうかを決定することさえ決定できないことを示しています。

したがって、一般的にできる最善のことは、人間の知能(私たちの知る限り、チューリングマシンの制限に拘束されない)を使用して、パターンを認識または発明することです。関数の実行時間を分析する一般的な方法は、関数の再帰的定義をその実行時の再帰方程式(または相互再帰関数の一連の方程式)に変換することです。

T_a(x) = if x ≤ 0 then 1 else T_b(x-1) + T_a(x-1)
T_b(x) = if x ≤ -5 then 1 else T_b(T_a(x-1))

次は何?これで数学の問題が発生しました。これらの関数方程式を解く必要があります。よく機能するアプローチは、整数関数のこれらの方程式を分析関数の方程式に変換し、微積分を使用してこれらを解決し、関数T_aおよびT_bgenerating functions として解釈することです。

関数の生成やその他の離散数学のトピックについては、Ronald Graham、Donald Knuth、およびOren Patashnikによる本 Concrete Mathematics をお勧めします。

他の人が指摘したように、再帰の分析は非常に速く非常に難しくなる可能性があります。そのようなものの別の例を次に示します: http://rosettacode.org/wiki/Mutual_recursionhttp:// en.wikipedia.org/wiki/Hofstadter_sequence#Hofstadter_ Female_and_Male_sequences これらの回答と実行時間を計算することは困難です。これは、これらの相互再帰関数が「難しい形式」を持っているためです。

とにかく、この簡単な例を見てみましょう:

http://pramode.net/clojure/2010/05/08/clojure-trampoline/

_(declare funa funb)
(defn funa [n]
  (if (= n 0)
    0
    (funb (dec n))))
(defn funb [n]
  (if (= n 0)
    0
    (funa (dec n))))
_

funa(m), m > 0を計算してみましょう:

_funa(m) = funb(m - 1) = funa(m - 2) = ... funa(0) or funb(0) = 0 either way.
_

ランタイムは次のとおりです。

_R(funa(m)) = 1 + R(funb(m - 1)) = 2 + R(funa(m - 2)) = ... m + R(funa(0)) or m + R(funb(0)) = m + 1 steps either way
_

次に、もう少し複雑な例を選択します。

http://planetmath.org/encyclopedia/MutualRecursion.html に触発された、それ自体は読みやすいので、 "" "フィボナッチを見てみましょう。数値は相互再帰により解釈できます:F(0) = 1およびG(0) = 1、F(n + 1)= F(n) + G(n)およびG(n + 1)= F(n)。 "" "

では、Fのランタイムは何ですか?私たちは別の方法で行きます。
まあ、R(F(0)) = 1 = F(0); R(G(0)) = 1 = G(0)
今R(F(1)) = R(F(0)) + R(G(0)) = F(0) + G(0) = F(1)
...
R(F(m)) = F(m)であることを確認するのは難しくありません。インデックスiのフィボナッチ数を計算するために必要な関数呼び出しの数は、インデックスiのフィボナッチ数の値に等しくなります。これは、2つの数値を加算する方が関数呼び出しよりもはるかに高速であると想定しています。これが当てはまらない場合、これは真になります:R(F(1)) = R(F(0)) + 1 + R(G(0)) 、そしてこれの分析はもっと複雑で、おそらく簡単な閉形式のソリューションがなかったでしょう。

フィボナッチ数列の閉じた形式は、いくつかのより複雑な例は言うまでもなく、再発明が必ずしも容易ではありません。

1
Job

最初に行うことは、定義した関数が終了し、その値が正確であることを示すことです。定義した例では

_int a(int x){
  if (x < = 0)
    return 1010;
  else
    return b(x-1) + a(x-1);
}
int b(int y){
  if (y <= -5)
    return -2;
  else
    return b(a(y-1));
}
_

bは_y <= -5_でのみ終了します。他の値をプラグインすると、b(a(y-1))という形式の項ができるからです。もう少し拡張すると、b(a(y-1))という形式の用語が最終的に用語b(1010)につながり、用語b(a(1009))につながることがわかります。再び用語b(1010)につながります。これは、_x <= -4_を満たさないaに値をプラグインできないことを意味します。そうすると、計算される値が計算される値に依存する無限ループが発生するためです。したがって、この例では基本的に実行時間が一定です。

したがって、単純な答えは、再帰的に定義された関数が終了するかどうかを決定する一般的な手順がないため、再帰関数の実行時間を決定する一般的な方法がないということです。

0
davidk01