Подтвердить что ты не робот

Быстрое создание Фибоначчи

Мне потребовалось написать простую реализацию алгоритма Фибоначчи, а затем сделать его быстрее.

Вот моя первоначальная реализация

public class Fibonacci {

    public static long getFibonacciOf(long n) {
        if (n== 0) {
            return 0;
        } else if (n == 1) {
            return 1;
        } else {
            return getFibonacciOf(n-2) + getFibonacciOf(n-1);
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner (System.in);
        while (true) {
            System.out.println("Enter n :");
            long n = scanner.nextLong();
            if (n >= 0) {
                long beginTime = System.currentTimeMillis();
                long fibo = getFibonacciOf(n);
                long endTime = System.currentTimeMillis();

                long delta = endTime - beginTime;

                System.out.println("F(" + n + ") = " + fibo + " ... computed     in " + delta + " milliseconds");
            } else {
                break;

            }
        }

    }

}

Как вы можете видеть, я использую System.currentTimeMillis(), чтобы получить простую меру времени, прошедшего при вычислении Фибоначчи.

Эта реализация быстро становится экспоненциально медленной, как вы можете видеть на следующем рисунке

simple version of fibonacci's algorithm

Итак У меня есть простая идея оптимизации. Чтобы поместить предыдущие значения в HashMap и вместо этого переучитывать их каждый раз, просто вернуть их из HashMap, если они существуют. Если они не существуют, мы помещаем их в HashMap.

Вот новая версия кода

public class FasterFibonacci {

