文字列を明示的に計算しなくてもこの問題を解決できます。これがおそらく問題を解決するための最良の方法です。結局のところ、50番目のフィボナッチ文字列を計算するように求められた場合、メモリが不足することはほぼ確実です。F(50)は12,586,269,025なので、それを保持するためだけに12GB以上のメモリが必要になります。
解決策の背後にある直感は、フィボナッチ文字列の各行が前の行の文字で構成されているため、(行、オフセット)ペアを別の(行'、オフセット')ペアに変換できることです。常に最初の文字列よりも小さいフィボナッチ文字列の場合。これを十分な回数繰り返すと、最終的には行0または行1のいずれかのフィボナッチ文字列に戻ります。この場合、答えはすぐに読み取られます。
このアルゴリズムを機能させるには、いくつかの事実を確立する必要があります。まず、ゼロインデックスになるフィボナッチ数列を定義しましょう。つまり、シーケンスは
F(0) = 0
F(1) = 1
F(n+2) = F(n) + F(n + 1)
これを考えると、フィボナッチ文字列のn番目の行(1つのインデックス)には合計F(n + 1)文字が含まれていることがわかります。これは、誘導によってすばやく確認できます。
- 行1の長さは1=F(2)= F(1 + 1)
- 行2の長さは2=F(3)= F(2 + 1)です。
- 一部の行n+2の場合、その行の長さはSize(n)+ Size(n + 1)= F(n + 1)+ F(n + 2)= F(n + 3)= F( (n + 2)+ 1)
この知識を使用して、フィボナッチ文字列の7行目の7番目の文字を検索するとします。行7は行5と6の連結で構成されていることがわかっているため、文字列は次のようになります。
R(7) = R(5) R(6)
行5にはF(5 + 1)= F(6)= 8文字が含まれています。これは、行7の最初の8文字がR(5)からのものであることを意味します。この行から7番目の文字が必要であり、7≤8であるため、この値を取得するには、行5の7番目の文字を調べる必要があることがわかります。行5は、行3と4の連結のように見えます。
R(5) = R(3) R(4)
この行の7番目の文字を見つけたいと思います。ここで、R(3)にはF(4)= 3文字が含まれています。つまり、R(5)の7番目の文字を探している場合、R(5)ではなくR(4)の部分になります。 3)一部。この行の7番目の文字を探しているので、R(4)の7-F(4)= 7-3 = 4番目の文字を探していることを意味します。ここで、ここを探します。ここでも、R(4)は次のように定義されます。
R(4) = R(2) R(3)
R(2)にはF(3)= 2文字が含まれているため、行の4番目の文字を見つけるためにR(2)を調べたくありません。これはR(3)に含まれます。行の4番目の文字は、R(3)の2番目の文字でなければなりません。そこを見てみましょう。R(3)は次のように定義されます
R(3) = R(1) R(2)
R(1)には1文字が含まれているため、この行の2番目の文字はR(1)の最初の文字である必要があるため、ここで確認します。しかし、私たちはそれを知っています
R(2) = bc
したがって、この文字列の最初の文字はb
、です。これが私たちの答えです。これが正しいかどうか見てみましょう。フィボナッチ文字列の最初の7行は
1 a
2 bc
3 abc
4 bcabc
5 abcbcabc
6 bcabcabcbcabc
7 abcbcabcbcabcabcbcabc
案の定、7番目の文字列の7番目の文字を見ると、それが確かにであることがわかりますb
。これはうまくいくようです!
より正式には、関心のある漸化式は次のようになります。
char NthChar(int row, int index) {
if (row == 1) return 'a';
if (row == 2 && index == 1) return 'b';
if (row == 2 && index == 2) return 'c';
if (index < Fibonacci(row - 1)) return NthChar(row - 2, index);
return NthChar(row - 1, index - Fibonacci(row - 1));
}
もちろん、ここに書かれているように、実装には問題があります。行インデックスの範囲は最大109Fibonacci(row)
であるため、すべての場合に計算できるとは限りません。10億番目のフィボナッチ数は大きすぎて表現できません!
幸い、これを回避することができます。フィボナッチ数の表を見ると、F(45)= 1,134,903,170であり、これは10 9よりも大きいことがわかります(フィボナッチ数がこれよりも小さいことはありません)。さらに、関心のあるインデックスも10億を超えてはならないことがわかっているため、行46以上の場合は、常にフィボナッチ文字列の前半を見るブランチを使用します。これは、コードを次のように書き直すことができることを意味します
char NthChar(int row, int index) {
if (row == 1) return 'a';
if (row == 2 && index == 1) return 'b';
if (row == 2 && index == 2) return 'c';
/* Avoid integer overflow! */
if (row >= 46) return NthChar(row - 2, index);
if (index < Fibonacci(row - 1)) return NthChar(row - 2, index);
return NthChar(row - 1, index - Fibonacci(row - 1));
}
この時点で、私たちは解決策に非常に近づいています。対処すべき問題がまだいくつかあります。まず、上記のコードは、コンパイラが末尾再帰を使用してすべてのスタックフレームを削除するのに十分でない限り、ほぼ確実にスタックを吹き飛ばします。一部のコンパイラ(gccなど)はこれを検出できますが、これに依存するのはおそらく良い考えではないため、この再帰関数を繰り返し書き直す必要があります。考えられる実装の1つは次のとおりです。
char NthChar(int row, int index) {
while (true) {
if (row == 1) return 'a';
if (row == 2 && index == 1) return 'b';
if (row == 2 && index == 2) return 'c';
/* Avoid integer overflow! */
if (row >= 46 || index < Fibonacci(row - 1)) {
row -= 2;
} else {
index -= Fibonacci(row - 1);
row --;
}
}
}
しかしもちろん、これよりもはるかにうまくいくことができます。特に、驚くほど大きな行番号(たとえば、10億)が与えられた場合、46未満になるまで、行から2を引いて何度もループし続けるのは、本当にばかげています。すべての減算を行った後、最終的にどのような値になるかを決定するだけです。しかし、これは非常に簡単に行うことができます。少なくとも46の偶数行がある場合は、44になるまで2を減算することになります。少なくとも46の奇数行がある場合は、45になるまで2を減算することになります。上記のコードを書き直して、このケースを明示的に処理できます。
char NthChar(int row, int index) {
/* Preprocess the row to make it a small value. */
if (row >= 46) {
if (row % 2 == 0)
row = 45;
else
row = 44;
}
while (true) {
if (row == 1) return 'a';
if (row == 2 && index == 1) return 'b';
if (row == 2 && index == 2) return 'c';
if (index < Fibonacci(row - 1)) {
row -= 2;
} else {
index -= Fibonacci(row - 1);
row --;
}
}
}
最後に処理することが1つあります。それは、キャラクターが範囲外にあるために問題の解決策がない場合に発生することです。しかし、これは簡単に修正できます。
string NthChar(int row, int index) {
/* Preprocess the row to make it a small value. */
if (row >= 46) {
if (row % 2 == 0)
row = 45;
else
row = 44;
}
while (true) {
if (row == 1 && index == 1) return "a"
if (row == 2 && index == 1) return "b";
if (row == 2 && index == 2) return "c";
/* Bounds-checking. */
if (row == 1) return "no solution";
if (row == 2) return "no solution";
if (index < Fibonacci(row - 1)) {
row -= 2;
} else {
index -= Fibonacci(row - 1);
row --;
}
}
}
そして、実用的なソリューションがあります。
さらに最適化する方法の1つは、必要なすべてのフィボナッチ数を事前に計算し、それらを巨大な配列に格納することです。F(2)からF(44)までのフィボナッチ値のみが必要なので、次のようなことができます。
const int kFibonacciNumbers[45] = {
0, 1, 1, 2, 3, 5,
8, 13, 21, 34, 55, 89,
144, 233, 377, 610,
987, 1597, 2584, 4181,
6765, 10946, 17711, 28657,
46368, 75025, 121393, 196418,
317811, 514229, 832040,
1346269, 2178309, 3524578,
5702887, 9227465, 14930352,
24157817, 39088169, 63245986,
102334155, 165580141, 267914296,
433494437, 701408733
};
この事前計算された配列を使用すると、コードの最終バージョンは次のようになります。
string NthChar(int row, int index) {
/* Preprocess the row to make it a small value. */
if (row >= 46) {
if (row % 2 == 0)
row = 45;
else
row = 44;
}
while (true) {
if (row == 1 && index == 1) return "a"
if (row == 2 && index == 1) return "b";
if (row == 2 && index == 2) return "c";
/* Bounds-checking. */
if (row == 1) return "no solution";
if (row == 2) return "no solution";
if (index < kFibonacciNumbers[row - 1]) {
row -= 2;
} else {
index -= kFibonacciNumbers[row - 1];
row --;
}
}
}
私はまだこれをテストしていません。ドン・クヌースを言い換えると、私はそれが正しいことを証明しただけです。:-)しかし、これがあなたの質問に答えるのに役立つことを願っています。私はこの問題が本当に好きでした!