web-dev-qa-db-ja.com

Javaのフィボナッチのようなシーケンスの非再帰的なソリューションは何ですか?

関数のこの擬似コードを考えると

f(0) = 1; 
f(1) = 3; 
f(n) = 3 * f(n - 1) - f(n - 2); // for n >= 2.

これを行う非再帰的な方法はありますか?

13
cclerv

はい、すべての再帰的アルゴリズムは反復アルゴリズムに変換できます。問題の再帰的な解決策は、(擬似コード)のようなものです。

_def f(n):
    if n == 0: return 1
    if n == 1: return 3
    return 3 * f(n-1) - f(n-2)
_

現在の用語を計算するには、前の2つの用語を覚えておくだけでよいので、次の擬似コードのようなものを使用できます。

_def f(n):
    if n == 0:
        return 1
    if n == 1:
        return 3
    grandparent = 1
    parent = 3
    for i = 2 to n:
        me = 3 * parent - grandparent
        grandparent = parent
        parent = me
    return me
_

これは、最初に「再帰的」終了条件を処理し、次に通常は自分自身を呼び出す場所を反復するだけです。各反復で、現在の用語を計算してから、祖父母と親を介して用語をローテーションします。

現在の反復を計算した後は、使用されなくなったため、祖父母を維持する必要はありません。

実際、項は再帰的ソリューションのように再計算されないため、(パフォーマンスの観点から)反復ソリューションの方が優れていると言えます。ただし、再帰的ソリューションそれについて一定の優雅さを持っています(再帰的ソリューションは一般的にそうです)。


もちろん、フィボナッチ数列のように、計算する値は非常に急速に上昇するため、おそらく最速の解決策が必要な場合(私のものを含むすべてのパフォーマンスの主張を確認する必要があります)、事前に計算されたルックアップテーブルが最適です。

次のJavaコードを使用して長い値のテーブルを作成します(そのwhile条件は、オーバーフローをキャッチするための卑劣なトリックです。これは、ビルドを停止できるポイントです。アレイ):

_class GenLookup {
    public static void main(String args[]) {
        long a = 1, b = 3, c;
        System.out.print ("long lookup[] = { " + a + "L, " + b + "L");
        c = 3 * b - a;
        while ((c + a) / 3 == b) {
            System.out.print (", " + c + "L");
            a = b; b = c; c = 3 * b - a;
        }
        System.out.println (" };");
    }
} 
_

次の例のように、ルックアップ関数にプラグインするだけの配列定義を提供します。

_public static long fn (int n) {
    long lookup[] = { 1L, 3L, 8L, 21L, 55L, 144L, 377L, 987L, 2584L, 6765L,
        17711L, 46368L, 121393L, 317811L, 832040L, 2178309L, 5702887L,
        14930352L, 39088169L, 102334155L, 267914296L, 701408733L,
        1836311903L, 4807526976L, 12586269025L, 32951280099L, 86267571272L,
        225851433717L, 591286729879L, 1548008755920L, 4052739537881L,
        10610209857723L, 27777890035288L, 72723460248141L, 190392490709135L,
        498454011879264L, 1304969544928657L, 3416454622906707L,
        8944394323791464L, 23416728348467685L, 61305790721611591L,
        160500643816367088L, 420196140727489673L, 1100087778366101931L,
        2880067194370816120L, 7540113804746346429L };

    if ((n < 1) || (n > lookup.length))
        return -1L;

    return lookup[n-1];
}
_

興味深いことに、WolframAlphaは、反復さえ使用しない定型的なアプローチを考え出します。 彼らのサイト に移動してf(0)=1, f(1)=3, f(n)=3f(n-1)-f(n-2)と入力すると、次の式が返されます。

enter image description here

残念ながら、浮動小数点を使用するため、入力値の数が限られているため、Java longに収まるものになるため、反復ほど高速ではない可能性があります。 。ほぼ確実に(ただし、これを確認する必要があります)、テーブルルックアップよりも低速です。

そして、それは、非無限ストレージのような実世界の制限が機能しない数学の世界ではおそらく完璧ですが、おそらくIEEE精度の制限のために、nのより高い値で機能しなくなります。 。

次の関数は、その式とルックアップソリューションに相当します。

