二重再帰関数のランタイムを決定するにはどうすればよいですか?


15

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

例(擬似コード):

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));
}

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

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


2
これは宿題ですか?
バーナード

5
いいえ、それは夏の時間であり、私は学ぶのが好きです。脳をドロドロにするのではなく、先に進むと考えています。
if_zero_equals_one

11
わかった これをStack Overflowに移行する投票者:これはここではトピックであり、Stack Overflowではトピック外です。Programmers.SEは、概念的なホワイトボードの質問用です。スタックオーバーフローは、実装中の問題、コーディング中の問題です。

3
そもそも私がここでやった理由です。それに、魚を受け取るよりも釣り方を知ったほうがいい。
if_zero_equals_one

1
この特定のケースでは、b(a(0))が他の多くのb(a(0))項を無限に呼び出すため、一般に無限再帰です。それが数式だったら違うでしょう。もしあなたの設定が異なっていたら、それは違ったやり方で働いていただろう。数学のように、csでは解決策がある問題、解決できない問題、簡単な問題、そうでない問題があります。ソリューションが存在する相互再帰的なケースが多数あります。時々、スタックを吹き飛ばさないために、トランポリンパターンを使用する必要があります。
ジョブ

回答:


11

機能を変え続けます。ただし、変換せずに永久に実行されるものを選択してください。

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

トレースして回答が得られた場合は、回答間のパターンまたは再帰関係を考えてみてください。それが得られたら、そのランタイムを把握することができます。それを理解することは非常に非常に複雑になる可能性がありますが、多くの場合に答えを理解できるマスター定理などの結果があります。

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

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

現在、この関数が常に明確に定義されているかどうかは不明であり、そのランタイムは何なのかは言うまでもありません。


5

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


ここでは特定の機能を探していません。私はお互いを呼び出す再帰関数の実行時間を見つける一般的な方法を探しています。
if_zero_equals_one

1
一般的な場合に解決策があるかどうかはわかりません。Big-Oが意味をなすためには、アルゴリズムが停止するかどうかを知る必要があります。計算にかかる時間を知る前に計算を実行する必要がある再帰アルゴリズムがいくつかあります(たとえば、ポイントがMandlebrotセットに属するかどうかを判断する)。
11

常にではaなくb、渡された数が0以上の場合にのみ呼び出します。しかし、はい、無限ループがあります。
btilly

1
@btillyは、回答を投稿した後に例が変更されました。
11

1
@jimreed:そして再び変更されました。可能であればコメントを削除します。
btilly

4

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

関数の実行時間を見つけることは、ライスの定理により同様に決定できません。実際、ライスの定理は、関数が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_b生成することです。

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


1

他の人が指摘したように、再帰の分析は非常に速く非常に難しくなる可能性があります。ここではそのようなことのもう一つの例である:http://rosettacode.org/wiki/Mutual_recursion http://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 and 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 これは、2つの数値を加算することが関数呼び出しよりもはるかに高速であると仮定しました。これが当てはまらない場合、これは真になります:R(F(1))= R(F(0))+ 1 + R(G(0))、およびこの分析はより複雑でした。おそらく簡単な閉じたフォームのソリューションなし。

フィボナッチ数列の閉じた形式は、いくつかのより複雑な例を言うまでもなく、必ずしも簡単に再発明することはできません。


0

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

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)ます。これaは、満たされない値にプラグインできないことを意味しますx <= -4。そうすると、計算される値が計算される値に依存する無限ループになってしまうからです。したがって、本質的にこの例には一定の実行時間があります。

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


-5

Big-Oのようなランタイム?

それは簡単です:O(N) -終了条件があると仮定します。

再帰は単なるループであり、単純なループは、そのループで何回実行しても、O(N)です(別のメソッドを呼び出すことは、ループの別のステップにすぎません)。

興味深いのは、1つ以上の再帰メソッド内にループがある場合です。その場合、何らかの指数関数的なパフォーマンス(メソッドを通過するたびにO(N)を掛ける)になります。


2
Big-Oのパフォーマンスを決定するには、呼び出されたメソッドの最高の順序を取得し、呼び出し元のメソッドの順序で乗算します。ただし、指数パフォーマンスと要因パフォーマンスについて話し始めると、多項式パフォーマンスを無視できます。指数関数的と要因的を比較するときも同じことが言えると思います:要因的勝利。私がいたシステムを分析しなければならなかったことがありません両方の指数と階乗を。
アノン

5
これは間違っています。n番目のフィボナッチ数とクイックソートを計算する再帰形式は、それぞれO(2^n)およびO(n*log(n))です。
-unpythonic


4
なぜ人々はこの恐ろしく間違った答えに投票したのですか?メソッドの呼び出しには、メソッドにかかる時間に比例した時間がかかります。この場合、メソッドa呼び出しbb呼び出しなaので、どちらのメソッドにも時間がかかると単純に想定することはできませんO(1)
btilly

2
@Anon-ポスターは、上記の関数だけでなく、任意の二重再帰関数を要求していました。説明に合わない単純な再帰の例を2つ挙げました。古い標準を「二重再帰」形式に変換するのは簡単です。指数関数型(警告に適合)とそうでない型(カバーされていない)です。
-unpythonic
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.