Что такое динамическое программирование?

Что такое динамическое программирование?

Чем он отличается от рекурсии, запоминания и т.д.?

Я прочитал статью в Википедии, но до сих пор не понимаю этого.

Ответ 1

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

Хорошим примером является решение последовательности Фибоначчи для n = 1000,002.

Это будет очень долгий процесс, но что, если я дам вам результаты для n = 1,000,000 и n = 1,000,001? Внезапно проблема стала более управляемой.

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

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

В книге Cormen Algorithms есть замечательная глава о динамическом программировании. И это бесплатно в Google Книгах! Проверьте здесь.

Ответ 2

Динамическое программирование - это метод, используемый для избежания многократного вычисления одной и той же подзадачи в рекурсивном алгоритме.

Возьмем простой пример чисел фибоначчи: найдем число фибоначчи n th определенное

F n= F n-1 + F n-2 и F 0= 0, F 1= 1

Рекурсия

Очевидный способ сделать это рекурсивным:

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1

    return fibonacci(n - 1) + fibonacci(n - 2)

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

  • Наверху - Memoization

Рекурсия делает много ненужных вычислений, потому что данное число фибоначчи будет вычисляться несколько раз. Легкий способ улучшить это - кешировать результаты:

cache = {}

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    if n in cache:
        return cache[n]

    cache[n] = fibonacci(n - 1) + fibonacci(n - 2)

    return cache[n]
  • Bottom-Up

Лучший способ сделать это - избавиться от рекурсии все вместе, оценив результаты в правильном порядке:

cache = {}

def fibonacci(n):
    cache[0] = 0
    cache[1] = 1

    for i in range(2, n + 1):
        cache[i] = cache[i - 1] +  cache[i - 2]

    return cache[n]

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

def fibonacci(n):
  fi_minus_2 = 0
  fi_minus_1 = 1

  for i in range(2, n + 1):
      fi = fi_minus_1 + fi_minus_2
      fi_minus_1, fi_minus_2 = fi, fi_minus_1

  return fi
  • Как применить динамическое программирование?

    • Найдите рекурсию в проблеме.
    • Сверху вниз: сохраняйте ответ для каждой подзадачи в таблице, чтобы не перепрограммировать их.
    • Bottom-up: найдите правильный порядок для оценки результатов, чтобы при необходимости были доступны частичные результаты.

Динамическое программирование обычно работает для проблем, которые имеют неотъемлемый правый и правый порядок, такие как строки, деревья или целые последовательности. Если наивный рекурсивный алгоритм не вычисляет одну и ту же подзадачу многократно, динамическое программирование не поможет.

Я создал набор проблем, чтобы помочь понять логику: https://github.com/tristanguigue/dynamic-programing

Ответ 3

Вот мой ответ в аналогичной теме

Начать с

Если вы хотите проверить себя, мой выбор в отношении онлайн-судей

и конечно

Вы также можете проверить хорошие курсы университетов алгоритмы

В конце концов, если вы не можете решить проблемы, спросите ТАК, что существует множество алгоритмов наркомана

Ответ 4

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

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

Динамическое программирование - это процесс решения более простых для решения подзадач и формирования ответа на него. Большинство алгоритмов DP будут находиться в промежутке между алгоритмом Greedy (если таковой существует) и экспоненциальным (перечислять все возможности и находить лучший).

  • Алгоритмы DP могут быть реализованы с рекурсией, но они не обязательно должны быть.
  • Алгоритмы DP не могут быть ускорены с помощью memoization, так как каждая подзадача только когда-либо решается (или называется "решающей" функцией).

Ответ 5

Это оптимизация вашего алгоритма, который сокращает время выполнения.

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

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

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

Я собираюсь сделать краткий брифинг о важной части этого анализа проблем, взятой из книги "Проблемы программирования", я предлагаю вам проверить это.

Внимательно посмотрите на эту проблему, если мы определим функцию стоимости, сообщающую нам, насколько далеко расположены две строки, у нас есть две, учитывающие три естественных типа изменений:

Подстановка - замените один символ из шаблона "s" на другой символ в тексте "t", например, изменив "выстрел" на "точечный".

Вставка - вставьте один символ в шаблон "s", чтобы он соответствовал тексту "t", например, изменив "назад" на "agog".

Удаление - удалите один символ из шаблона "s", чтобы он соответствовал тексту "t", например, изменив "час" на "наш".

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

Мы можем определить рекурсивный алгоритм, используя наблюдение, что последний символ в строке должен совпадать, заменяться, вставляться или удаляться. Отрезание символов в последней операции редактирования оставляет парную операцию, оставляет пару меньших строк. Пусть i и j будут последним символом соответствующего префикса и t соответственно. после последней операции есть три пары более коротких строк, соответствующих строке после сопоставления/замены, вставки или удаления. Если бы мы знали стоимость редактирования трех пар более мелких строк, мы могли бы решить, какой вариант приведет к наилучшему решению, и выбрать этот вариант соответствующим образом. Мы можем узнать эту стоимость с помощью потрясающей рекурсии:

#define MATCH 0 /* enumerated type symbol for match */
#define INSERT 1 /* enumerated type symbol for insert */
#define DELETE 2 /* enumerated type symbol for delete */


int string_compare(char *s, char *t, int i, int j)

{

    int k; /* counter */
    int opt[3]; /* cost of the three options */
    int lowest_cost; /* lowest cost */
    if (i == 0) return(j * indel( ));
    if (j == 0) return(i * indel( ));
    opt[MATCH] = string_compare(s,t,i-1,j-1) +
      match(s[i],t[j]);
    opt[INSERT] = string_compare(s,t,i,j-1) +
      indel(t[j]);
    opt[DELETE] = string_compare(s,t,i-1,j) +
      indel(s[i]);
    lowest_cost = opt[MATCH];
    for (k=INSERT; k<=DELETE; k++)
    if (opt[k] < lowest_cost) lowest_cost = opt[k];
    return( lowest_cost );

}

Этот алгоритм правильный, но он также невероятно медленный.

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

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

Итак, как мы можем сделать алгоритм практичным? Важным наблюдением является то, что большинство этих рекурсивных вызовов являются вычислительными вещами, которые уже были вычислены ранее. Как мы узнаем? Ну, может быть только | s | · | Т | возможные уникальные рекурсивные вызовы, поскольку существует только столько различных (i, j) пар, которые служат параметрами рекурсивных вызовов.

Сохраняя значения для каждой из этих (i, j) пар в таблице, мы можем избежать пересчета их и просто посмотреть их по мере необходимости.

Таблица представляет собой двумерную матрицу m, где каждый из | s | · | t | В ячейках указана стоимость оптимального решения этой подзадачи, а также указатель родителя, объясняющий, как мы попали в это место:

typedef struct {
int cost; /* cost of reaching this cell */
int parent; /* parent cell */
} cell;

cell m[MAXLEN+1][MAXLEN+1]; /* dynamic programming table */

Динамическая версия программирования имеет три отличия от рекурсивной версии.

Во-первых, он получает свои промежуточные значения, используя поиск в таблице вместо рекурсивных вызовов.

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

** В-третьих, ** В-третьих, в ней используется более общая целевая функция cell() вместо простого возврата m [| s |] [| t |].cost. Это позволит нам применить эту процедуру к более широкому классу проблем.

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

Здесь альтернативное, полное решение той же проблемы. Это также "динамический", хотя его исполнение отличается. Я предлагаю вам проверить, насколько эффективно решение, отправив его в онлайн-судью UVA. Я нахожу удивительным, как такая тяжелая проблема была решена так эффективно.

Ответ 6

Ключевыми битами динамического программирования являются "перекрывающиеся подзадачи" и "оптимальная субструктура". Эти свойства задачи означают, что оптимальное решение состоит из оптимальных решений его подзадач. Например, проблемы с короткими траекториями демонстрируют оптимальную субструктуру. Самый короткий путь от A до C - это кратчайший путь от A до некоторого node B, за которым следует кратчайший путь от node B до C.

Более подробно для решения проблемы с кратчайшим путем вы будете:

  • найдите расстояния от начального node до каждого node, касающегося его (скажем от A до B и C)
  • найдите расстояния от этих узлов до тех, кто касается их (от B до D и E, а от C до E и F)
  • теперь мы знаем кратчайший путь от A до E: это кратчайшая сумма A-x и x-E для некоторого node x, который мы посетили (либо B, либо C)
  • повторите этот процесс, пока мы не достигнем конечного пункта назначения node

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

Помните, что проблемы динамического программирования должны иметь как перекрывающиеся подзадачи, так и оптимальную субструктуру. Создание последовательности Фибоначчи не является проблемой динамического программирования; он использует memoization, потому что он имеет перекрывающиеся подзадачи, но у него нет оптимальной подструктуры (потому что нет проблемы оптимизации).

Ответ 7

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

Определение

Динамическое программирование (DP) - это общий метод разработки алгоритма для решения проблемы с перекрывающимися подзадачами. Этот метод был изобретен американским математик "Ричард Беллман" в 1950-х годах.

Основная идея

Основная идея состоит в том, чтобы сохранить ответы на перекрывающиеся меньшие под-проблемы, чтобы избежать перерасчета.

Свойства динамического программирования

  • Экземпляр решается с помощью решений для меньших экземпляров.
  • Решения для меньшего экземпляра могут потребоваться несколько раз, поэтому сохраните их результаты в таблице.
  • Таким образом, каждый меньший экземпляр решается только один раз.
  • Дополнительное пространство используется для экономии времени.

Ответ 8

