Содержание статьи

Метод динамического программирования применяется для задач, где можно разбить проблему на пересекающиеся подзадачи. Классический пример – задача о рюкзаке: при весе до 50 кг и наборе из 10 предметов с разной стоимостью и весом, прямой перебор создаст 1024 комбинации, тогда как динамическое программирование решает её за время порядка O(nW), где n – число предметов, W – максимальный вес.
Динамическое программирование позволяет оптимизировать вычисления в задачах со строками и массивами. Например, поиск наибольшей общей подпоследовательности двух строк длиной 200 символов каждая требует порядка 40 000 сравнений при стандартной реализации, что существенно снижает нагрузку по сравнению с наивным подходом, перебирающим все подпоследовательности.
Метод также эффективен при планировании и разбиении ресурсов. В задаче о разрезании стержня длиной 10 метров с разными ценами за части длиной 1–10 метров, динамическое программирование помогает определить оптимальное разбиение для максимальной прибыли без перебора всех 2^9 вариантов.
В статье приведены конкретные примеры задач и пошаговые рекомендации по их решению: от подсчета количества способов разменять сумму монетами до нахождения кратчайшего пути в сетке с препятствиями. Каждый пример демонстрирует структуру подзадач, формулы для вычисления состояния и алгоритмы заполнения таблиц, что позволяет применять метод в практических сценариях.
Решение задачи о рюкзаке с ограничениями по весу

Задача о рюкзаке формулируется так: задан набор предметов, каждый с весом wi и стоимостью vi, и максимальный допустимый вес рюкзака W. Цель – выбрать подмножество предметов с максимальной суммарной стоимостью, не превышая W. Для n предметов и веса до 50 кг стандартная таблица динамического программирования создается размером (n+1) × (W+1).
Алгоритм строится снизу вверх: dp[i][j] хранит максимальную стоимость для первых i предметов при суммарном весе ≤ j. Формула обновления: dp[i][j] = max(dp[i-1][j], dp[i-1][j-wi] + vi), если j ≥ wi. Для весов меньше wi значение dp[i][j] копируется из dp[i-1][j]. Это обеспечивает учет всех комбинаций без перебора 2^n вариантов.
После заполнения таблицы оптимальное решение находится в dp[n][W]. Чтобы восстановить набор предметов, используется обратный проход: если dp[i][j] ≠ dp[i-1][j], предмет i включен в рюкзак, и j уменьшается на wi. Этот метод позволяет получить точное решение с вычислительной сложностью O(nW) и памятью O(nW) или O(W) при оптимизации по строкам.
Для практической реализации рекомендуется сначала сортировать предметы по соотношению vi/wi, чтобы при равных весах выбирать более ценные объекты. При больших W имеет смысл использовать сжатие таблицы и хранить только актуальные строки, что снижает потребление памяти без потери точности.
Поиск наибольшей общей подпоследовательности двух строк

Задача формулируется так: даны две строки длиной m и n, требуется найти максимальную последовательность символов, встречающуюся в обеих строках в одном и том же порядке, но не обязательно подряд. Для строк длиной 150 и 200 символов полный перебор потребовал бы порядка 1045 проверок, что невозможно на практике.
Алгоритм динамического программирования строит таблицу dp[i][j], где i и j – индексы символов первой и второй строки. Значение dp[i][j] хранит длину наибольшей общей подпоследовательности для первых i и j символов. Обновление выполняется по правилу: если s1[i] = s2[j], dp[i][j] = dp[i-1][j-1] + 1; иначе dp[i][j] = max(dp[i-1][j], dp[i][j-1]).
После заполнения таблицы длина наибольшей общей подпоследовательности хранится в dp[m][n]. Восстановление самой последовательности проводится обратным проходом: начиная с dp[m][n], если dp[i][j] ≠ dp[i-1][j] и dp[i][j] ≠ dp[i][j-1], символ s1[i] добавляется к результату, а индексы уменьшаются на один.
Для оптимизации памяти рекомендуется хранить только две строки таблицы одновременно, что уменьшает расход памяти с O(mn) до O(min(m,n)). Этот метод позволяет решать задачи для строк длиной несколько тысяч символов на стандартных компьютерах без потери точности.
Минимизация стоимости редактирования строки в строку