    private static Map<Long, Long> previousValuesHolder;
    static {
        previousValuesHolder = new HashMap<Long, Long>();
        previousValuesHolder.put(Long.valueOf(0), Long.valueOf(0));
        previousValuesHolder.put(Long.valueOf(1), Long.valueOf(1));
    }
    public static long getFibonacciOf(long n) {
        if (n== 0) {

            return 0;
        } else if (n == 1) {
            return 1;
        } else {
            if (previousValuesHolder.containsKey(Long.valueOf(n))) {
                return previousValuesHolder.get(n);
            } {

                long newValue = getFibonacciOf(n-2) + getFibonacciOf(n-1);
                previousValuesHolder.put(Long.valueOf(n),     Long.valueOf(newValue));
                return newValue;
            }

        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner (System.in);
        while (true) {
            System.out.println("Enter n :");
            long n = scanner.nextLong();
            if (n >= 0) {
                long beginTime = System.currentTimeMillis();
                long fibo = getFibonacciOf(n);
                long endTime = System.currentTimeMillis();

                long delta = endTime - beginTime;

                System.out.println("F(" + n + ") = " + fibo + " ... computed     in " + delta + " milliseconds");
            } else {
                break;

            }
        }

    }

Это изменение делает вычисления чрезвычайно быстрыми. Я вычисляю все значения от 2 до 103 в мгновение ока и получаю переполнение long в F (104) (Дает мне F (104) = -7076989329685730859, что неверно). Я нахожу это настолько быстрым, что ** Интересно, есть ли какие-либо ошибки в моем коде (спасибо за проверку и дайте мне знать, пожалуйста) **. Посмотрите на вторую картинку:

Faster Fibonacci

Правильно ли реализован алгоритм моего алгоритма фибоначчи (я думаю, что он для меня, потому что он получает те же значения, что и в первой версии, но поскольку первая версия была слишком медленной, я не мог вычислить с ней большие значения, такие как F (75) )? Каким другим способом я могу использовать его быстрее? Или есть лучший способ сделать это быстрее? Также как я могу вычислить Fibonacci для больших значений (например, 150, 200) без переполнения ** long **? Хотя это кажется быстрым, я хотел бы подтолкнуть его к пределам. Я помню, как мистер Абраш сказал: " Лучший оптимизатор находится между вашими двумя ушами", поэтому я считаю, что он все равно может быть улучшен. Спасибо, что помогли

[Замечание об издании:] Хотя вопрос этот затрагивает один из основных моментов моего вопроса, вы можете видеть сверху, что у меня есть дополнительные вопросы.

4b9b3361

Ответ 1

Динамическое программирование

Идея: вместо того, чтобы пересчитывать одно и то же значение несколько раз, вы просто сохраняете рассчитанное значение и используете их по ходу.

f(n)=f(n-1)+f(n-2) с f (0) = 0, f (1) = 1. Таким образом, в момент, когда вы рассчитали f (n-1), вы можете легко вычислить f (n), если вы сохраните значения f (n) и f (n-1).

Сначала возьмем массив Bignums. А [1..200]. Инициализируйте их до -1.

псевдокод

fact(n)
{
if(A[n]!=-1) return A[n];
A[0]=0;
A[1]=1;
for i=2 to n
  A[i]= addition of A[i],A[i-1];
return A[n]
}

Это выполняется в O (n) времени. Проверьте это самостоятельно.

Этот метод также называется memoization.

IDEA

Динамическое программирование (обычно называемое DP) - очень мощный метод решения определенного класса проблем. Это требует очень элегантной формулировки подхода и простого мышления, а кодирующая часть очень проста. Идея очень проста: если вы решили проблему с данным вводом, сохраните результат для будущей ссылки, чтобы избежать повторения той же проблемы снова. Вскоре "Помните свое прошлое".

Если данную проблему можно разбить на более мелкие подзадачи, и эти меньшие подзадачи, в свою очередь, делятся на еще более мелкие, и в этом процессе, если вы наблюдаете некоторый over-lappping subproblems, то это большой намек для DP. Кроме того, оптимальные решения подзадач способствуют оптимальному решению данной задачи (называемому Optimal Substructure Property).

Есть два способа сделать это.

1.) Сверху вниз: начните решать данную проблему, разбив ее. Если вы видите, что проблема уже решена, просто верните сохраненный ответ. Если он не был решен, решите его и сохраните ответ. Это, как правило, легко думать и очень интуитивно понятно. Это называется Memoization. (Я использовал эту идею).

2.) Bottom-Up: проанализируйте проблему и посмотрите порядок решения подзадач и начните решать из тривиальной подзадачи, вплоть до данной проблемы. В этом процессе гарантируется, что подзадачи решаются до решения проблемы. Это называется Динамическое программирование. (MinecraftShamrock использовал эту идею)


Там больше!

(Другие способы сделать это)

Посмотрите, что наш поиск, чтобы получить лучшее решение, не заканчивается здесь. Вы увидите другой подход -

Если вы знаете, как решить recurrence relation, то вы найдете решение этого отношения

f(n)=f(n-1)+f(n-2) given f(0)=0,f(1)=1

Вы получите формулу после ее решения -

f(n)= (1/sqrt(5))((1+sqrt(5))/2)^n - (1/sqrt(5))((1-sqrt(5))/2)^n

которая может быть записана в более компактной форме

f(n)=floor((((1+sqrt(5))/2)^n) /sqrt(5) + 1/2)

Сложность

Вы можете получить количество энергии в операциях O (logn). Вы должны изучить Экспоненциальность путем возведения в квадрат.

EDIT. Хорошо отметить, что это не обязательно означает, что число фибоначчи можно найти в O (logn). Фактически количество цифр, которые нам нужно вычислить, линейно растет. Вероятно, из-за позиции, в которой я заявлял, что, кажется, претендует на неверную идею о том, что факториал числа можно вычислить в O (logn) времени. [Бакуруи, MinecraftShamrock прокомментировал это]

Ответ 2

Если вам нужно очень часто вычислять n-е число фибоначчи, я предлагаю использовать амальдомный ответ.

Но если вы хотите вычислить очень большое число фибоначчи, у вас не хватит памяти, потому что вы храните все меньшие числа фибоначчи. Следующий псевдокод сохраняет только последние два числа фибоначчи в памяти, т.е. Требует гораздо меньше памяти:

fibonacci(n) {
    if n = 0: return 0;
    if n = 1: return 1;
    a = 0;
    b = 1;
    for i from 2 to n: {
        sum = a + b;
        a = b;
        b = sum;
    }
    return b;
}

Анализ
Это может вычислять очень высокие числа фибоначчи с довольно низким потреблением памяти: мы имеем O (n) время, когда цикл повторяется n-1 раз. Интересна и пространственная сложность: n-е число фибоначчи имеет длину O (n), что легко показать: F n <= 2 * F n-1
Это означает, что число n-й фибоначчи не превышает вдвое больше, чем его предшественник. Удвоение числа в двоичном эквиваленте эквивалентно одному сдвигу влево, что увеличивает количество необходимых бит на единицу. Таким образом, представление n-го числа фибоначчи занимает не более O (n) пространства. В памяти имеется не более трех последовательных чисел фибоначчи, что делает O (n) + O (n-1) + O (n-2) = O (n) общее потребление пространства. В отличие от этого алгоритм memoization всегда сохраняет первые n чисел фибоначчи в памяти, что делает O (n) + O (n-1) + O (n-2) +... + O (1) = O (n ^ 2).

Итак, какой способ использовать?
Единственная причина держать все нижние числа фибоначчи в памяти, если вам нужны номера фибоначчи очень часто. Речь идет о балансировании времени с потреблением памяти.

Ответ 3

Отойдите от рекурсии Фибоначчи и используйте идентификаторы

(F(2n), F(2n-1)) = (F(n)^2 + 2 F(n) F(n-1), F(n)^2+F(n-1)^2)
(F(2n+1), F(2n)) = (F(n+1)^2+F(n)^2, 2 F(n+1) F(n) - F(n)^2)

Это позволяет вам вычислить (F (m + 1), F (m)) в терминах (F (k + 1), F (k)) для k, равного половине m. Написанное итеративно с некоторым сдвигом бит для деления на 2, это должно дать вам теоретическую скорость экспоненциальности O (log n) путем возведения в квадрат, оставаясь полностью в целочисленной арифметике. (Ну, арифметические операции O (log n). Поскольку вы будете работать с числами с примерно n битами, это будет не время O (log n), когда вы будете вынуждены переключиться на большую целочисленную библиотеку. После F (50), вы переполните целочисленный тип данных, который будет только до 2 ^ (31).)

(Извинения за то, что я не помню Java достаточно хорошо, чтобы реализовать это на Java, любой, кто хочет, может свободно редактировать его.)

Ответ 4

