Чим відрізняється знизу вгору від верху вниз?


177

Підхід знизу вгору (до динамічного програмування) полягає в тому, щоб спочатку переглянути "менші" підпрограми, а потім вирішити більші підпрограми, використовуючи рішення для менших проблем.

Зверху вниз полягає у вирішенні проблеми «природним чином» і перевірити , якщо ви розрахували рішення підзадачі раніше.

Я трохи розгублений. Яка різниця між цими двома?


Відповіді:


247

rev4: Дуже красномовний коментар користувача Sammaron зазначив, що, можливо, ця відповідь раніше плутала зверху вниз і знизу вгору. Хоча спочатку ця відповідь (rev3) та інші відповіді говорили, що "знизу вгору - це запам'ятовування" ("припустимо, підпроблеми"), це може бути зворотним (тобто "зверху вниз" може бути "припустити підпрограми" і " знизу вгору "може бути" скласти підпроблеми "). Раніше я читав, як запам'ятовування є різним видом динамічного програмування на відміну від підтипу динамічного програмування. Я цитував цю точку зору, незважаючи на те, що не підписався на неї. Я переписав цю відповідь як агностик термінології, поки в літературі не знайдуться відповідні посилання. Я також перетворив цю відповідь у вікі спільноти. Будь ласка, віддайте перевагу академічним джерелам. Список посилань:} {Література: 5 }

Резюме

Динамічне програмування - це впорядкування обчислень таким чином, щоб уникнути перерахунку повторюваних робіт. У вас є основна проблема (корінь вашого дерева підпроблем) та підпроблеми (subtrees). Підпрограми, як правило, повторюються та перетинаються .

Наприклад, розгляньте свій улюблений приклад Fibonnaci. Це повне дерево підпроблем, якби ми виконували наївний рекурсивний виклик:

TOP of the tree
fib(4)
 fib(3)...................... + fib(2)
  fib(2)......... + fib(1)       fib(1)........... + fib(0)
   fib(1) + fib(0)   fib(1)       fib(1)              fib(0)
    fib(1)   fib(0)
BOTTOM of the tree

(У деяких інших рідкісних проблемах це дерево може бути нескінченним у деяких гілках, що представляє собою нерозрив час. Таким чином, вам може знадобитися стратегія / алгоритм, щоб вирішити, які підпрограми розкривати.)


Пам'ятка, табуляція

Існують щонайменше дві основні методи динамічного програмування, які не є взаємовиключними:

  • Пам'ятка - Це підхід, який випускаєте з лайсез-файру: ви припускаєте, що ви вже обчислили всі підпрограми і не знаєте, який оптимальний порядок оцінювання. Як правило, ви виконували б рекурсивний дзвінок (або якийсь ітеративний еквівалент) з кореня, або сподіваєтесь, що ви наблизитесь до оптимального порядку оцінки, або отримаєте доказ того, що допоможете досягти оптимального порядку оцінки. Ви б переконалися, що рекурсивний виклик ніколи не перераховує підпроблему, оскільки ви кешуєте результати, і, отже, дублікати під дерев не будуть перераховані.

    • Приклад: Якщо ви обчислюєте послідовність Фібоначчі fib(100), ви просто зателефонували б цьому, і він би зателефонував fib(100)=fib(99)+fib(98), який би зателефонував fib(99)=fib(98)+fib(97), ... і т.д. ..., який закликав би fib(2)=fib(1)+fib(0)=1+0=1. Тоді воно остаточно вирішиться fib(3)=fib(2)+fib(1), але його не потрібно перераховувати fib(2), тому що ми кешували це.
    • Це починається у верхній частині дерева і оцінює підпрограми від листя / підтрубки назад до кореня.
  • Таблиця - Динамічне програмування також можна розглядати як алгоритм «заповнення таблиці» (хоча зазвичай багатовимірний, ця «таблиця» може мати неевклідову геометрію в дуже рідкісних випадках *). Це як запам'ятовування, але більш активне і передбачає ще один додатковий крок: Ви повинні заздалегідь вибрати точний порядок, у якому ви будете робити свої обчислення. Це не повинно означати, що порядок повинен бути статичним, але у вас набагато більше гнучкості, ніж запам'ятовування.

    • Приклад: Якщо ви виконуєте Fibonacci, ви можете вибрати для обчислення числа в такому порядку: fib(2), fib(3), fib(4)... кешування кожне значення , так що ви можете вирахувати такі з них легше. Ви також можете вважати це заповненням таблиці (інша форма кешування).
    • Я особисто багато не чую слова «табуляція», але це дуже пристойний термін. Дехто вважає це "динамічним програмуванням".
    • Перш ніж запустити алгоритм, програміст розглядає ціле дерево, після чого пише алгоритм для оцінки підпроблем у певному порядку до кореня, як правило, заповнюючи таблицю.
    • * виноска: Іноді "стіл" не є прямокутною таблицею з сіткою, як такою, як таке. Це може мати складнішу структуру, наприклад дерево, або структуру, характерну для проблемного домену (наприклад, міста, що знаходяться в межах відстані на карті), або навіть графічну діаграму, яка, не маючи сітки, не має наприклад, структура підключення вгору-вліво-вправо тощо. Наприклад, user3290797 зв'язав приклад динамічного програмування пошуку максимального незалежного набору в дереві , що відповідає заповненню пробілів у дереві.