Задача редактирования строки требует преобразовать строку source длиной m в строку target длиной n с минимальными затратами. Допустимые операции:
- Вставка символа с стоимостью cinsert
- Удаление символа с стоимостью cdelete
- Замена символа с стоимостью creplace
Для решения строится таблица dp[i][j], где i и j – длины подстрок исходной и целевой строк. Значение dp[i][j] показывает минимальную стоимость приведения первых i символов source к первым j символам target. Формула:
- Если source[i] = target[j], dp[i][j] = dp[i-1][j-1]
- Если символы различны, dp[i][j] = min(dp[i-1][j] + cdelete, dp[i][j-1] + cinsert, dp[i-1][j-1] + creplace)
Решение хранится в dp[m][n]. Для восстановления последовательности операций выполняется обратный обход таблицы, выбирая действие, приведшее к минимальной стоимости на каждом шаге.
Практические рекомендации:
- Для строк длиной до 1000 символов стандартная таблица O(mn) подходит для большинства компьютеров.
- При ограниченной памяти можно хранить только текущую и предыдущую строку таблицы, что снижает расход с O(mn) до O(n).
- Если операции имеют разные стоимости, стоит заранее вычислить минимальные комбинации для часто встречающихся символов, чтобы ускорить вычисления.
Оптимальный способ разрезания стержня на части для максимальной прибыли

Задача разрезания стержня формулируется так: имеется стержень длиной n метров и таблица цен p[i] за куски длиной i метров. Требуется определить разбиение стержня на части, которое приносит максимальную суммарную прибыль. Полный перебор всех разбиений невозможен для n > 20, так как количество вариантов растет экспоненциально.
Для решения используется динамическое программирование с массивом dp[j], где j – длина стержня, а dp[j] – максимальная прибыль для этой длины. Алгоритм:
- Инициализировать dp[0] = 0.
- Для каждой длины j от 1 до n вычислять dp[j] = max(p[i] + dp[j-i]) для всех i ≤ j.
- Результат для полного стержня хранится в dp[n].
Для восстановления оптимального разреза используется дополнительный массив cut[j], который хранит длину первой части при оптимальном разбиении стержня длиной j. Обратным проходом по массиву cut определяется последовательность разрезов.
Практические рекомендации:
- При длине стержня до 1000 метров алгоритм с O(n²) вычислений решается за доли секунды на стандартном компьютере.
- Если цена кусочков не задана для всех длин, можно использовать p[i] = 0 для отсутствующих длин, что позволяет алгоритму корректно игнорировать недопустимые куски.
- Для ускорения расчетов на больших n полезно хранить только актуальные значения dp[j] и cut[j], минимизируя использование памяти.
Подсчет числа способов разменять сумму монетами

Задача формулируется так: задан набор монет с номиналами c1, c2, …, cn и сумма S. Требуется определить число различных комбинаций монет, которые дают сумму S. Полный перебор всех комбинаций невозможен для S > 50 и n > 10 из-за экспоненциального роста вариантов.
Для решения строится таблица динамического программирования dp[i][j], где i – индекс монеты, а j – сумма. Значение dp[i][j] хранит количество способов составить сумму j с использованием первых i монет. Правила обновления:
| Условие | Формула |
|---|---|
| Если j ≥ ci | dp[i][j] = dp[i-1][j] + dp[i][j-ci] |
| Если j < ci | dp[i][j] = dp[i-1][j] |
Начальное значение: dp[0][0] = 1, остальные dp[0][j] = 0. Итоговое количество способов для суммы S хранится в dp[n][S].
Практические рекомендации:
- Для набора из 5 монет и суммы 50 таблица занимает (n+1) × (S+1) = 6 × 51 элементов, что позволяет быстро вычислить результат.
- Для экономии памяти можно использовать одномерный массив dp[j] длиной S+1, обновляя значения в порядке увеличения суммы.
- Если требуется учесть порядок монет, алгоритм корректируется, перебирая сначала суммы, затем монеты.
Нахождение кратчайшего пути в сетке с препятствиями

