mirror of
https://github.com/krahets/hello-algo.git
synced 2026-04-14 10:20:40 +08:00
First version.
This commit is contained in:
168
ru/docs/chapter_dynamic_programming/knapsack_problem.md
Normal file
168
ru/docs/chapter_dynamic_programming/knapsack_problem.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Задача о рюкзаке 0-1
|
||||
|
||||
Задача о рюкзаке является отличной вводной задачей для динамического программирования и представляет собой одну из наиболее распространенных форм задач в динамическом программировании. Она имеет множество вариаций, таких как задача о рюкзаке 0-1, задача о полном рюкзаке, задача о множественном рюкзаке и другие.
|
||||
|
||||
В этом разделе мы сначала решим наиболее распространенную задачу о рюкзаке 0-1.
|
||||
|
||||
!!! question
|
||||
|
||||
Дано $n$ предметов, где $i$-й предмет имеет вес $wgt[i-1]$ и ценность $val[i-1]$, а также рюкзак вместимостью $cap$. Каждый предмет можно выбрать только один раз. Найдите максимальную ценность предметов, которые можно поместить в рюкзак при ограниченной вместимости.
|
||||
|
||||
Рассмотрим рисунок ниже: поскольку нумерация предметов $i$ начинается с $1$, а индексы массива начинаются с $0$, предмет $i$ соответствует весу $wgt[i-1]$ и ценности $val[i-1]$.
|
||||
|
||||

|
||||
|
||||
Мы можем рассматривать задачу о рюкзаке 0-1 как процесс, состоящий из $n$ раундов принятия решений, где для каждого предмета существует два решения: не класть в рюкзак и положить в рюкзак. Таким образом, эта задача удовлетворяет модели дерева решений.
|
||||
|
||||
Цель этой задачи — найти "максимальную ценность предметов, которые можно поместить в рюкзак при ограниченной вместимости", поэтому с большой вероятностью это задача динамического программирования.
|
||||
|
||||
**Шаг первый: обдумать решение на каждом раунде, определить состояние и получить таблицу $dp$**
|
||||
|
||||
Для каждого предмета: если не класть его в рюкзак, вместимость рюкзака не изменяется; если положить в рюкзак, вместимость рюкзака уменьшается. Отсюда получаем определение состояния: текущий номер предмета $i$ и вместимость рюкзака $c$, обозначаемые как $[i, c]$.
|
||||
|
||||
Состояние $[i, c]$ соответствует подзадаче: **максимальная ценность первых $i$ предметов в рюкзаке вместимостью $c$**, обозначаемая как $dp[i, c]$.
|
||||
|
||||
Необходимо найти $dp[n, cap]$, поэтому требуется двумерная таблица $dp$ размером $(n+1) \times (cap+1)$.
|
||||
|
||||
**Шаг второй: найти оптимальную подструктуру и вывести уравнение перехода состояния**
|
||||
|
||||
После принятия решения относительно предмета $i$ остается подзадача принятия решений для первых $i-1$ предметов, которая может быть разделена на следующие два случая:
|
||||
|
||||
- **Не класть предмет $i$**: вместимость рюкзака не изменяется, состояние переходит в $[i-1, c]$.
|
||||
- **Положить предмет $i$**: вместимость рюкзака уменьшается на $wgt[i-1]$, ценность увеличивается на $val[i-1]$, состояние переходит в $[i-1, c-wgt[i-1]]$.
|
||||
|
||||
Приведенный выше анализ раскрывает оптимальную подструктуру этой задачи: **максимальная ценность $dp[i, c]$ равна большему значению из двух вариантов: не класть предмет $i$ и положить предмет $i$**. Отсюда можно вывести уравнение перехода состояния:
|
||||
|
||||
$$
|
||||
dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
$$
|
||||
|
||||
Необходимо отметить, что если текущий вес предмета $wgt[i - 1]$ превышает оставшуюся вместимость рюкзака $c$, то можно выбрать только вариант не класть предмет в рюкзак.
|
||||
|
||||
**Шаг третий: определить граничные условия и порядок перехода состояний**
|
||||
|
||||
Когда нет предметов или вместимость рюкзака равна $0$, максимальная ценность равна $0$, то есть первый столбец $dp[i, 0]$ и первая строка $dp[0, c]$ равны $0$.
|
||||
|
||||
Текущее состояние $[i, c]$ переходит из состояния сверху $[i-1, c]$ и состояния слева сверху $[i-1, c-wgt[i-1]]$, поэтому с помощью двух вложенных циклов можно последовательно обойти всю таблицу $dp$.
|
||||
|
||||
На основе приведенного выше анализа мы далее последовательно реализуем решения методом полного перебора, мемоизации поиска и динамического программирования.
|
||||
|
||||
### Первый метод: полный перебор
|
||||
|
||||
Код поиска включает следующие элементы:
|
||||
|
||||
- **Параметры рекурсии**: состояние $[i, c]$.
|
||||
- **Возвращаемое значение**: решение подзадачи $dp[i, c]$.
|
||||
- **Условие завершения**: когда номер предмета выходит за границы $i = 0$ или оставшаяся вместимость рюкзака равна $0$, завершить рекурсию и вернуть ценность $0$.
|
||||
- **Обрезка**: если текущий вес предмета превышает оставшуюся вместимость рюкзака, можно выбрать только вариант не класть предмет в рюкзак.
|
||||
|
||||
```src
|
||||
[file]{knapsack}-[class]{}-[func]{knapsack_dfs}
|
||||
```
|
||||
|
||||
Как показано на рисунке ниже, поскольку каждый предмет порождает две ветви поиска (не выбирать и выбирать), временная сложность составляет $O(2^n)$.
|
||||
|
||||
Наблюдая за деревом рекурсии, легко заметить наличие перекрывающихся подзадач, например $dp[1, 10]$ и других. Когда предметов много, вместимость рюкзака велика, особенно когда много предметов одинакового веса, количество перекрывающихся подзадач значительно увеличивается.
|
||||
|
||||