  • Фибоначчи (0) = 0
  • Фибоначчи (1) = 1
  • Фибоначчи (n) = Фибоначчи (n - 1) + Фибоначчи (n - 2), когда n >= 2

Обычно есть два способа вычисления числа Фибоначчи:

  • Рекурсия

    public long getFibonacci(long n) {
      if(n <= 1) {
        return n;
      } else {
        return getFibonacci(n - 1) + getFibonacci(n - 2);
      }
    }
    

    Этот способ интуитивно понятен и понятен, а поскольку он не использует повторно рассчитанное число Фибоначчи, сложность времени составляет O(2^n), но он не хранит вычисленный результат, поэтому он экономит место много, на самом деле сложность пространства O(1).

  • Динамическое программирование:

    public long getFibonacci(long n) {
      long[] f = new long[(int)(n + 1)];
      f[0] = 0;
      f[1] = 1;
      for(int i=2;i<=n;i++) {
        f[i] = f[i - 1] + f[i - 2];
      }
      return f[(int)n];
    }
    

    Этот Memoization способ расчета чисел Фибоначчи и повторное использование их при вычислении следующего. Сложность времени довольно хороша, это O(n), а сложность пространства - O(n). Позвольте исследовать, можно ли оптимизировать пространственную сложность... Поскольку f(i) требуется только f(i - 1) и f(i - 2), нет необходимости хранить все рассчитанные числа Фибоначчи.

