0Pricing
Coding Interview Prep · Урок

Неограниченный рюкзак и размен монет 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 — локальная установка не требуется.

Все уроки этого курса

  1. Рюкзак 0/1 и оптимизация памяти
  2. Неограниченный рюкзак и размен монет II
  3. Разбиение на подмножества с равной суммой
  4. Целевая сумма с положительными и отрицательными знаками
← Назад к Coding Interview Prep