Неограниченный рюкзак и размен монет II
Разрешите повторно использовать предметы, перебирая вместимость по возрастанию, и решите задачи о размене монет II — подсчёте способов — и о разрезании стержня с помощью этого варианта
«Неограниченный рюкзак и размен монет II» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.
Концепция неограниченного рюкзака
В задаче о неограниченном рюкзаке каждый элемент можно брать любое количество раз (в отличие от рюкзака 0/1, где каждый элемент используется не более одного раза). Определение состояния остаётся тем же: dp[c] = максимальная ценность, достижимая при вместимости c, — но направление перебора меняется. Поскольку элементы можно использовать повторно, при обновлении dp[c] нужно разрешить повторное использование текущего элемента, поэтому вместимость перебирается слева направо (в прямом порядке).
Прямой перебор позволяет повторно использовать элементы
Вспомните, что в задаче о рюкзаке 0/1 мы перебирали вместимость справа налево, чтобы не допустить повторного использования элементов. В неограниченном рюкзаке поступают наоборот: перебирают слева направо. При вычислении dp[c] значение dp[c-w] уже обновлено в текущем проходе — это означает, что элемент i уже мог быть включён. Именно этого мы и хотим: элемент i можно снова добавить в решение, которое уже содержит элемент i.
def unbounded_knapsack(weights, values, W):
dp = [0] * (W + 1)
for i in range(len(weights)):
w, v = weights[i], values[i]
for c in range(w, W + 1): # iterate LEFT TO RIGHT
dp[c] = max(dp[c], dp[c - w] + v)
return dp[W]
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
print(unbounded_knapsack(weights, values, 7)) # 9Задача о монетах II: подсчёт способов
Задача о монетах II формулируется так: по данным номиналам монет и сумме нужно подсчитать количество различных способов получить эту сумму (каждую монету можно использовать неограниченное число раз). Это вариант неограниченного рюкзака, в котором вместо максимизации ценности мы подсчитываем комбинации (combinations). Определим dp[c] как количество способов получить сумму c. Базовый случай: dp[0] = 1 (существует один способ получить 0: ничего не брать).
Реализация задачи о монетах II
Для каждой монеты перебирайте суммы слева направо и накапливайте результат: dp[c] += dp[c - coin]. Базовый случай dp[0] = 1 задаёт начальное количество способов. Обратите внимание: внешний цикл проходит по монетам, а внутренний — по суммам. Благодаря этому естественным образом подсчитываются комбинации (combinations), а не перестановки (permutations), поскольку каждый номинал монеты рассматривается ровно один раз во внешнем проходе.
def change(amount, coins):
dp = [0] * (amount + 1)
dp[0] = 1 # one way to make amount 0
for coin in coins:
for c in range(coin, amount + 1):
dp[c] += dp[c - coin]
return dp[amount]
print(change(5, [1, 2, 5])) # 4
print(change(3, [2])) # 0
print(change(10, [10])) # 1Комбинации и перестановки
Порядок циклов имеет критическое значение. Если во внешнем цикле перебирать сумму, а во внутреннем — номинал монеты, мы подсчитываем перестановки (permutations), где порядок важен. Для amount=5 с монетами [1,2] варианты 1+2+2 и 2+1+2 считаются отдельно. Если во внешнем цикле перебирать номиналы монет, мы подсчитываем комбинации (combinations), где порядок не важен: 1+2+2 и 2+1+2 считаются одним и тем же вариантом. В задаче о монетах II требуется подсчитывать комбинации, поэтому номиналы монет находятся во внешнем цикле.
# Count COMBINATIONS (order does not matter) — coin outer loop
def combinations(amount, coins):
dp = [0] * (amount + 1)
dp[0] = 1
for coin in coins: # coin outer
for c in range(coin, amount + 1):
dp[c] += dp[c - coin]
return dp[amount]
# Count PERMUTATIONS (order matters) — amount outer loop
def permutations(amount, coins):
dp = [0] * (amount + 1)
dp[0] = 1
for c in range(1, amount + 1): # amount outer
for coin in coins:
if c >= coin:
dp[c] += dp[c - coin]
return dp[amount]
print(combinations(5, [1,2,5])) # 4
print(permutations(5, [1,2,5])) # 13Задача о распиле стержня
Ещё одна классическая задача о неограниченном рюкзаке: дан стержень длины n и цены для каждой длины стержня от 1 до n. Нужно найти максимальную выручку, оптимально распилив стержень. Каждый отрезок длины l можно продать по цене price[l], а отрезки можно использовать повторно (стержень можно распилить на несколько частей одинаковой длины). Это напрямую сводится к задаче о неограниченном рюкзаке, где W = n, а элементами являются различные длины отрезков.
def rod_cutting(prices, n):
# prices[i] = price of rod of length i+1
dp = [0] * (n + 1)
for length in range(1, n + 1): # each cut length
price = prices[length - 1]
for c in range(length, n + 1):
dp[c] = max(dp[c], dp[c - length] + price)
return dp[n]
prices = [1, 5, 8, 9, 10, 17, 17, 20]
print(rod_cutting(prices, 8)) # 22Задача о монетах I: минимальное количество монет
Задача о монетах I — это другая задача, в которой требуется найти минимальное количество монет для получения целевой суммы. Здесь dp[c] = минимальное количество монет для получения суммы c. Рекуррентное соотношение: dp[c] = min(dp[c], dp[c - coin] + 1). Инициализируйте все элементы значением inf, кроме dp[0] = 0. Это также задача о неограниченном рюкзаке (монеты можно использовать повторно), поэтому суммы перебираются слева направо. Верните dp[amount], если значение конечно, иначе верните -1.
def coinChange(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for c in range(coin, amount + 1):
dp[c] = min(dp[c], dp[c - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
print(coinChange([1,5,6,9], 11)) # 2 (5+6 or other combos)
print(coinChange([2], 3)) # -1Ключевое различие: максимум, минимум и количество
В трёх вариантах задачи о неограниченном рюкзаке используются разные операции над dp[c-coin]: максимизировать ценность: dp[c] = max(dp[c], dp[c-w] + v); инициализировать нулём. минимизировать стоимость: dp[c] = min(dp[c], dp[c-coin] + 1); инициализировать значением inf, dp[0]=0. подсчитывать количество способов: dp[c] += dp[c-coin]; инициализировать нулём, dp[0]=1. Распознать, какой вариант перед Вами, — это половина успеха в задачах на собеседованиях.
Сложность и советы для собеседования
Все варианты задачи о неограниченном рюкзаке работают за O(n × W) времени и O(W) памяти, где n — количество типов элементов, а W — целевая сумма. В задачах о монетах n — это количество номиналов монет. На собеседовании назовите вариант (максимум, минимум или количество), запишите одномерный DP и чётко укажите, во внешнем цикле перебираются монеты или сумма: интервьюеры знают, что это различие проверяет глубокое понимание DP.
Как отличить неограниченный рюкзак от рюкзака 0/1
Используйте следующие признаки, чтобы определить подходящий вариант: неограниченное повторное использование → неограниченный рюкзак (перебор слева направо); каждый элемент используется ровно один раз → рюкзак 0/1 (перебор справа налево); в условии сказано «любое количество раз», «неограниченный запас» или «повторное использование разрешено» → неограниченный рюкзак. Примеры: задача о монетах, задача о распиле стержня, разбиение целого числа — всё это задачи о неограниченном рюкзаке. Задача о сумме подмножества, разбиение и рюкзак 0/1 — задачи 0/1. Ошибка в этом выборе приводит к неправильным ответам, которые трудно отлаживать.
Разбиение целого числа и другие варианты
Разбиение целого числа (LeetCode 343): разделите целое число n на как минимум 2 положительных целых числа так, чтобы максимизировать их произведение. Это задача о неограниченном рюкзаке, где «элементами» являются целые числа от 2 до n-1. Определим dp[i] = максимальное произведение целых чисел, сумма которых равна i. Для каждого элемента j от 2 до i: dp[i] = max(dp[i], max(j, dp[j]) * max(i-j, dp[i-j])). Этот пример показывает, как шаблон неограниченного рюкзака обобщается за пределы задач о монетах.
def integerBreak(n):
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
for j in range(1, i):
dp[i] = max(dp[i], max(j, dp[j]) * max(i-j, dp[i-j]))
return dp[n]
print(integerBreak(10)) # 36 (3+3+4 = 3*3*4 = 36)Быстрая проверка
Проверьте, насколько Вы усвоили понятия из курса «Структуры данных & алгоритмы — подготовка к собеседованию» по этой теме.
Итоги урока
В этом уроке Вы узнали: в неограниченном рюкзаке вместимость перебирается слева направо, чтобы разрешить повторное использование элементов, задача о монетах II подсчитывает комбинации (combinations), помещая монету во внешний цикл, и три варианта — максимизация, минимизация и подсчёт — различаются только операцией DP и инициализацией. Далее мы используем рюкзак 0/1 для решения задачи о разбиении на подмножества с равными суммами.
Часто задаваемые вопросы
Урок «Неограниченный рюкзак и размен монет II» бесплатный?
Да — полный текст урока «Неограниченный рюкзак и размен монет II» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Неограниченный рюкзак и размен монет II»?
Разрешите повторно использовать предметы, перебирая вместимость по возрастанию, и решите задачи о размене монет II — подсчёте способов — и о разрезании стержня с помощью этого варианта Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Неограниченный рюкзак и размен монет II»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Рюкзак 0/1 и оптимизация памяти
- Неограниченный рюкзак и размен монет II
- Разбиение на подмножества с равной суммой
- Целевая сумма с положительными и отрицательными знаками