    Более эффективная реализация:

    public long getFibonacci(long n) {
      if(n <= 1) {
        return n;
      }
      long x = 0, y = 1;
      long ans;
      for(int i=2;i<=n;i++) {
        ans = x + y;
        x = y;
        y = ans;
      }
      return ans;
    }
    

    С временной сложностью O(n) и сложностью пространства O(1).

Добавлено: Так как число Фибоначчи увеличивается очень быстро, long может обрабатывать менее 100 чисел Фибоначчи. В Java мы можем использовать BigInteger для хранения большего числа чисел Фибоначчи.

Ответ 5

Предварительно скопируйте большое количество результатов fib(n) и сохраните их как таблицу поиска внутри вашего алгоритма. Бам, свободная "скорость"

Теперь, если вам нужно вычислить fib(101), и у вас уже есть фиксы от 0 до 100, это похоже на попытку вычислить fib(1).

Скорее всего, это не то, что ищет эта домашняя работа, но это вполне законная стратегия и в основном идея кеширования, извлеченная дальше от запуска алгоритма. Если вы знаете, что вы, вероятно, часто будете вычислять первые 100 фибов, и вам нужно сделать это действительно очень быстро, там ничего более быстрого, чем O (1). Так что вычислите эти значения полностью вне диапазона и сохраните их, чтобы их можно было просмотреть позже.

Конечно, значения кеша при их вычислении тоже:) Дублированные вычисления являются отходами.

Ответ 6

Здесь вырезано код с итеративным подходом вместо рекурсии.

Пример вывода:

Enter n: 5
F(5) = 5 ... computed in 1 milliseconds
Enter n: 50
F(50) = 12586269025 ... computed in 0 milliseconds
Enter n: 500
F(500) = ...4125 ... computed in 2 milliseconds
Enter n: 500
F(500) = ...4125 ... computed in 0 milliseconds
Enter n: 500000
F(500000) = ...453125 ... computed in 5,718 milliseconds
Enter n: 500000
F(500000) = ...453125 ... computed in 0 milliseconds

Некоторые фрагменты результатов опущены с ... для лучшего просмотра.

Фрагмент кода:

public class CachedFibonacci {
    private static Map<BigDecimal, BigDecimal> previousValuesHolder;
    static {
        previousValuesHolder = new HashMap<>();
        previousValuesHolder.put(BigDecimal.ZERO, BigDecimal.ZERO);
        previousValuesHolder.put(BigDecimal.ONE, BigDecimal.ONE);
    }

    public static BigDecimal getFibonacciOf(long number) {
        if (0 == number) {
            return BigDecimal.ZERO;
        } else if (1 == number) {
            return BigDecimal.ONE;
        } else {
            if (previousValuesHolder.containsKey(BigDecimal.valueOf(number))) {
                return previousValuesHolder.get(BigDecimal.valueOf(number));
            } else {
                BigDecimal olderValue = BigDecimal.ONE,
                        oldValue = BigDecimal.ONE,
                        newValue = BigDecimal.ONE;

                for (int i = 3; i <= number; i++) {
                    newValue = oldValue.add(olderValue);
                    olderValue = oldValue;
                    oldValue = newValue;
                }
                previousValuesHolder.put(BigDecimal.valueOf(number), newValue);
                return newValue;
            }
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("Enter n: ");
            long inputNumber = scanner.nextLong();
            if (inputNumber >= 0) {
                long beginTime = System.currentTimeMillis();
                BigDecimal fibo = getFibonacciOf(inputNumber);
                long endTime = System.currentTimeMillis();
                long delta = endTime - beginTime;

                System.out.printf("F(%d) = %.0f ... computed in %,d milliseconds\n", inputNumber, fibo, delta);
            } else {
                System.err.println("You must enter number > 0");
                System.out.println("try, enter number again, please:");
                break;
            }
        }
    }
}