_class CheckWolf {
    public static long fn2 (int n) {
        return (long)(
            (5.0 - 3.0 * Math.sqrt(5.0)) *
                Math.pow(((3.0 - Math.sqrt(5.0)) / 2.0), n-1) +
            (5.0 + 3.0 * Math.sqrt(5.0)) *
                Math.pow(((3.0 + Math.sqrt(5.0)) / 2.0), n-1)
            ) / 10;
    }

    public static long fn (int n) {
        long lookup[] = { 1L, 3L, 8L, 21L, 55L, 144L, 377L, 987L, 2584L, 6765L,
            17711L, 46368L, 121393L, 317811L, 832040L, 2178309L, 5702887L,
            14930352L, 39088169L, 102334155L, 267914296L, 701408733L,
            1836311903L, 4807526976L, 12586269025L, 32951280099L, 86267571272L,
            225851433717L, 591286729879L, 1548008755920L, 4052739537881L,
            10610209857723L, 27777890035288L, 72723460248141L, 190392490709135L,
            498454011879264L, 1304969544928657L, 3416454622906707L,
            8944394323791464L, 23416728348467685L, 61305790721611591L,
            160500643816367088L, 420196140727489673L, 1100087778366101931L,
            2880067194370816120L, 7540113804746346429L };
        if ((n < 1) || (n > lookup.length)) return -1L;
        return lookup[n-1];
    }
_

次に、それらを比較するためのメインラインが必要です。

_    public static void main(String args[]) {
        for (int i = 1; i < 50; i++)
            if (fn(i) != fn2(i))
                System.out.println ("BAD:  " + i + ": " + fn(i) + ", " + fn2(i)
                    + " (" + Math.abs(fn(i) - fn2(i)) + ")");
            else
                System.out.println ("GOOD: " + i + ": " + fn(i) + ", " + fn2(i));
        }
    }
_

これは出力します:

_GOOD: 1: 1, 1
GOOD: 2: 3, 3
GOOD: 3: 8, 8
GOOD: 4: 21, 21
GOOD: 5: 55, 55
GOOD: 6: 144, 144
GOOD: 7: 377, 377
GOOD: 8: 987, 987
GOOD: 9: 2584, 2584
GOOD: 10: 6765, 6765
GOOD: 11: 17711, 17711
GOOD: 12: 46368, 46368
GOOD: 13: 121393, 121393
GOOD: 14: 317811, 317811
GOOD: 15: 832040, 832040
GOOD: 16: 2178309, 2178309
GOOD: 17: 5702887, 5702887
GOOD: 18: 14930352, 14930352
GOOD: 19: 39088169, 39088169
GOOD: 20: 102334155, 102334155
GOOD: 21: 267914296, 267914296
GOOD: 22: 701408733, 701408733
GOOD: 23: 1836311903, 1836311903
GOOD: 24: 4807526976, 4807526976
GOOD: 25: 12586269025, 12586269025
_

ここまで見栄えが良く、もう少し:

_GOOD: 26: 32951280099, 32951280099
GOOD: 27: 86267571272, 86267571272
GOOD: 28: 225851433717, 225851433717
GOOD: 29: 591286729879, 591286729879
GOOD: 30: 1548008755920, 1548008755920
GOOD: 31: 4052739537881, 4052739537881
GOOD: 32: 10610209857723, 10610209857723
GOOD: 33: 27777890035288, 27777890035288
GOOD: 34: 72723460248141, 72723460248141
GOOD: 35: 190392490709135, 190392490709135
GOOD: 36: 498454011879264, 498454011879264
_

しかし、その後、何かがうまくいかなくなり始めます。

_BAD:  37: 1304969544928657, 1304969544928658 (1)
BAD:  38: 3416454622906707, 3416454622906709 (2)
BAD:  39: 8944394323791464, 8944394323791472 (8)
BAD:  40: 23416728348467685, 23416728348467705 (20)
BAD:  41: 61305790721611591, 61305790721611648 (57)
BAD:  42: 160500643816367088, 160500643816367232 (144)
BAD:  43: 420196140727489673, 420196140727490048 (375)
_

上記が興味をそそるほど近く、エラーの桁数が結果の桁数に比例するという事実は、おそらく精度の低下の問題であることを示しています。

この時点以降、公式関数は最大のlong値を返し始めます。

_BAD:  44: 1100087778366101931, 922337203685477580 (177750574680624351)
BAD:  45: 2880067194370816120, 922337203685477580 (1957729990685338540)
BAD:  46: 7540113804746346429, 922337203685477580 (6617776601060868849)
_

そして、数値が長すぎるため、ルックアップ関数も機能しなくなります。

_BAD:  47: -1, 922337203685477580 (922337203685477581)
BAD:  48: -1, 922337203685477580 (922337203685477581)
BAD:  49: -1, 922337203685477580 (922337203685477581)
_
45
paxdiablo

ここでの答えは正しいですが、O(n)で機能しますが、O(log n)で実行すると、指数関数的に高速になります。それを観察する

[f(n)  ] = [3 -1] [f(n-1)]
[f(n-1)]   [1  0] [f(n-2)]

Vをしましょうn ベクトル[f(n)、f(n-1)]であり、Aは上記の行列であるため、vが得られます。n = A vn-1、したがってvn = An-1 v12進数のべき乗 を使用して行列Aの(n-1)乗を計算し、vを掛けます1。線形再発の詳細については、 ここ を参照してください。

12
sdcvvc

関数の同等の非再帰的定義が見つかるかどうかについての質問の場合は、 フィボナッチ数列 のプロパティを検索する必要があります。

