Files
hello-algo/ru/docs/chapter_dynamic_programming/exercises.md
T
2026-07-19 07:19:42 +08:00

112 lines
10 KiB
Markdown

# Упражнения
## Вопросы для самопроверки
### Когда подходит динамическое программирование
Ученик утверждает: «Если можно записать рекуррентное соотношение, нужно использовать динамическое программирование».
Для каждой из трех задач выберите наиболее подходящий из трех вариантов: динамическое программирование; поиск с возвратом; цикл или математическая формула без таблицы `dp`. Укажите одну основную причину.
<!-- numbered-subquestions -->
1. Набрать сумму 6 с помощью монет номиналов `[1, 3, 4]`, используя каждый номинал любое число раз, и найти минимальное число монет;
2. вывести все перестановки `[1, 2, 3]`;
3. вычислить $1 + 2 + \dots + n$.
Для задачи, которой подходит динамическое программирование, также объясните значение `dp[i]`.
??? success "Ответ"
1. Подходит динамическое программирование. Пусть `dp[i]` — минимальное число монет, необходимое для получения суммы `i`.
Для каждой монеты `c`, не превосходящей `i`, значение `dp[i-c] + 1` можно рассматривать как возможный ответ,
а затем выбрать минимум среди всех вариантов. Разные последовательности выбора многократно приводят к одной и той же сумме, а оптимальное решение для большей суммы можно составить из оптимальных решений для меньших сумм.
Для суммы 6 ответ равен 2: `3 + 3`.
2. Следует использовать поиск с возвратом. Задача требует поочередно получить все 6 перестановок; поиск с возвратом позволяет систематически сделать выбор, продолжить поиск,
затем отменить выбор и перейти к другой ветви. Каким бы ни был метод, при фактическом выводе всех перестановок пропустить их перебор нельзя.
3. Достаточно цикла или формулы суммы арифметической прогрессии. Хотя можно записать `S(i) = S(i-1) + i`, при вычислении `S(i)` требуется только одно меньшее значение `S(i-1)`,
а каждая частичная сумма вычисляется один раз. Повторяющихся подзадач нет, поэтому таблица `dp` не нужна. Возможность записать рекуррентное соотношение не означает необходимости динамического программирования.
### Как вычислить одну ячейку таблицы рюкзака
Задача о рюкзаке 0-1: веса предметов `wgt = [1, 2, 3]`, их стоимости `val = [5, 11, 15]`, вместимость рюкзака равна 4.
`dp[i][c]` — максимальная стоимость, которую можно получить, рассматривая только первые $i$ предметов при предельной вместимости рюкзака $c$;
заполнять рюкзак точно до предела не требуется.
Вычислите только состояние `dp[3][4]`. Известно, что `dp[2][4] = 16` и `dp[2][1] = 5`.
<!-- numbered-subquestions -->
1. Какова возможная стоимость, если не брать 3-й предмет?
2. Какая вместимость останется, если взять 3-й предмет, и чему равна возможная стоимость?
3. Какое значение нужно присвоить `dp[3][4]`? Какие предметы ему соответствуют?
??? success "Ответ"
1. Если не брать 3-й предмет, сохраняется результат для первых двух предметов, поэтому возможная стоимость равна `dp[2][4] = 16`.
2. Вес 3-го предмета равен 3, после его добавления остается вместимость $4-3=1$. Возможная стоимость равна
`dp[2][1] + 15 = 5 + 15 = 20`.
3. Сравнив 16 и 20, получаем `dp[3][4] = 20`. Это соответствует выбору 1-го и 3-го предметов:
их общий вес равен $1+3=4$, а общая стоимость — $5+15=20$.
Вычисление этого состояния показывает один выбор в задаче о рюкзаке 0-1: «взять или не взять».
### В каком порядке обновлять вместимость рюкзака
В задаче о рюкзаке 0-1 есть только один предмет весом 2 и стоимостью 5; вместимость рюкзака равна 4.
Каждый предмет разрешено выбрать не более одного раза, а исходный одномерный массив имеет вид `dp = [0, 0, 0, 0, 0]`.
Обрабатывая предмет, ученик обновляет вместимость от 2 до 4:
- после обновления `dp[2]` получается 5;
- после обновления `dp[3]` также получается 5;
- при обновлении `dp[4]` снова используется только что полученное `dp[2]`, поэтому выходит `dp[4] = 10`.
<!-- numbered-subquestions -->
1. Правилен ли результат `dp[4] = 10`? Почему?
2. Чему должно быть равно `dp[4]`, если каждый предмет можно выбрать не более одного раза?
3. При обработке каждого предмета вместимость нужно перебирать от большей к меньшей или от меньшей к большей? Какую проблему предотвращает такой порядок?
??? success "Ответ"
1. Результат неверен. Стоимость 10 означает, что предмет стоимостью 5 положили в рюкзак дважды,
нарушив условие «каждый предмет можно выбрать не более одного раза».
2. В рюкзак можно положить не более одного такого предмета, поэтому правильное значение `dp[4]` равно 5.
3. Вместимость нужно обновлять от большей к меньшей, то есть в порядке 4, 3, 2.
Тогда при вычислении `dp[c]` читаемое значение `dp[c-2]` все еще относится к состоянию до обработки текущего предмета,
и этот предмет нельзя повторно использовать на той же итерации.
## Задачи по программированию
### Число способов подняться по лестнице
Лестница состоит из `n` ступеней. За один шаг можно подняться только на 1 или 2 ступени, причем нужно попасть точно на ступень `n`.
Вычислите число различных способов подъема. Считайте, что `n >= 1`; способы различаются только последовательностью шагов на 1 и 2 ступени.
Используйте одномерный массив динамического программирования; пока не применяйте оптимизацию памяти, сохраняющую лишь два состояния.
??? tip "Подсказки"
1. Последний шаг на ступень i мог быть только на 1 или на 2 ступени
2. Поэтому dp[i] = dp[i-1] + dp[i-2]
3. Сначала обработайте случаи n, равного 1 и 2, а затем заполняйте таблицу начиная с 3-й ступени
[LeetCode](https://leetcode.com/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### Рюкзак 0-1
Даны массивы одинаковой длины `wgt` и `val`: вес предмета `i` — положительное целое число `wgt[i]`, а его стоимость — неотрицательное целое число `val[i]`.
Вместимость рюкзака `cap` — неотрицательное целое число. Каждый предмет можно выбрать не более одного раза. Найдите максимальную суммарную стоимость предметов,
общий вес которых не превышает `cap`. Используйте одномерное динамическое программирование.
??? tip "Подсказки"
1. Создайте массив dp длины cap + 1, где dp[c] — максимальная стоимость при предельной вместимости c
2. Обрабатывая предмет i, сравните dp[c] для варианта «не брать предмет» и dp[c-wgt[i]] + val[i] для варианта «взять предмет»
3. Вместимость нужно обновлять от большей к меньшей, чтобы не выбрать текущий предмет повторно на одной итерации