Динамическое программирование

Задача о рюкзаке

Дано N предметов массой m1, …, mN и стоимостью c1, …, cN соответственно. Ими наполняют рюкзак, который выдерживает вес не более M. Какую наибольшую стоимость могут иметь предметы в рюкзаке?

Эта задача является частным случаем задачи об укладке рюкзака, которая в общем случае формулируется, как:
Дано n предметов, i-й предмет имеет массу weighti > 0 и стоимость costi > 0. Необходимо выбрать из этих предметов такой набор, чтобы суммарная масса не превосходила заданной величины M (вместимость рюкзака), а суммарная стоимость была максимальна. Другими словами, нужно определить набор бинарных величин (b1, b2,..., bn), при котором выполняется соотношение:

\[\begin{aligned} b_1 \cdot weight_1 & + b_2 \cdot weight_2 + \cdots + b_n \cdot weight_n \le M \\ \text{и сумма } b_1 \cdot cost_1 & + b_2 \cdot cost_2 + \cdots + b_n \cdot cost_n \text{ максимальна} \end{aligned} \]


Введем функцию A. A(n, m) - максимальная стоимость предметов, которые можно уложить в рюкзак максимальной вместимости m, если можно использовать только первые n предметов из заданных N.
Зададим краевые значения для A(n, m)
Если ни один предмет брать нельзя, т.е. n = 0, то A(0, m) = 0, для любого m.
Если вместимость рюкзака равна 0, т.е. m = 0 , то A(n, 0) = 0, для любого n. \[\begin{aligned} n=0, \text{тогда } \forall m \quad & \Rightarrow A(0, m) = 0 \\ m = 0, \text{тогда } \forall n \quad & \Rightarrow A(n, 0) = 0 \end{aligned} \]

Теперь составим рекуррентное соотношение. Необходимо из предметов с номерами 1, ..., n составить рюкзак максимальной стоимости, чей вес не превышает m. При этом возможно два случая: когда в максимальный рюкзак включен предмет с номером n и когда предмет n не попал в максимальный рюкзак.

Если предмет n не попал в максимальный рюкзак массы m, то максимальный рюкзак будет составлен только из предметов с номерами 1, ..., n - 1, следовательно, A(n, m) = A(n - 1, m).

Если же в максимальный рюкзак включен предмет n, то масса оставшихся предметов не превышает m - weightn, а от добавления предмета n общая стоимость рюкзака увеличивается на costn. Значит, A(n, m) = A(n - 1, m - weightn) + costn. Теперь из двух возможных вариантов составить рюкзак массы, не превосходящей m, из предметов 1, ..., n нужно выбрать наилучший, т.е. \[ max(A(n - 1, m),\quad A(n - 1, m - weight_n) + cost_n) \]
В программе веса предметов считаем в список weight, а стоимости в список cost. Т.к. принято индексировать массивы начиная с индекса 0, то чтобы у нас weight[1] и cost[1] соответствовали предмету с номером 1, то после считывания добавим фиктивный предмет с весом 0 и стоимостью 0. Значения функции A(n,m) будем хранить в списке A[n][m]. Ответ на задачу будет находится в элементе A[N][M].

Примерный код программы:

        
        N, M = map(int, input().split()) # N - количество предметов
                                         # M - предельная масса
        weight = [int(i) for i in input().split()] # веса
        cost = [int(i) for i in input().split()]   # стоимости
        weight.insert(0, 0)
        cost.insert(0, 0)
        A = []
        for i in range(N+1):
            A.append([0]*(M+1))
        
        for n in range(1, N+1):
            for m in range(M+1):
                A[n][m] = A[n-1][m]
                if (m >= weight[n] and A[n-1][m-weight[n]] + cost[n] > A[n][m]):
                    A[n][m] =  A[n-1][m-weight[n]] + cost[n]
        
        print(A[N][M])
        
        

Рассмотрим пример:

        
        4 6
        2 4 1 2
        7 2 5 1
        
        
Предельный вес (m)
Кол-во предметов 0 1 2 3 4 5 6
weight = 0 cost = 0 n = 0 0 0 0 0 0 0 0
weight = 2 cost = 7 n = 1 0 0 7 7 7 7 7
weight = 4 cost = 2 n = 2 0 0 7 7 7 7 9
weight = 1 cost = 5 n = 3 0 5 7 12 12 12 12
weight = 2 cost = 1 n = 4 0 5 7 12 12 13 13

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

        
        def print_answer(n, m):
            # n предметов
            # m предельная масса
            if A[n][m] == 0:
                return
            elif A[n-1][m] == A[n][m]:
                print_answer(n-1, m)
            else:
                print_answer(n-1, m - weight[n])
                print(n, end = " ")
        
        

Банкомат

В обороте находятся банкноты k различных номиналов: a1, a2, ..., ak рублей. Банкомат должен выдать сумму в N рублей при помощи минимального количества банкнот или сообщить, что запрашиваемую сумму выдать нельзя. Будем считать, что запасы банкнот каждого номинала неограничены.

Пусть F(n) -- минимальное количество банкнот, которым можно заплатить сумму в n рублей. Очевидно, что F(0) = 0, F(a1) = F(a2) =...= F(ak) = 1. Если некоторую сумму n невозможно выдать, будем считать, что F(n) = (бесконечность).
Выведем рекуррентную формулу для F(n), считая, что значения F(0), F(1), ..., F(n - 1) уже вычислены. Как можно выдать сумму n? Мы можем выдать сумму n - a1, а потом добавить одну банкноту номиналом a1. Тогда нам понадобится F(n - a1) + 1 банкнота. Можем выдать сумму n - a2 и добавить одну банкноту номиналом a2, для такого способа понадобится F(n - a2) + 1 банкнота и т. д. Из всевозможных способов выберем наилучший, то есть:

\[ F(n) = min(F(n - a_1), F(n - a_2),\cdots, F(n - a_k)) + 1 \]

В итоге в элементе F[S] - будет ответ, т.е. минимальное количество банкнот, которыми можно выдать номинал S.

Запишем код программы:

        
        N = int(input()) # количество номиналов банкнот в обращении
        nominal = [int(i) for i in input().split()] # номиналы банкнот
        S = int(input()) # сумма, которую нужно выдать
        
        F = []
        F.append(0)
        for m in range(1, S+1):
            F.insert(m, float("+inf"))
            for i in range(len(nominal)):
                if m >= nominal[i] and F[m-nominal[i]] + 1 < F[m]:
                    F[m] = F[m - nominal[i]] + 1
        print(F[S])
        
        

Если нам нужно вывести еще и сами номиналы банкнот, как это можно сделать? Рассмотрим все номиналы банкнот и значения n - a1, n - a2, ..., n - ak. Если для какого-то i окажется, что F(n - ai) = F(n) - 1, значит, мы можем выдать банкноту в ai рублей и после этого свести задачу к выдаче суммы n - ai, и так будем продолжать этот процесс, пока величина выдаваемой суммы не станет равна 0.

Ссылки на материалы по теме

Задачи