Files
hello-algo/ru/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md
Yudong Jin 22b3b568ef fix Ru translation (#1894)
* docs(ru): replace prose quotes with guillemets

* docs(ru): replace prose semicolons with periods

* docs(ru): align animation title forms

* docs(ru): align figure and table references
2026-04-14 18:10:12 +08:00

15 KiB
Raw Blame History

Задача о полном рюкзаке

В этом разделе сначала решим еще одну распространенную задачу о рюкзаке - задачу о полном рюкзаке, а затем рассмотрим один из ее типичных частных случаев: задачу о размене монет.

Задача о полном рюкзаке

!!! question

Даны $n$ предметов. Вес $i$-го предмета равен $wgt[i-1]$ , стоимость равна $val[i-1]$ . Также дан рюкзак вместимости $cap$ . **Каждый предмет можно выбирать многократно**. Найдите максимальную суммарную стоимость, которую можно поместить в рюкзак при заданной вместимости. Пример показан на рисунке ниже.

Пример данных для задачи о полном рюкзаке

Идея динамического программирования

Задача о полном рюкзаке очень похожа на задачу о рюкзаке 0-1. Разница состоит только в том, что количество выборов каждого предмета не ограничено.

  • В задаче о рюкзаке 0-1 каждого предмета существует только один экземпляр, поэтому после того как предмет i помещен в рюкзак, выбирать можно только из первых i-1 предметов.
  • В задаче о полном рюкзаке количество предметов не ограничено, поэтому после того как предмет i помещен в рюкзак, можно продолжать выбирать из первых i предметов.

При этом состояние [i, c] в задаче о полном рюкзаке может изменяться двумя способами.

  • Не брать предмет $i$ : как и в задаче о рюкзаке 0-1, переход осуществляется в [i-1, c] .
  • Взять предмет $i$ : в отличие от рюкзака 0-1 переход происходит в [i, c-wgt[i-1]] .

Следовательно, уравнение перехода состояния принимает вид:


dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])

Реализация кода

Если сравнить код этой задачи с кодом задачи о рюкзаке 0-1, то окажется, что в переходе состояний меняется только одна деталь: вместо i-1 появляется i. Все остальное остается таким же:

[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp}

Оптимизация пространства

Поскольку текущее состояние переходит из состояния слева и состояния сверху, после оптимизации памяти каждую строку таблицы dp нужно обходить слева направо.

Этот порядок обхода как раз противоположен задаче о рюкзаке 0-1. Эту разницу удобно понять, рассмотрев то, что показано на рисунке ниже.

=== "<1>" Процесс динамического программирования после оптимизации памяти для полного рюкзака

=== "<2>" unbounded_knapsack_dp_comp_step2

=== "<3>" unbounded_knapsack_dp_comp_step3

=== "<4>" unbounded_knapsack_dp_comp_step4

=== "<5>" unbounded_knapsack_dp_comp_step5

=== "<6>" unbounded_knapsack_dp_comp_step6

Код реализации здесь довольно прост: достаточно просто убрать первое измерение массива dp :

[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp_comp}

Задача о размене монет

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

!!! question

Даны $n$ видов монет, номинал монеты $i$ равен $coins[i - 1]$ , а целевая сумма равна $amt$ . **Монеты каждого вида можно брать многократно**. Требуется найти минимальное число монет, которыми можно набрать целевую сумму. Если набрать сумму невозможно, верните $-1$ . Пример показан на рисунке ниже.

Пример данных для задачи о размене монет

Идея динамического программирования

Задачу о размене монет можно рассматривать как частный случай задачи о полном рюкзаке. Между ними существуют следующие соответствия и различия.

  • Эти две задачи можно взаимно преобразовать: «предмет» соответствует «монете», «вес предмета» соответствует «номиналу монеты», а «вместимость рюкзака» соответствует «целевой сумме».
  • Цель оптимизации противоположна: в задаче о полном рюкзаке нужно максимизировать стоимость предметов, а в задаче о размене монет - минимизировать число монет.
  • В задаче о полном рюкзаке ищется решение, не превышающее вместимость, а в задаче о размене монет требуется ровно набрать целевую сумму.

Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу $dp$

Подзадача, соответствующая состоянию [i, a] , выглядит так: минимальное число монет из первых i видов, которыми можно набрать сумму $a$. Решение этой подзадачи обозначается как dp[i, a] .