(Як правило, в парадигмі "динамічного програмування" я б сказав, що програміст розглядає ціле дерево, тодіпише алгоритм, який реалізує стратегію оцінки підпроблем, яка може оптимізувати будь-які властивості, які ви хочете (зазвичай це комбінація часової складності та просторової складності). Ваша стратегія повинна починатися десь із якоїсь конкретної підпроблеми і, можливо, може адаптуватися на основі результатів цих оцінок. У загальному розумінні "динамічного програмування" ви можете спробувати кешувати ці підпрограми, і, загалом, спробувати уникати повторного перегляду підпроблем з тонким відмінністю, можливо, це стосується графіків у різних структурах даних. Дуже часто такі структури даних лежать в основі, як масиви або таблиці. Рішення підпроблем можна викинути, якщо вони вже не потрібні.)

[Раніше ця відповідь робила заяву про термінологію зверху вниз та знизу вгору; чітко існують два основні підходи, що називаються "Мемоїзація та Таблиця", які можуть бути в поєднанні з цими термінами (хоча і не повністю). Загальним терміном, який більшість людей використовують, все ще є "Динамічне програмування", і деякі люди кажуть "Пам'ять", щоб посилатися на саме цей підтип "Динамічне програмування". Ця відповідь відмовляється сказати, що знаходиться зверху вниз і знизу вгору, поки громада не зможе знайти належні посилання в наукових працях. Зрештою, важливо зрозуміти відмінність, а не термінологію.]


Плюси і мінуси

Простота кодування

Пам'ять дуже легко кодувати (ви, як правило, * можете написати анотацію "мемоалізатора" або функцію обгортки, яка автоматично робить це за вас), і це має бути вашим першим напрямком підходу. Недоліком таблиць є те, що вам потрібно придумати замовлення.