Задача формулируется так: задана сетка размером m × n с ячейками, каждая из которых может быть проходимой или содержать препятствие. Необходимо найти количество кратчайших путей от верхнего левого угла до нижнего правого, двигаясь только вправо или вниз.
Для решения используется таблица динамического программирования dp[i][j], где i и j – координаты ячейки. Значение dp[i][j] хранит количество способов достигнуть этой ячейки. Алгоритм:
- Если ячейка содержит препятствие, dp[i][j] = 0.
- Если ячейка проходима, dp[i][j] = dp[i-1][j] + dp[i][j-1], с учетом границ сетки.
Начальное значение: dp[0][0] = 1, если стартовая ячейка свободна. Итоговое количество кратчайших путей хранится в dp[m-1][n-1].
Практические рекомендации:
- Для сетки 100 × 100 таблица размером 10 000 элементов легко помещается в память и вычисляется за доли секунды.
- Если требуется восстановить путь, можно хранить указатели на предыдущую ячейку с максимальным количеством способов или выполнять обратный обход.
- Для экономии памяти допустимо хранить только текущую и предыдущую строки таблицы, что снижает расход с O(mn) до O(n).
Решение задачи о разбиении массива для максимальной суммы подпоследовательностей

Задача формулируется так: дан массив чисел arr[0…n-1] и целое число k. Требуется разбить массив на непересекающиеся подпоследовательности длиной не более k, чтобы сумма максимальных элементов каждой подпоследовательности была максимальной.
Алгоритм динамического программирования строит массив dp[i], где i – индекс элемента массива. Значение dp[i] хранит максимальную сумму для первых i+1 элементов. Вычисление:
- Для каждого индекса i рассматриваем длины подпоследовательностей 1…k, не превышающие i+1.
- Для длины len определяем максимальный элемент maxVal в последних len элементах.
- Обновляем dp[i] = max(dp[i], dp[i-len] + len × maxVal), где dp[-1] = 0 для пустой подпоследовательности.
Пример: массив [1,15,7,9,2,5,10], k=3. Подпоследовательности [15,7,9] и [10] дают сумму 15+10=25. Таблица dp позволяет вычислить оптимальное разбиение за O(nk) операций.
Практические рекомендации:
- При больших n и k стоит хранить только последние k значений dp для экономии памяти.
- Для восстановления разбиения полезно хранить массив partition[i], где i – индекс конца подпоследовательности.
- Если элементы массива могут быть отрицательными, алгоритм корректно работает, так как учитывает максимальные элементы каждой подпоследовательности.
Вопрос-ответ:
Как выбрать правильную стратегию при решении задачи о рюкзаке с ограничением по весу?
При выборе стратегии следует учитывать два параметра: количество предметов n и максимальный вес рюкзака W. Если W невелик, удобно использовать таблицу dp[n+1][W+1], где каждая ячейка хранит максимальную стоимость для конкретного веса и количества предметов. При большом W можно хранить только одну строку таблицы, обновляя значения по мере перебора предметов. Для оптимизации стоит вычислять соотношение vi/wi и при равных весах отдавать приоритет более ценным предметам.
Почему метод динамического программирования подходит для поиска наибольшей общей подпоследовательности?
Метод позволяет разбить задачу на пересекающиеся подзадачи: для каждой пары индексов строк i и j вычисляется длина наибольшей общей подпоследовательности первых i и j символов. Это позволяет хранить промежуточные результаты в таблице dp[i][j], что уменьшает количество вычислений с экспоненциального числа комбинаций до порядка O(mn). Восстановление самой последовательности выполняется обратным проходом по таблице.
Как изменяется подход к размену суммы монетами, если порядок использования монет имеет значение?
Если важно учитывать порядок, то стандартный метод с накоплением количества способов в dp[j] требует изменения: перебор выполняется сначала по сумме, затем по каждой монете. В этом случае каждая комбинация монет считается отдельно, что увеличивает количество вариантов. Для сумм до 50 и 5–6 типов монет таблица легко вычисляется и позволяет получить точное число способов. Для больших наборов стоит использовать оптимизацию по памяти, сохраняя только текущую строку таблицы.
Какие особенности нужно учитывать при разбиении массива для максимальной суммы подпоследовательностей длиной до k?
Необходимо отслеживать максимальный элемент в каждой подпоследовательности, так как итоговая сумма зависит от него. Для каждого индекса i рассматриваются подпоследовательности длиной от 1 до k, не превышающие i+1. Для каждой длины вычисляется потенциальная сумма len × maxVal и сравнивается с текущим значением dp[i]. Важно хранить информацию о конце каждой подпоследовательности, если требуется восстановить разбиение. Если массив содержит отрицательные значения, алгоритм корректно учитывает их при выборе максимального элемента.