|
||||
|
||||
### Второй метод: мемоизация поиска
|
||||
|
||||
Чтобы гарантировать, что перекрывающиеся подзадачи вычисляются только один раз, мы используем список мемоизации `mem` для записи решений подзадач, где `mem[i][c]` соответствует $dp[i, c]$.
|
||||
|
||||
После введения мемоизации **временная сложность зависит от количества подзадач**, то есть $O(n \times cap)$. Код реализации приведен ниже:
|
||||
|
||||
```src
|
||||
[file]{knapsack}-[class]{}-[func]{knapsack_dfs_mem}
|
||||
```
|
||||
|
||||
На рисунке ниже показаны ветви поиска, которые были обрезаны при мемоизации поиска.
|
||||
|
||||

|
||||
|
||||
### Третий метод: динамическое программирование
|
||||
|
||||
Динамическое программирование по сути представляет собой процесс заполнения таблицы $dp$ при переходе состояний. Код приведен ниже:
|
||||
|
||||
```src
|
||||
[file]{knapsack}-[class]{}-[func]{knapsack_dp}
|
||||
```
|
||||
|
||||
Как показано на рисунке ниже, временная сложность и пространственная сложность определяются размером массива `dp`, то есть $O(n \times cap)$.
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||
|
||||
=== "<11>"
|
||||

|
||||
|
||||
=== "<12>"
|
||||

|
||||
|
||||
=== "<13>"
|
||||

|
||||
|
||||
=== "<14>"
|
||||

|
||||
|
||||
### Оптимизация пространства
|
||||
|
||||
Поскольку каждое состояние зависит только от состояний предыдущей строки, мы можем использовать два массива для последовательного продвижения, снижая пространственную сложность с $O(n^2)$ до $O(n)$.
|
||||
|
||||
Подумаем дальше: можем ли мы реализовать оптимизацию пространства, используя только один массив? Как видно, каждое состояние переходит из ячейки прямо сверху или слева сверху. Предположим, что есть только один массив: когда начинается обход $i$-й строки, этот массив все еще хранит состояния строки $i-1$.
|
||||
|
||||
- Если использовать прямой порядок обхода, то при достижении $dp[i, j]$ значения слева сверху $dp[i-1, 1]$ ~ $dp[i-1, j-1]$ могут быть уже перезаписаны, и в этом случае невозможно получить правильный результат перехода состояния.
|
||||
- Если использовать обратный порядок обхода, проблема перезаписи не возникнет, и переход состояния может выполняться корректно.
|
||||
|
||||
На рисунке ниже показан процесс перехода от строки $i = 1$ к строке $i = 2$ в одном массиве. Подумайте о разнице между прямым и обратным порядком обхода.
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
В реализации кода нам нужно только удалить первое измерение $i$ массива `dp` и изменить внутренний цикл на обратный порядок обхода:
|
||||
|
||||
```src
|
||||
[file]{knapsack}-[class]{}-[func]{knapsack_dp_comp}
|
||||
```
|
||||
Reference in New Issue
Block a user