あなたのシーケンスは、フィボナッチ(最初の2つの数字なし)を書き、2番目ごとの数字を削除することによって見つけることができます:1、3、8、21、55、144、...

sqrt5 = sqrt(5)
phi = ( 1 + sqrt5 ) / 2
fibonacci(n) = round( power( phi, n ) / sqrt5 ) 
f(n) = fibonacci( 2*n + 2 )
10
ypercubeᵀᴹ

簡単です。Javaでは、ソリューションは次のようになります。

public int f(int n) {

      int tmp;
      int a = 3;
      int b = 1;

      for (int i = 0; i < n; i++) {
          tmp = a;
          a = 3 * a - b;
          b = tmp;
      }

      return b;

}

すべての再帰的解は反復解に変換できます(反対も当てはまります。これを参照してください post )。ただし、再帰的解が末尾再帰形式の場合は簡単です。

上記のアルゴリズムは、元の再帰に対する動的計画法のソリューションとして理解できます。反復の各ポイントで前の2つの値を保存するだけでよいため、非常に効率的です。

7
Óscar López

[おっと、これはPerlの質問だと思いました。それでも、コードはJava開発者。]に十分に読める必要があります。

これは実際には再帰をユーザーランドに移動するだけですが、次を使用できます。

sub f { 
    my ($n) = @_;
    my @f = (1,3);
    $f[$_] = 3 * $f[$_-1]- $f[$_-2] for 2 .. $n;
    return $f[$n];
}

もちろん、これはキャッシュを要求します。すでにわかっている値を再計算する必要はありません。

my @f = (1,3);
sub f { 
    my ($n) = @_;
    $f[$_] = 3 * $f[$_-1]- $f[$_-2] for @f .. $n;
    return $f[$n];
}
2
ikegami

関数はそれ自体で定義されているため、ある数学者が来てf(n)f(n-1)を評価せずにf(n-2)を評価できると言わない限り、ある意味では実装は再帰的です。他の人が示しているように、それ自体を呼び出さないJava関数に実装する方法があります。

2
def func(n):
    f= array(n+1)
    f[0]=1
    f[1]=3

    for i in 2:n :
        f[i] = 3*f[i-1]-f[i-2]
    return f[n]
1
ElKamina

これは、コードの行が最小で柔軟性が最大の関数です。

add任意の「初期値」およびその他の再帰的な「関数」を簡単に実行できます。

def fib(n):
  fibs = [1, 3]       # <--  your initial values here 
  if n == 1:
    return fibs[0]
  if n == 2:
    return fibs[:1]
  for i in range(2, n):
    fibs.append(3*fibs[-1] - fibs[-2])  # <-- your function here
  return fibs

そして結果は次のとおりです。

n=10
print(fib(n))

[1, 3, 8, 21, 55, 144, 377, 987, 2584, 6765]
1
imanzabet

フィボナッチ数列の番号は次のように始まります:0,1,1,2,3,5,8,13,21,34,55 ....

これは、n> 1の単純な漸化式F(n F(n-1)+ F(n-2)と2つの初期条件F( 0)= 1およびF(1)= 1)==

アルゴリズムフィボナッチ

// n番目のフィボナッチ数を計算します

//入力:非負の整数

//出力:n番目のフィボナッチ数

1. Begin Fibo
2. integer n, i;
3.    if n<=1 then
4.     return n;
5.  else
6.    F(0)<-0; F(1)<-1;
7.    for i<-2 to n do
8.     F(i)<-F(i-1)+F(i-2);
9.     F(i-2)=F(i-2);
10.    F(i-1)=F(i);
11. done
12. end if
13. end Fibo
1

@paxdiabloからのリクエストに応じて、私はこれに答えています。これは漸化式であり、別の回答で言及されているフィボナッチ数列と同様に、非再帰的に解くことができます。それは(Python表記)であることが判明しました。

def f(n):
    return int((13**0.5-3)/(2*13**0.5)*((3-13**0.5)/2)**n + (0.5+3/(2*13**0.5))*((3+13**0.5)/2)**n)

ただしこのフォーラムは、浮動小数点の精度が制限されているため、nが大きい場合はおそらく動作しません。指定されたpythonバージョンはn = 30で失敗します:

>>> print ([f(n) == 3 * f(n-1) + f(n-2) for n in range(2, 30)])
[True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, False]
>>> print([f(n) for n in range(30)])
[1, 3, 10, 33, 109, 360, 1189, 3927, 12970, 42837, 141481, 467280, 1543321, 5097243, 16835050, 55602393, 183642229, 606529080, 2003229469, 6616217487, 21851881930, 72171863277, 238367471761, 787274278560, 2600190307441, 8587845200883, 28363725910090, 93679022931153, 309400794703549, 1021881407041801]

警告:「-」の代わりに「+」を使用したため、式が間違っています。コメントを参照してください。

0