* (це насправді просто, якщо ви пишете функцію самостійно та / або кодуєте нечистою / нефункціональною мовою програмування ... наприклад, якщо хтось уже написав попередньо складену fibфункцію, вона обов'язково робить рекурсивні дзвінки до себе, і ви не можете магічно запам'ятати функцію, не забезпечивши, щоб ці рекурсивні дзвінки викликали вашу нову запам'ятовувану функцію (а не оригінальну невстановлену функцію))

Рекурсивність

Зауважте, що і зверху вниз, і знизу вгору можна реалізувати за допомогою рекурсії або ітеративного заповнення таблиці, хоча це може бути не природно.

Практичні проблеми

Якщо запам'ятовується, якщо дерево дуже глибоке (наприклад fib(10^6)), у вас буде не вистачає простору стека, тому що кожне затримка обчислень повинна бути поставлена ​​на стек, і у вас буде 10 ^ 6 з них.

Оптимальність

Будь-який підхід може не бути оптимальним за часом, якщо замовлення, яке ви (або намагаєтесь) відвідати підпрограми, не є оптимальним, зокрема, якщо існує декілька способів обчислення підпрограми (звичайно кешування вирішить це, але теоретично можливо, що кешування може не в деяких екзотичних випадках). Пам'ять зазвичай додасть вашої часової складності вашій просторовій складності (наприклад, за допомогою табуляції у вас є більше свободи відкидати обчислення, як-от використання табуляції за допомогою Fib дозволяє використовувати простір O (1), але запам'ятовування за допомогою Fib використовує O (N) простір укладання).

Розширені оптимізації

Якщо ви також робите надзвичайно складні проблеми, у вас, можливо, не залишається іншого вибору, як робити табуляцію (або, принаймні, брати більш активну роль у керуванні запам'ятовуванням, куди ви хочете). Крім того, якщо ви потрапляєте в ситуацію, коли оптимізація є абсолютно критичною, і ви повинні оптимізувати її, таблична таблиця дозволить вам зробити оптимізацію, запам'ятовування якої інакше не дозволило б вам зробити це здоровим способом. На мою скромну думку, у звичайній інженерії програмного забезпечення жоден із цих двох випадків ніколи не з'являється, тому я просто використовував би запам'ятовування ("функція, яка кешує свої відповіді"), якщо щось (наприклад, простір стека) не потребує табуляції ... хоча технічно уникнути вибуху стека ви можете 1) збільшити обмеження розміру стека на мовах, які це дозволяють, або 2) з'їсти постійний фактор додаткової роботи для віртуалізації стека (ick),


Більш складні приклади

Тут ми перераховуємо приклади, що представляють особливий інтерес, які є не просто загальними проблемами ДП, але цікаво розрізняють запам’ятовування та табуляцію. Наприклад, одна рецептура може бути набагато простішою, ніж інша, або може бути оптимізація, яка в основному вимагає складання таблиць:

  • алгоритм обчислення відстані редагування [ 4 ], цікавий як нетривіальний приклад двовимірного алгоритму заповнення таблиці

3
@ coder000001: для прикладів python ви можете шукати в Google python memoization decorator; деякі мови дозволять вам написати макрос або код, який інкапсулює шаблон запам'ятовування. Шаблон запам'ятовування - це не що інше, як "замість виклику функції. Знайдіть значення з кеша (якщо значення там немає, обчисліть його та додайте його в кеш спочатку)".
ninjagecko

16
Я не бачу, щоб це хтось згадував, але я думаю, що ще одна перевага Top Down - це те, що ви будуватимете таблицю / кеш лише для відстеження. (тобто ви заповнюєте значення там, де вони вам фактично потрібні). Отож, це може бути і плюси, крім простого кодування. Іншими словами, зверху вниз може заощадити фактичний час роботи, оскільки ви не обчислюєте все (у вас може бути надзвичайно кращий час роботи, але той же асимптотичний час роботи). Але для збереження додаткових кадрів стека потрібна додаткова пам'ять (знову ж таки, споживання пам’яті «може» (тільки може) подвоїтись, але асимптотично це те саме.
Проінформовано

2
У мене складається враження, що підходи зверху вниз, що кешують рішення перекриваються підпроблем, - це методика, що називається пам яткою . Техніка знизу вгору, яка заповнює таблицю, а також дозволяє уникнути повторного обчислення перепрофілюваних підпроблем, називається табуляцією . Ці методи можуть бути використані при використанні динамічного програмування , яке стосується вирішення підпроблем для вирішення набагато більшої проблеми. Це здається суперечливим з цією відповіддю, де ця відповідь у багатьох місцях використовує динамічне програмування замість табуляції . Хто правильний?
Саммарон