Вот один учебник Майкла А. Трика из CMU, который я нашел особенно полезным:

http://mat.gsia.cmu.edu/classes/dynamic/dynamic.html

Это, безусловно, в дополнение ко всем ресурсам, рекомендованным другими (все остальные ресурсы, особенно CLR и Kleinberg, Tardos очень хороши!).

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

Также проверьте страницу Стивена Скиена и лекции по динамическому программированию: http://www.cs.sunysb.edu/~algorith/video-lectures/

http://www.cs.sunysb.edu/~algorith/video-lectures/1997/lecture12.pdf

Ответ 9

Я также очень много нового для динамического программирования (мощный алгоритм для конкретного типа проблем)

В самых простых словах просто подумайте о динамическом программировании как рекурсивном подходе с использованием предыдущих знаний

Предыдущие знания - это то, что важно здесь больше всего. Следите за решением проблем, которые у вас уже есть.

Рассмотрим это, самый простой пример для dp из Википедии

Поиск последовательности фибоначчи

function fib(n)   // naive implementation
    if n <=1 return n
    return fib(n − 1) + fib(n − 2)

Позволяет разбить вызов функции с помощью n = 5

fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

В частности, fib (2) вычислялся три раза с нуля. В более крупных примерах многие другие значения fib или под-проблем пересчитываются, что приводит к экспоненциальному алгоритму времени.

Теперь попробуйте, сохранив значение, которое мы уже обнаружили в структуре данных, скажем Карта

var m := map(0 → 0, 1 → 1)
function fib(n)
    if key n is not in map m 
        m[n] := fib(n − 1) + fib(n − 2)
    return m[n]

Здесь мы сохраняем решение подзадач на карте, если у нас ее уже нет. Этот метод сохранения значений, которые мы уже вычислили, называется Memoization.

Наконец, для проблемы сначала попробуйте найти состояния (возможные подзадачи и попытайтесь придумать лучший рекурсивный подход, чтобы вы могли использовать решение предыдущей подзадачи в дальнейшем).

Ответ 10

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

Семь шагов в разработке алгоритма динамического программирования следующие:

  1. Установите рекурсивное свойство, которое дает решение экземпляра проблемы.
  2. Разработать рекурсивный алгоритм согласно рекурсивному свойству
  3. Посмотрите, решается ли тот же самый экземпляр проблемы снова и снова в рекурсивных вызовах
  4. Разработайте запомненный рекурсивный алгоритм
  5. Смотрите шаблон в хранении данных в памяти
  6. Преобразовать запомненный рекурсивный алгоритм в итерационный алгоритм
  7. Оптимизируйте итерационный алгоритм, используя хранилище по мере необходимости (оптимизация хранилища)

Ответ 11

Короче говоря, разница между рекурсивной memoization и динамическим программированием

Динамическое программирование в качестве названия предполагает использование предыдущего вычисленного значения для динамического построения следующего нового решения

Где применять динамическое программирование: если решение основано на оптимальной субструктуре и перекрывающейся подпроцессе, тогда в этом случае использование более раннего вычисленного значения будет полезно, поэтому вам не придется его компрометировать. Это подход снизу. Предположим, вам нужно вычислить fib (n), в этом случае все, что вам нужно сделать, это добавить предыдущее вычисленное значение fib (n-1) и fib (n-2)

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

Запоминание: в основном сохранение старого рассчитанного значения рекурсии в таблице известно как memoization, что позволит избежать повторного вычисления, если он уже был рассчитан каким-то предыдущим вызовом, поэтому любое значение будет вычисляться один раз. Поэтому перед вычислением мы проверяем, было ли это значение уже вычислено или нет, если оно уже вычислено, тогда мы возвращаем то же самое из таблицы, а не для пересчета. Это также подход сверху вниз

Ответ 12

Вот простой пример кода Python подхода Recursive, Top-down, Bottom-up для рядов Фибоначчи:

Рекурсивно: O (2 n)

def fib_recursive(n):
    if n == 1 or n == 2:
        return 1
    else:
        return fib_recursive(n-1) + fib_recursive(n-2)


print(fib_recursive(40))

Сверху вниз: O (n) Эффективно для большего ввода

def fib_memoize_or_top_down(n, mem):
    if mem[n] is not 0:
        return mem[n]
    else:
        mem[n] = fib_memoize_or_top_down(n-1, mem) + fib_memoize_or_top_down(n-2, mem)
        return mem[n]


n = 40
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
print(fib_memoize_or_top_down(n, mem))

Вверху: O (n) Для простоты и небольших входных размеров

def fib_bottom_up(n):
    mem = [0] * (n+1)
    mem[1] = 1
    mem[2] = 1
    if n == 1 or n == 2:
        return 1

    for i in range(3, n+1):
        mem[i] = mem[i-1] + mem[i-2]

    return mem[n]


print(fib_bottom_up(40))