Размер двумерной таблицы dp равен (n+1) \times (amt+1) .

Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

По сравнению с задачей о полном рюкзаке здесь есть два отличия в уравнении перехода состояния.

  • Нужно искать минимум, а не максимум, поэтому оператор \max() заменяется на \min() .
  • Оптимизируемое значение - это число монет, а не суммарная стоимость, поэтому при выборе монеты нужно просто прибавить 1 .

dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)

Шаг 3: определить граничные условия и порядок переходов

Когда целевая сумма равна 0 , минимальное число монет для ее набора равно 0 , то есть весь первый столбец dp[i, 0] заполняется нулями.

Когда монет нет, невозможно набрать никакую целевую сумму $> 0$. Это и есть недопустимое решение. Чтобы функция \min() в уравнении перехода состояния могла распознавать и отбрасывать такие недопустимые решения, удобно использовать значение + \infty. То есть всю первую строку dp[0, a] нужно инициализировать значением + \infty .

Реализация кода

Большинство языков программирования не предоставляет представление для + \infty в целочисленном виде, поэтому обычно приходится заменять его на максимальное значение типа int . Но тогда возникает риск переполнения: операция + 1 в уравнении перехода может переполнить большое число.

Поэтому здесь мы используем число amt + 1 как обозначение недопустимого решения, потому что для набора суммы amt максимум нужно не больше чем amt монет. Перед возвратом результата проверяем, равно ли dp[n, amt] значению amt + 1. Если да, то возвращаем -1 , что означает невозможность набрать целевую сумму. Код приведен ниже:

[file]{coin_change}-[class]{}-[func]{coin_change_dp}

Как показано на рисунке ниже, процесс динамического программирования для задачи о размене монет очень похож на задачу о полном рюкзаке.

=== "<1>" Процесс динамического программирования для задачи о размене монет

=== "<2>" coin_change_dp_step2

=== "<3>" coin_change_dp_step3

=== "<4>" coin_change_dp_step4

=== "<5>" coin_change_dp_step5

=== "<6>" coin_change_dp_step6

=== "<7>" coin_change_dp_step7

=== "<8>" coin_change_dp_step8

=== "<9>" coin_change_dp_step9

=== "<10>" coin_change_dp_step10

=== "<11>" coin_change_dp_step11

=== "<12>" coin_change_dp_step12

=== "<13>" coin_change_dp_step13

=== "<14>" coin_change_dp_step14

=== "<15>" coin_change_dp_step15

Оптимизация пространства

Оптимизация пространства для задачи о размене монет выполняется так же, как и для полного рюкзака:

[file]{coin_change}-[class]{}-[func]{coin_change_dp_comp}

Задача о размене монет II

!!! question

Даны $n$ видов монет, номинал монеты $i$ равен $coins[i - 1]$ , а целевая сумма равна $amt$ . Монеты каждого вида можно брать многократно. **Найдите число различных комбинаций монет, которыми можно набрать целевую сумму**. Пример показан на рисунке ниже.

Пример данных для задачи о размене монет II

Идея динамического программирования

По сравнению с предыдущей задачей здесь целью является число комбинаций. Поэтому подзадача меняется на следующую: число комбинаций из первых i видов монет, которыми можно набрать сумму $a$. При этом таблица dp по-прежнему остается двумерной матрицей размера (n+1) \times (amt + 1) .

Число комбинаций для текущего состояния равно сумме числа комбинаций для двух решений: не брать текущую монету и брать текущую монету. Поэтому уравнение перехода состояния принимает вид:


dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]

Когда целевая сумма равна 0 , ее можно набрать, не выбирая ни одной монеты, поэтому весь первый столбец dp[i, 0] нужно инициализировать единицами. Когда монет нет, невозможно набрать никакую сумму >0 , поэтому вся первая строка dp[0, a] должна быть заполнена нулями.

Реализация кода

[file]{coin_change_ii}-[class]{}-[func]{coin_change_ii_dp}

Оптимизация пространства

При оптимизации памяти способ остается тем же самым: достаточно убрать измерение, отвечающее за виды монет:

[file]{coin_change_ii}-[class]{}-[func]{coin_change_ii_dp_comp}