mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-10 14:36:06 +00:00
build
This commit is contained in:
@@ -18,7 +18,7 @@ comments: true
|
||||
|
||||
<p align="center"> Рисунок 14-1 Число способов подняться на 3-ю ступень </p>
|
||||
|
||||
Цель этой задачи - вычислить количество способов. **Поэтому можно попробовать использовать для ее решения метод поиска с возвратом**. Если представить подъем по лестнице как последовательность решений, то мы начинаем от земли и на каждом раунде выбираем прыжок на $1$ или на $2$ ступени; всякий раз, когда достигаем вершины, увеличиваем число способов на $1$ , а если перескакиваем вершину, обрезаем эту ветвь. Код выглядит так:
|
||||
Цель этой задачи - вычислить количество способов. **Поэтому можно попробовать использовать для ее решения метод поиска с возвратом**. Если представить подъем по лестнице как последовательность решений, то мы начинаем от земли и на каждом раунде выбираем прыжок на $1$ или на $2$ ступени. Всякий раз, когда достигаем вершины, увеличиваем число способов на $1$ , а если перескакиваем вершину, обрезаем эту ветвь. Код выглядит так:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -427,9 +427,9 @@ comments: true
|
||||
|
||||
## 14.1.1 Метод 1: полный перебор
|
||||
|
||||
Алгоритм поиска с возвратом обычно не раскладывает задачу явно на подзадачи; вместо этого он рассматривает решение как последовательность решений, используя попытки и обрезку для поиска всех возможных ответов.
|
||||
Алгоритм поиска с возвратом обычно не раскладывает задачу явно на подзадачи. Вместо этого он рассматривает решение как последовательность решений, используя попытки и обрезку для поиска всех возможных ответов.
|
||||
|
||||
Попробуем посмотреть на задачу именно как на разложение подзадач. Пусть число способов добраться до ступени $i$ равно $dp[i]$ ; тогда $dp[i]$ - это исходная задача, а ее подзадачи включают:
|
||||
Попробуем посмотреть на задачу именно как на разложение подзадач. Пусть число способов добраться до ступени $i$ равно $dp[i]$. Тогда $dp[i]$ - это исходная задача, а ее подзадачи включают:
|
||||
|
||||
$$
|
||||
dp[i-1], dp[i-2], \dots, dp[2], dp[1]
|
||||
@@ -706,7 +706,7 @@ $$
|
||||
|
||||
<p align="center"> Рисунок 14-3 Дерево рекурсии для подъема по лестнице </p>
|
||||
|
||||
Если посмотреть на рисунок 14-3, то видно, что **экспоненциальная временная сложность порождается "перекрывающимися подзадачами"**. Например, $dp[9]$ раскладывается в $dp[8]$ и $dp[7]$ , а $dp[8]$ - в $dp[7]$ и $dp[6]$ ; обе ветви содержат подзадачу $dp[7]$ .
|
||||
Как видно на рисунке 14-3, **экспоненциальная временная сложность порождается «перекрывающимися подзадачами»**. Например, $dp[9]$ раскладывается в $dp[8]$ и $dp[7]$ , а $dp[8]$ - в $dp[7]$ и $dp[6]$. Обе ветви содержат подзадачу $dp[7]$ .
|
||||
|
||||
Продолжая это рассуждение, мы видим, что подзадачи порождают все более мелкие перекрывающиеся подзадачи без конца. Подавляющая часть вычислительных ресурсов уходит именно на них.
|
||||
|
||||
@@ -1075,11 +1075,11 @@ $$
|
||||
|
||||
## 14.1.3 Метод 3: динамическое программирование
|
||||
|
||||
**Поиск с мемоизацией - это метод "сверху вниз"** : мы начинаем с исходной задачи (корня), рекурсивно раскладываем более крупные подзадачи на меньшие, пока не достигнем наименьших подзадач с уже известным ответом (листьев). Затем в процессе возврата постепенно собираем решения подзадач и тем самым получаем решение исходной задачи.
|
||||
**Поиск с мемоизацией - это метод «сверху вниз»** : мы начинаем с исходной задачи (корня), рекурсивно раскладываем более крупные подзадачи на меньшие, пока не достигнем наименьших подзадач с уже известным ответом (листьев). Затем в процессе возврата постепенно собираем решения подзадач и тем самым получаем решение исходной задачи.
|
||||
|
||||
Напротив, **динамическое программирование - это метод "снизу вверх"** : начиная с решений наименьших подзадач, мы итеративно строим решения для более крупных подзадач, пока не получим ответ на исходную задачу.
|
||||
Напротив, **динамическое программирование - это метод «снизу вверх»** : начиная с решений наименьших подзадач, мы итеративно строим решения для более крупных подзадач, пока не получим ответ на исходную задачу.
|
||||
|
||||
Поскольку в динамическом программировании нет этапа возврата, для его реализации достаточно обычных циклов, без рекурсии. В приведенном ниже коде мы инициализируем массив `dp` для хранения решений подзадач; он выполняет ту же роль, что и массив `mem` в мемоизированном поиске:
|
||||
Поскольку в динамическом программировании нет этапа возврата, для его реализации достаточно обычных циклов, без рекурсии. В приведенном ниже коде мы инициализируем массив `dp` для хранения решений подзадач. Он выполняет ту же роль, что и массив `mem` в мемоизированном поиске:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -1349,7 +1349,7 @@ $$
|
||||
|
||||
<p align="center"> Рисунок 14-5 Процесс динамического программирования для подъема по лестнице </p>
|
||||
|
||||
Как и в поиске с возвратом, в динамическом программировании используется понятие "состояние" для обозначения некоторого этапа решения задачи; каждое состояние соответствует одной подзадаче и ее локально оптимальному решению. Например, в задаче о лестнице состояние определяется текущим номером ступени $i$ .
|
||||
Как и в поиске с возвратом, в динамическом программировании используется понятие «состояние» для обозначения некоторого этапа решения задачи. Каждое состояние соответствует одной подзадаче и ее локально оптимальному решению. Например, в задаче о лестнице состояние определяется текущим номером ступени $i$ .
|
||||
|
||||
На основе сказанного можно подвести несколько часто используемых терминов динамического программирования.
|
||||
|
||||
@@ -1359,7 +1359,7 @@ $$
|
||||
|
||||
## 14.1.4 Оптимизация пространства
|
||||
|
||||
Внимательный читатель мог заметить, что **поскольку $dp[i]$ зависит только от $dp[i-1]$ и $dp[i-2]$ , нам не нужен весь массив `dp` для хранения ответов всех подзадач** ; достаточно двух переменных, которые будут "перекатываться" вперед. Код имеет вид:
|
||||
Внимательный читатель мог заметить, что **поскольку $dp[i]$ зависит только от $dp[i-1]$ и $dp[i-2]$ , нам не нужен весь массив `dp` для хранения ответов всех подзадач**. Достаточно двух переменных, которые будут «перекатываться» вперед. Код имеет вид:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -1582,4 +1582,4 @@ $$
|
||||
|
||||
Из кода видно, что после отказа от массива `dp` пространственная сложность уменьшается с $O(n)$ до $O(1)$ .
|
||||
|
||||
Во многих задачах динамического программирования текущее состояние зависит лишь от ограниченного числа предыдущих состояний. Тогда можно сохранять только действительно нужные состояния и за счет "уменьшения размерности" экономить память. **Этот прием оптимизации памяти называют "скользящими переменными" или "скользящим массивом"**.
|
||||
Во многих задачах динамического программирования текущее состояние зависит лишь от ограниченного числа предыдущих состояний. Тогда можно сохранять только действительно нужные состояния и за счет «уменьшения размерности» экономить память. **Этот прием оптимизации памяти называют «скользящими переменными» или «скользящим массивом»**.
|
||||
|
||||
Reference in New Issue
Block a user