Этот подход работает намного быстрее, чем рекурсивная версия.

В такой ситуации итеративное решение имеет тенденцию быть немного быстрее, поскольку каждый рекурсивный вызов метода занимает определенное количество процессорного времени. В принципе, это возможно, для умного компилятора, чтобы избежать рекурсивных вызовов методов, если они следуют простым шаблонов, но большинство компиляторов этого не делают. С этой точки зрения, итеративный решение предпочтительнее.

Ответ 7

Здесь способ доказуемо сделать это в O (log n) (поскольку цикл работает log n раз):

/* 
 * Fast doubling method
 * F(2n) = F(n) * (2*F(n+1) - F(n)).
 * F(2n+1) = F(n+1)^2 + F(n)^2.
 * Adapted from:
 *    https://www.nayuki.io/page/fast-fibonacci-algorithms
 */
private static long getFibonacci(int n) {
    long a = 0;
    long b = 1;
    for (int i = 31 - Integer.numberOfLeadingZeros(n); i >= 0; i--) {
        long d = a * ((b<<1) - a);
        long e = (a*a) + (b*b);
        a = d;
        b = e;
        if (((n >>> i) & 1) != 0) {
            long c = a+b;
            a = b;
            b = c;
        }
    }
    return a;
}

Я предполагаю здесь (как обычно), что операция multiply/add/whatever является постоянным временем независимо от количества бит, то есть будет использоваться тип данных фиксированной длины.

Эта страница объясняет несколько способов, которые являются самыми быстрыми. Я просто перевел его на использование BigInteger для удобства чтения. Здесь версия BigInteger:

/* 
 * Fast doubling method.
 * F(2n) = F(n) * (2*F(n+1) - F(n)).
 * F(2n+1) = F(n+1)^2 + F(n)^2.
 * Adapted from:
 *    http://www.nayuki.io/page/fast-fibonacci-algorithms
 */
private static BigInteger getFibonacci(int n) {
    BigInteger a = BigInteger.ZERO;
    BigInteger b = BigInteger.ONE;
    for (int i = 31 - Integer.numberOfLeadingZeros(n); i >= 0; i--) {
        BigInteger d = a.multiply(b.shiftLeft(1).subtract(a));
        BigInteger e = a.multiply(a).add(b.multiply(b));
        a = d;
        b = e;
        if (((n >>> i) & 1) != 0) {
            BigInteger c = a.add(b);
            a = b;
            b = c;
        }
    }
    return a;
}

Ответ 8

Следуя аналогичному подходу некоторое время назад, я только что понял, что вы можете сделать еще одну оптимизацию.

Если вы знаете два больших последовательных ответа, вы можете использовать это как отправную точку. Например, если вы знаете F (100) и F (101), то вычисление F (104) примерно так же сложно (*), как вычисление F (4) на основе F (0) и F (1).

Вычисление итеративно вверх так же эффективно, как и при использовании кэшированной рекурсии, но использует меньше памяти.

Сделав некоторые суммы, я также понял, что для любого заданного z < n:

F (n) = F (z) * F (n-z) + F (z-1) * F (n-z-1)

Если n нечетно, и вы выбираете z=(n+1)/2, то это сводится к

Р (п) = Р (г) ^ 2 + Р (г-1) ^ 2

Мне кажется, что вы должны использовать это методом, который мне еще предстоит найти, чтобы вы могли использовать приведенную выше информацию, чтобы найти F (n) в количестве операций, равных:

количество бит в n удвоениях (как указано выше) + количество бит 1 в n сложениях; в случае 104 это будет (7 бит, 3 '1' бит) = 14 умножений (squarings), 10 дополнений.

(*), предполагая, что добавление двух чисел занимает одно и то же время, не имеет значения размера двух чисел.