1
@Sammaron: Хм, ви добре зробите свою думку. Я, мабуть, мав би перевірити своє джерело у Вікіпедії, якого я не можу знайти. Перевіривши cstheory.stackexchange трохи, я погоджуюся, що "знизу вгору" означало б, що дно відомо заздалегідь (табуляція), а "згори вниз" - це ви припускаєте рішення підпроблем / підрядів. У той час я виявив термін неоднозначним, і я інтерпретував фрази в подвійному перегляді ("знизу вгору" ви приймаєте рішення для підпроблем і запам'ятовуєте; "згори вниз" ви знаєте, які підпрограми ви збираєтесь і можете скласти таблицю). Я спробую вирішити це в редакції.
ninjagecko

1
@mgiuffrida: Простір стеків іноді трактується по-різному, залежно від мови програмування. Наприклад, у python спроба виконати запам'ятовуваний рекурсивний фіб не вдасться сказати fib(513). Перевантажена термінологія, яку я відчуваю, стає тут на шляху. 1) Ви завжди можете викинути підпрограми, які вам більше не потрібні. 2) Ви завжди можете уникати обчислення підпроблем, які вам не потрібні. 3) 1 і 2 може бути набагато складніше кодувати без чіткої структури даних, щоб зберігати підпрограми в АБО, складніше, якщо потік управління повинен перепливати між викликами функцій (можливо, вам знадобиться стан або продовження).
ninjagecko

76

DP “згори вниз і вгору” - це два різні способи вирішення одних і тих же проблем. Розгляньте запам’ятоване (зверху вниз) та динамічне (програмування знизу вгору) рішення для обчислення чисел чисел.

fib_cache = {}

def memo_fib(n):
  global fib_cache
  if n == 0 or n == 1:
     return 1
  if n in fib_cache:
     return fib_cache[n]
  ret = memo_fib(n - 1) + memo_fib(n - 2)
  fib_cache[n] = ret
  return ret

def dp_fib(n):
   partial_answers = [1, 1]
   while len(partial_answers) <= n:
     partial_answers.append(partial_answers[-1] + partial_answers[-2])
   return partial_answers[n]

print memo_fib(5), dp_fib(5)

Особисто я вважаю запам'ятовування набагато природнішим. Ви можете взяти рекурсивну функцію та запам'ятати її механічним процесом (спочатку шукайте відповідь у кеші та повертайте її, якщо можливо, інакше обчислюйте її рекурсивно, а потім перед поверненням ви зберігаєте обчислення в кеші для подальшого використання), виконуючи при цьому знизу вгору динамічне програмування вимагає кодування порядку, в якому розраховуються рішення, таким чином, щоб не було обчислено жодної «великої проблеми» перед меншою проблемою, від якої це залежить.


1
Ага, тепер я бачу, що означають «зверху вниз» та «знизу вгору»; це насправді лише посилання на запам'ятовування проти DP. І подумати я був тим, хто редагував питання, щоб згадати DP у назві ...
ninjagecko

Який час виконання запам'ятовуваних фіб / с нормальних рекурсивних фіб?
Сіддхартха

експоненціальна (2 ^ n) для нормального coz це дерево рекурсії, я думаю.
Сіддхартха

1
Так, це лінійно! Я намалював дерево рекурсії і побачив, яких дзвінків можна уникнути, і зрозумів, що дзвінки memo_fib (n - 2) будуть уникати після першого виклику до нього, і тому всі правильні гілки дерева рекурсії будуть відрізані, і це зведемо до лінійного.
Сіддхартха

1
Оскільки DP по суті включає створення таблиці результатів, де кожен результат обчислюється не більше одного разу, один простий спосіб візуалізації виконання алгоритму DP - це бачити, наскільки велика таблиця. У цьому випадку він має розмір n (один результат на кожне вхідне значення), тому O (n). В інших випадках це може бути матриця n ^ 2, в результаті чого O (n ^ 2) і т. Д.
Джонсон Вонг

22

Ключовою особливістю динамічного програмування є наявність підпрограм, що перекриваються . Тобто проблема, яку ви намагаєтеся вирішити, може бути розбита на підпрограми, і багато з цих підпроблем поділяють підпроблеми. Це як "Розділяй і перемагай", але ти в кінці кінців робиш те саме, багато-багато разів. Приклад, який я використовував з 2003 року, коли викладав або пояснював ці питання: ви можете обчислити числа Фібоначчі рекурсивно.

def fib(n):
  if n < 2:
    return n
  return fib(n-1) + fib(n-2)

Скористайтеся улюбленою мовою та спробуйте запустити її fib(50). Пройде дуже, дуже довго. Приблизно стільки ж часу, як і fib(50)сама! Однак багато непотрібної роботи робиться. fib(50)зателефонує fib(49)і fib(48), але тоді обидва ці завершать дзвінок fib(47), навіть якщо значення однакове. Фактично, fib(47)буде обчислено три рази: прямим дзвінком від fib(49), прямим дзвінком від fib(48), а також прямим дзвінком від іншого fib(48), того, що було породжене обчисленням fib(49)... Отже, ви бачите, у нас підпрограми, що перекриваються .

Чудова новина: немає необхідності обчислювати одне і те ж значення багато разів. Після того, як ви обчислите його один раз, кешуйте результат, а в наступний раз використовуйте кешоване значення! У цьому суть динамічного програмування. Ви можете назвати це "зверху вниз", "запам'ятовування" або будь-що інше. Цей підхід дуже інтуїтивно зрозумілий і дуже простий у здійсненні. Просто спочатку напишіть рекурсивне рішення, протестуйте його на невеликих тестах, додайте запам'ятовування (кешування вже обчислених значень) та --- бінго! --- ви закінчили.

Зазвичай ви також можете написати еквівалентну ітераційну програму, яка працює знизу вгору, без рекурсії. У цьому випадку це був би більш природний підхід: обведіть від 1 до 50 обчислення всіх чисел Фібоначчі, як ви йдете.

fib[0] = 0
fib[1] = 1
for i in range(48):
  fib[i+2] = fib[i] + fib[i+1]

У будь-якому цікавому сценарії рішення знизу вгору зазвичай важче зрозуміти. Однак, як тільки ви це зрозумієте, зазвичай ви отримаєте набагато чіткішу картину того, як працює алгоритм. На практиці при вирішенні нетривіальних проблем я рекомендую спочатку написати підхід зверху вниз і протестувати його на невеликих прикладах. Потім напишіть рішення знизу вгору і порівняйте два, щоб переконатися, що ви отримуєте те саме. В ідеалі порівняйте два рішення автоматично. Напишіть невеликий розпорядок, який створив би багато тестів, в ідеалі - всеневеликі тести до певного розміру --- і підтверджують, що обидва рішення дають однаковий результат. Після цього використовуйте рішення "знизу вгору" у виробництві, але зберігайте код зверху вниз, коментуючи його. Це полегшить іншим розробникам зрозуміти, що це ви робите: код знизу вгору може бути зовсім незрозумілим, навіть ви його написали і навіть якщо ви точно знаєте, що робите.

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

Зараз є проблеми, коли підхід зверху вниз - єдине можливе рішення, оскільки простір проблеми настільки великий, що неможливо вирішити всі підпрограми. Однак "кешування" все ще працює в розумний час, оскільки для вашого введення потрібна лише частина субпроблем, яку потрібно вирішити --- але занадто хитро, щоб чітко визначити, які підпрограми вам потрібно вирішити, а отже, і написати " рішення. З іншого боку, є ситуації, коли ви знаєте, що вам потрібно буде вирішити всі підпрограми. У цьому випадку продовжуйте та використовуйте знизу вгору.

Я особисто використовував би оптимізацію абзацу, відому проблему оптимізації обгортання Word (шукайте алгоритми розбиття рядків Knuth-Plass; принаймні TeX використовує його, а деяке програмне забезпечення Adobe Systems використовує аналогічний підхід). Я б використав знизу вгору для швидкої трансформації Фур'є .


Здравствуйте!!! Я хочу визначити, чи є правильними наступні пропозиції. - Для алгоритму динамічного програмування обчислення всіх значень знизу вгору асимптотично швидше, ніж використання рекурсії та запам'ятовування. - Час динамічного алгоритму завжди Ο (Ρ), де Ρ - кількість підпроблем. - Кожна проблема в НП може бути вирішена експоненціально.
Мері Стар

Що я можу сказати про вищезазначені пропозиції? У вас є ідея? @osa
Mary Star

@evinda, (1) завжди помиляється. Це або однаково, або асимптотично повільніше (коли вам не потрібні всі підпрограми, рекурсія може бути швидшою). (2) є правильним, лише якщо ви можете вирішити кожну підпроблему в O (1). (3) є своєрідним правом. Кожна проблема в NP може бути вирішена в поліноміальний час на недетермінованій машині (як квантовий комп'ютер, який може робити кілька речей одночасно: мати торт і одночасно з'їсти його та простежити обидва результати). Тож у певному сенсі кожну проблему в НП можна вирішити в експоненційному часі на звичайному комп’ютері. Примітка SIde: все в P також в NP. Наприклад, додавання двох цілих чисел
osa

19

Давайте візьмемо приклад серій фільмів

1,1,2,3,5,8,13,21....

first number: 1
Second number: 1
Third Number: 2

Ще один спосіб,

Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21

У випадку першої п'ятикласної цифри

Bottom(first) number :1
Top (fifth) number: 5 

Тепер давайте розглянемо рекурсивний алгоритм рядів Фібоначчі як приклад

public int rcursive(int n) {
    if ((n == 1) || (n == 2)) {
        return 1;
    } else {
        return rcursive(n - 1) + rcursive(n - 2);
    }
}

Тепер, якщо ми виконуємо цю програму з наступними командами

rcursive(5);

якщо ми уважно вивчимо алгоритм, то для того, щоб генерувати п'яте число, йому потрібні 3-й та 4-й числа. Тож моя рекурсія насправді починається з верху (5), а потім йде до самого нижнього / нижнього числа. Цей підхід є фактично підходом зверху вниз.

Щоб не робити один і той же розрахунок кілька разів, ми використовуємо методи динамічного програмування. Ми зберігаємо обчислені раніше значення та повторно їх використовуємо. Ця методика називається запам’ятовуванням. Для динамічного програмування є більше, ніж запам'ятовування, яке не потрібно для обговорення поточної проблеми.

З верху до низу

Давайте перепишемо наш оригінальний алгоритм та додамо запам’ятовувані методи.

public int memoized(int n, int[] memo) {
    if (n <= 2) {
        return 1;
    } else if (memo[n] != -1) {
        return memo[n];
    } else {
        memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
    }
    return memo[n];
}

І ми виконуємо такий метод, як наступний

   int n = 5;
    int[] memo = new int[n + 1];
    Arrays.fill(memo, -1);
    memoized(n, memo);

Це рішення все ще є зверху вниз, оскільки алгоритм починається з найвищого значення і переходить до низу кожного кроку, щоб отримати наше максимальне значення.

Знизу вгору

Але питання полягає в тому, чи можемо ми почати знизу, як з першого номера, тоді ми підемо вгору. Дозволяє переписати його за допомогою цих прийомів,

public int dp(int n) {
    int[] output = new int[n + 1];
    output[1] = 1;
    output[2] = 1;
    for (int i = 3; i <= n; i++) {
        output[i] = output[i - 1] + output[i - 2];
    }
    return output[n];
}

Тепер, якщо ми розглянемо цей алгоритм, він фактично починається з нижчих значень, тоді переходимо до верхнього. Якщо мені потрібно число 5-го рівня, я фактично обчислюю 1-е, потім друге, а потім третє, аж до 5-го числа. Ця техніка насправді називається прийомами знизу вгору.

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


Чи можна сказати, що підхід знизу вгору часто реалізується не рекурсивно?
Льюїс Чан

Ні, ви можете перетворити будь-яку логіку циклу на рекурсію
Ашвін Шарма

3

Динамічне програмування часто називають Пам'ять!

1.Мемоїзація - це техніка зверху вниз (почніть вирішувати задану проблему шляхом її розбиття), а динамічне програмування - це техніка знизу вгору (починайте вирішувати з тривіальної підпроблеми, до заданої задачі)

2.DP знаходить рішення, починаючи з базових випадків і працює вгору. DP вирішує всі підпроблеми, оскільки робить це знизу вгору

На відміну від Memoization, який вирішує лише необхідні підпроблеми

  1. DP має потенціал перетворення рівномірних рішень грубої сили в алгоритми багаточленного часу.

  2. DP може бути набагато ефективнішим, оскільки його ітераційний

Навпаки, спогади повинні платити за (часто значні) накладні витрати через рекурсію.

Щоб бути більш простим, пам'ять використовує підхід зверху вниз для вирішення проблеми, тобто починається з основної (головної) проблеми, потім розбиває її на підпроблеми і вирішує ці підпроблеми аналогічно. У такому підході одна і та ж субпроблема може виникати багаторазово і потребувати більше циклу процесора, отже, збільшуючи часову складність. Тоді як в динамічному програмуванні одна і та ж підпроблема не буде вирішена багаторазово, але попередній результат буде використаний для оптимізації рішення.


4
це неправда, в пам’яті використовується кеш-пам'ять, яка допоможе вам зберегти трудомісткість до того ж, що і DP
InformedA

3

Простіше кажучи, підхід зверху вниз використовує рекурсію для повторного виклику проблем Sub,
де в якості підходу знизу вгору використовується одинарна без виклику жодної, а значить, вона є більш ефективною.


1

Далі йде рішення на основі DP для вирішення проблеми відстані, яка знаходиться зверху вниз. Я сподіваюся, що це також допоможе зрозуміти світ динамічного програмування:

public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
         int m = word2.length();
            int n = word1.length();


     if(m == 0) // Cannot miss the corner cases !
                return n;
        if(n == 0)
            return m;
        int[][] DP = new int[n + 1][m + 1];

        for(int j =1 ; j <= m; j++) {
            DP[0][j] = j;
        }
        for(int i =1 ; i <= n; i++) {
            DP[i][0] = i;
        }

        for(int i =1 ; i <= n; i++) {
            for(int j =1 ; j <= m; j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1))
                    DP[i][j] = DP[i-1][j-1];
                else
                DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
            }
        }

        return DP[n][m];
}

Ви можете подумати про його рекурсивну реалізацію у себе вдома. Це досить добре і складно, якщо ви раніше не вирішували щось подібне.


1

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

int n = 5;
fibTopDown(1, 1, 2, n);

private int fibTopDown(int i, int j, int count, int n) {
    if (count > n) return 1;
    if (count == n) return i + j;
    return fibTopDown(j, i + j, count + 1, n);
}

Знизу вгору : поточний результат залежить від результату його підзадачі.

int n = 5;
fibBottomUp(n);

private int fibBottomUp(int n) {
    if (n <= 1) return 1;
    return fibBottomUp(n - 1) + fibBottomUp(n - 2);
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.