Otimização de Problemas de Mochila usando Programação Dinâmica

Mochila 0/1 (0/1 Knapsack)

O problema da Mochila 0/1 restringe a seleção de cada item a, no máximo, uma única vez. Embora possa ser resolvido utilizando uma matriz bidimensional para rastrear os estados, é possível otimizar o consumo de memória reduzindo a estrutura para um array unidimensional.

A chave para a otimização unidimensional reside na ordem de iteração da capacidade da mochila. Ao percorrer a capacidade de forma decrescente (do valor máximo até o peso do item atual), garantimos que o estado atual seja calculado com base nos resultados da iteração anterior do item, impedindo que o mesmo item seja incluído mais de uma vez.

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
    int max_capacity, item_count;
    if (!(cin >> max_capacity >> item_count)) return 0;

    vector<int> weights(item_count), values(item_count);
    for (int i = 0; i < item_count; ++i) {
        cin >> weights[i] >> values[i];
    }

    // Inicializado com -1 para indicar estados inalcançáveis, exceto a capacidade 0
    vector<int> dp(max_capacity + 1, -1);
    dp[0] = 0;

    for (int i = 0; i < item_count; ++i) {
        // Iteração decrescente para garantir a propriedade 0/1
        for (int c = max_capacity; c >= weights[i]; --c) {
            if (dp[c - weights[i]] != -1) {
                dp[c] = max(dp[c], dp[c - weights[i]] + values[i]);
            }
        }
    }

    int best_value = -1;
    for (int c = 0; c <= max_capacity; ++c) {
        best_value = max(best_value, dp[c]);
    }

    cout << best_value << "\n";
    return 0;
}

Mochila Ilimitada (Unbounded Knapsack)

Na variação ilimitada, cada tipo de item pode ser selecionado um número infinito de vezes. A estrutura de programação dinâmica é quase idêntica à da Mochila 0/1, com uma diferença fundamental na direção do loop da capacidade.

Para permitir a reutilização do mesmo item, a capacidade deve ser iterada de forma crescente (do peso do item até a capacidade máxima). Dessa forma, ao calcular o estado atual, o algoritmo pode utilizar um estado que já incluiu o item corrente na mesma iteração.

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
    int max_capacity, item_count;
    if (!(cin >> max_capacity >> item_count)) return 0;

    vector<int> weights(item_count), values(item_count);
    for (int i = 0; i < item_count; ++i) {
        cin >> weights[i] >> values[i];
    }

    vector<int> dp(max_capacity + 1, 0);

    for (int i = 0; i < item_count; ++i) {
        // Iteração crescente para permitir múltiplas escolhas do mesmo item
        for (int c = weights[i]; c <= max_capacity; ++c) {
            dp[c] = max(dp[c], dp[c - weights[i]] + values[i]);
        }
    }

    cout << dp[max_capacity] << "\n";
    return 0;
}

Mochila Limitada (Bounded Knapsack)

Neste modelo, cada item possui uma quantidade máxima disponível. A abordagem mais ingênua consiste em tratar cada unidade do item como um item individual, o que resulta em uma complexidade de tempo excessiva. Para otimizar esse processo, utiliza-se a Decomposição Binária.

A decomposição binária baseia-se no princípio de que qualquer número inteiro pode ser representado como a soma de potências de 2 (ex: $2^0, 2^1, 2^2, \dots$). Em vez de processar $C_i$ unidades individualmante, agrupamos as unidades em pacotes de tamanhos $1, 2, 4, \dots, 2^k$ e o restante. Isso reduz a complexidade de $O(C_i)$ para $O(\log C_i)$ por tipo de item, mantendo a capacidade de formar qualquer quantidade até o limite original.

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
    int max_capacity, item_count;
    if (!(cin >> max_capacity >> item_count)) return 0;

    vector<int> weights(item_count), values(item_count), counts(item_count);
    for (int i = 0; i < item_count; ++i) {
        cin >> weights[i] >> values[i] >> counts[i];
    }

    vector<int> dp(max_capacity + 1, 0);

    for (int i = 0; i < item_count; ++i) {
        int remaining = counts[i];
        int k = 1;
        
        // Decomposição binária das quantidades
        while (remaining > 0) {
            int current_count = min(k, remaining);
            int w = weights[i] * current_count;
            int v = values[i] * current_count;
            
            // Aplica a lógica da mochila 0/1 para os pacotes criados
            for (int c = max_capacity; c >= w; --c) {
                dp[c] = max(dp[c], dp[c - w] + v);
            }
            
            remaining -= current_count;
            k *= 2;
        }
    }

    cout << dp[max_capacity] << "\n";
    return 0;
}

Mochila Agrupada (Grouped Knapsack)

No problema da Mochila Agrupada, os itens são divididos em grupos distintos. A restrição principal é que, no máximo, um único item pode ser selecionado de cada grupo. A ordem dos loops de iteração é o aspecto mais crítico desta variação.

A estrutura correta exige que o loop mais externo itere sobre os grupos, o loop intermediário itere sobre a capacidade da mochila (em ordem decrescente) e o loop mais interno itere sobre os itens dentro do grupo atual. Se a capacidade e os itens do grupo forem invertidos, o algoritmo acabaria permitindo a seleção de múltiplos itens do mesmo grupo, violando a restrição do problema. Na perspectiva da programação dinâmica, o grupo representa o "estágio", a capacidade e o grupo formam o "estado", e a escolha do item é a "decisão".

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
    int max_capacity, group_count;
    if (!(cin >> group_count >> max_capacity)) return 0;

    vector<vector<int>> weights(group_count), values(group_count);
    for (int i = 0; i < group_count; ++i) {
        int items_in_group;
        cin >> items_in_group;
        weights[i].resize(items_in_group);
        values[i].resize(items_in_group);
        for (int j = 0; j < items_in_group; ++j) {
            cin >> weights[i][j] >> values[i][j];
        }
    }

    vector<int> dp(max_capacity + 1, 0);

    for (int i = 0; i < group_count; ++i) {
        // Capacidade em ordem decrescente
        for (int c = max_capacity; c >= 0; --c) {
            // Iteração sobre os itens do grupo atual
            for (int j = 0; j < weights[i].size(); ++j) {
                if (c >= weights[i][j]) {
                    dp[c] = max(dp[c], dp[c - weights[i][j]] + values[i][j]);
                }
            }
        }
    }

    cout << dp[max_capacity] << "\n";
    return 0;
}

Mochila em Árvore (Tree Knapsack)

A Mochila em Árvore é uma extensão avançada onde a seleção de itens possui dependências hierárquicas estruturadas em forma de árvore. Um exemplo clássico é o problema de seleção de cursos, onde um curso pré-requisito deve ser selecionado antes de seus cursos dependentes. A resolução deste modelo geralmente combina a travessia em árvore (como pós-ordem) com a tabela de programação dinâmica da mochila, fundindo a estrutura de dados da árvore com as transições de estado da capacidade.

Tags: programação-dinâmica problema-da-mochila Algoritmos cplusplus otimizacao-combinatoria

Publicado em 6-11 01:19 por Thomas