Вы можете решить эту проблему, не вычисляя явно ни одну из строк, и это, вероятно, лучший способ решения проблемы.В конце концов, если вас попросят вычислить 50-ю строку Фибоначчи, вам почти наверняка не хватит памяти;F (50) равно 12 586 269 025, поэтому вам понадобится более 12 ГБ памяти только для ее удержания!
Интуиция, лежащая в основе решения, заключается в том, что каждая строка строк Фибоначчи состоит из символов предыдущих строкВы можете преобразовать пару (строка, смещение) в другую пару (строка ', смещение'), где новая строка всегда для строки Фибоначчи меньшего размера, чем та, с которой вы начали.Если вы повторите это достаточно много раз, в конечном итоге вы вернетесь к строкам Фибоначчи либо для строки 0, либо для строки 1, и в этом случае ответ может быть немедленно считан.
Чтобы заставить этот алгоритм работать, мынужно установить несколько фактов.Во-первых, давайте определим ряд Фибоначчи с нулевой индексацией;то есть последовательность равна
F(0) = 0
F(1) = 1
F(n+2) = F(n) + F(n + 1)
. Учитывая это, мы знаем, что в n-й строке (с одним индексом) строк Фибоначчи содержится всего 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)
Используя это знание, предположим, что мы хотим найти седьмой символ седьмой строкиструны Фибоначчи.Мы знаем, что строка семь состоит из конкатенации строк пять и шесть, поэтому строка выглядит следующим образом:
R(7) = R(5) R(6)
Строка пять имеет F (5 + 1) = F (6) = 8 символов вэто, что означает, что первые восемь символов строки семь происходят из R (5).Поскольку нам нужен седьмой символ из этой строки, а поскольку 7 ≤ 8, мы знаем, что теперь нам нужно взглянуть на седьмой символ строки 5, чтобы получить это значение.Ну, строка 5 выглядит как объединение строк 3 и 4:
R(5) = R(3) R(4)
Мы хотим найти седьмой символ этой строки.Теперь R (3) содержит F (4) = 3 символа, что означает, что если мы ищем седьмой символ R (5), он будет в части R (4), а не в R (3) часть.Так как мы ищем седьмой символ этой строки, это означает, что мы ищем 7 - F (4) = 7 - 3 = 4-й символ R (4), так что теперь мы смотрим там.Опять же, R (4) определяется как
R(4) = R(2) R(3)
R (2) имеет F (3) = 2 символа в нем, поэтому мы не хотим искать в нем, чтобы найти четвертый символстрока;это будет содержаться в R (3).Четвертый символ строки должен быть вторым символом R (3).Давайте посмотрим там.R (3) определяется как
R(3) = R(1) R(2)
R (1) содержит один символ, поэтому второй символ этой строки должен быть первым символом R (1), поэтому мы смотрим там.Однако мы знаем, что
R(2) = bc
Таким образом, первый символ этой строки - b
, что является нашим ответом.Посмотрим, правильно ли это.Первые семь строк строк Фибоначчи:
1 a
2 bc
3 abc
4 bcabc
5 abcbcabc
6 bcabcabcbcabc
7 abcbcabcbcabcabcbcabc
Конечно, если вы посмотрите на седьмой символ седьмой строки, вы увидите, что это действительно 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));
}
Теперь, конечно, есть проблема с реализацией, как написано здесь.Поскольку индекс строки может варьироваться до 10 9 , мы не можем вычислить Fibonacci(row)
во всех случаях;Миллиардное число Фибоначчи слишком велико, чтобы его представлять!
К счастью, мы можем обойти это. Если вы посмотрите на таблицу чисел Фибоначчи, то обнаружите, что F (45) = 1 134 903 170, что больше 10 9 (и не меньшее число Фибоначчи больше этого). Более того, поскольку мы знаем, что индекс, который нас интересует, также не должен превышать одного миллиарда, если мы находимся в строке 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) могут обнаружить это, вероятно, не стоит полагаться на это, и поэтому нам, вероятно, следует переписать эту рекурсивную функцию итеративно. Вот одна из возможных реализаций:
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 --;
}
}
}
Но, конечно, мы можем добиться гораздо большего, чем это В частности, если вам дается потрясающе огромное число строк (скажем, один миллиард), глупо продолжать циклически вычитать два из ряда, пока оно не станет меньше 46. Это имеет гораздо больший смысл просто определите, какой ценностью это в конечном итоге станет после того, как мы выполним все вычитания. Но мы можем сделать это довольно легко. Если у нас есть четная строка, по крайней мере, 46, мы в конечном итоге вычтем 2, пока она не станет 44. Если у нас будет нечетная строка, которая по крайней мере 46, мы в итоге вычтем 2, пока она не станет 45. Следовательно мы можем переписать приведенный выше код, чтобы явно обработать этот случай:
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 --;
}
}
}
Есть еще одна вещь, которую нужно решить, это то, что происходит, если нет решения проблемы, потому что персонаж находится вне диапазона. Но мы можем легко это исправить:
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 --;
}
}
}
И у нас есть рабочее решение.
Еще одна оптимизация, которую вы можете выполнить, - это предварительный расчет всех необходимых вам чисел Фибоначчи и их сохранение в гигантском массиве. Вам нужны только значения Фибоначчи от 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 --;
}
}
}
Я еще не проверял это; Перефразируя Дона Кнута, я просто доказал, что это правильно. :-) Но я надеюсь, что это поможет ответить на ваш вопрос. Мне очень понравилась эта проблема!