← Todos os algoritmos

0/1 Knapsack

Programação Dinâmica · ver código-fonte no GitHub

Leia em: English · Português · Español

Categoria: Dynamic Programming

O problema

Dado um conjunto de itens, cada um com um peso e um valor, e um orçamento de capacidade, escolha o subconjunto que maximiza o valor total sem exceder o orçamento — cada item é levado por inteiro ou não é levado (sem dividir um item entre o "0" e o "1" de levá-lo ou não, daí o nome). Testar diretamente cada subconjunto é O(2^n): mesmo para apenas 20-30 itens, isso já são dezenas de milhões a um bilhão de combinações para verificar.

A solução

Defina dp[i][w] como o melhor valor alcançável usando apenas os primeiros i itens dentro da capacidade w. Esse valor sempre depende apenas da linha acima: ou o item i é descartado (dp[i][w] = dp[i-1][w]), ou é levado, consumindo weight[i] da capacidade (dp[i][w] = dp[i-1][w - weight[i]] + value[i]) — usa-se o que for maior entre os dois. Preencher essa tabela de baixo para cima toca cada célula (item, capacity) uma única vez: O(n × capacity). Percorrendo a tabela pronta de trás para frente a partir de dp[n][capacity], comparando cada linha com a de cima para ver se a inclusão do valor daquele item foi o que fez a célula melhorar, recupera-se exatamente quais itens foram escolhidos — sem resolver nada de novo.

flowchart LR
    subgraph "dp[i][w] depends only on the row above"
        direction TB
        A["dp[i-1][w]  (skip item i)"]
        B["dp[i-1][w - weight(i)] + value(i)  (take item i)"]
        A --> C["dp[i][w] = max(A, B)"]
        B --> C
    end
Operação Custo Por quê
Preenchimento da tabela DP O(n × capacity) uma decisão de tempo constante por célula (item, capacity)
Recuperação dos itens (backtrack) O(n) uma comparação de linha por item, sem resolver de novo
Força bruta (todo subconjunto) O(2^n) nenhum subproblema compartilhado é reaproveitado — cada combinação é avaliada de forma independente

Exemplo clássico

classic/Knapsack implementa tanto o preenchimento da tabela DP com backtracking (solve, que retorna um KnapsackResult com o valor máximo e quais itens foram escolhidos) quanto uma força bruta recursiva direta (bruteForceMaxValue), usada abaixo pelo benchmark como referência de comparação. KnapsackTest cobre um caso com uma combinação ótima única (verificada em relação à seleção de itens, não apenas ao valor), capacidade zero, nenhum item, um único item que cabe, um único item que não cabe, a força bruta concordando com o resultado do DP na mesma entrada, e as proteções contra null/comprimentos incompatíveis/capacidade negativa.

Exemplo aplicado: seleção de projetos de capex em telecom

applied/CapexProjectSelector seleciona quais projetos de infraestrutura candidatos financiar a partir de um orçamento anual fixo de capex, maximizando o valor total projetado — o enquadramento de negócio clássico do 0/1 Knapsack: um projeto ou é financiado por completo ou não é financiado, não existe financiar 60% da implantação de uma rede de fibra, e o orçamento é a restrição rígida de capacidade. Listas reais de projetos costumam ser pequenas o suficiente para que o custo da tabela DP seja trivial na prática, mas o problema de seleção em si é exatamente tão combinatoriamente difícil quanto qualquer outra instância de Knapsack — escolher projetos "pelo melhor ROI primeiro" (um atalho guloso) não encontra de forma confiável a combinação ótima, ao contrário do que acontece com o módulo Coin Change deste repositório sobre denominações de moeda comuns. CapexProjectSelectorTest cobre a seleção da combinação de maior valor dentro do orçamento, uma lista de candidatos vazia, um orçamento zero, e a proteção contra argumento null.

Benchmark

./gradlew :dynamic-programming:knapsack:jmh

Execução real nesta máquina (JMH 1.37, JDK 26.0.2, 2 iterações de warmup + 3 de medição, 1 fork). A quantidade de itens permanece pequena — a força bruta com 22 itens já verifica mais de 4 milhões de subconjuntos, e qualquer valor maior tornaria este benchmark impraticavelmente lento:

Custo 15 itens 18 itens 22 itens
DP 2.94 µs 4.55 µs 6.66 µs
força bruta 79.53 µs 840.46 µs 13,111.05 µs

Com 22 itens, a força bruta é ~1,968x mais lenta que o DP para a mesma resposta. O próprio crescimento da força bruta confirma diretamente o formato exponencial: ir de 15 para 18 itens (3 a mais) custa ~10.6x mais tempo, e de 18 para 22 (4 a mais) custa ~15.6x mais — ambos próximos do crescimento 2^3 = 8 e 2^4 = 16 que 2^n prevê exatamente. O DP, por sua vez, cresce suavemente ao longo do mesmo intervalo — seu custo acompanha items × capacity, não 2^items.

Quando não usar

Cobertura de testes

100% de cobertura de instruções, 100% de cobertura de branches (JaCoCo). Reproduza você mesmo:

./gradlew :dynamic-programming:knapsack:jacocoTestReport

Relatório em dynamic-programming/knapsack/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

src/test/java/com/algorithms/dynamicprogramming/knapsack/classic/KnapsackTest.java
package com.algorithms.dynamicprogramming.knapsack.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class KnapsackTest {

    @Test
    void picksTheHighestValueCombinationWithinCapacity() {
        int[] weights = {1, 3, 4, 5};
        int[] values = {1, 4, 5, 7};

        KnapsackResult result = Knapsack.solve(weights, values, 7);

        assertThat(result.maxValue()).isEqualTo(9);
        assertThat(result.selected()).containsExactly(false, true, true, false);
    }

    @Test
    void zeroCapacityTakesNothing() {
        KnapsackResult result = Knapsack.solve(new int[] {2, 3}, new int[] {10, 20}, 0);

        assertThat(result.maxValue()).isZero();
        assertThat(result.selected()).containsExactly(false, false);
    }

    @Test
    void noItemsProducesZeroValue() {
        KnapsackResult result = Knapsack.solve(new int[0], new int[0], 10);

        assertThat(result.maxValue()).isZero();
        assertThat(result.selected()).isEmpty();
    }

    @Test
    void aSingleItemThatFitsIsTaken() {
        KnapsackResult result = Knapsack.solve(new int[] {5}, new int[] {10}, 5);

        assertThat(result.maxValue()).isEqualTo(10);
        assertThat(result.selected()).containsExactly(true);
    }

    @Test
    void aSingleItemThatDoesNotFitIsSkipped() {
        KnapsackResult result = Knapsack.solve(new int[] {10}, new int[] {10}, 5);

        assertThat(result.maxValue()).isZero();
        assertThat(result.selected()).containsExactly(false);
    }

    @Test
    void bruteForceAgreesWithTheDpSolutionOnTheSameInputs() {
        int[] weights = {1, 3, 4, 5};
        int[] values = {1, 4, 5, 7};

        int dpValue = Knapsack.solve(weights, values, 7).maxValue();
        int bruteForceValue = Knapsack.bruteForceMaxValue(weights, values, 7);

        assertThat(bruteForceValue).isEqualTo(dpValue);
    }

    @Test
    void rejectsNullWeightsOrValues() {
        assertThatThrownBy(() -> Knapsack.solve(null, new int[0], 1)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> Knapsack.solve(new int[0], null, 1)).isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsMismatchedArrayLengths() {
        assertThatThrownBy(() -> Knapsack.solve(new int[] {1, 2}, new int[] {1}, 5))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsANegativeCapacity() {
        assertThatThrownBy(() -> Knapsack.solve(new int[] {1}, new int[] {1}, -1))
                .isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/algorithms/dynamicprogramming/knapsack/applied/CapexProjectSelectorTest.java
package com.algorithms.dynamicprogramming.knapsack.applied;

import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class CapexProjectSelectorTest {

    @Test
    void selectsTheHighestValueCombinationWithinBudget() {
        CapexProjectSelector selector = new CapexProjectSelector();
        List<CapexProject> candidates = List.of(
                new CapexProject("fiber-north", 3, 4),
                new CapexProject("fiber-south", 4, 5),
                new CapexProject("tower-upgrade", 1, 1),
                new CapexProject("backup-power", 5, 7)
        );

        List<CapexProject> selected = selector.selectWithinBudget(candidates, 7);

        assertThat(selected).extracting(CapexProject::name).containsExactly("fiber-north", "fiber-south");
    }

    @Test
    void emptyCandidateListSelectsNothing() {
        CapexProjectSelector selector = new CapexProjectSelector();

        List<CapexProject> selected = selector.selectWithinBudget(List.of(), 100);

        assertThat(selected).isEmpty();
    }

    @Test
    void zeroBudgetSelectsNothing() {
        CapexProjectSelector selector = new CapexProjectSelector();
        List<CapexProject> candidates = List.of(new CapexProject("fiber-north", 3, 4));

        List<CapexProject> selected = selector.selectWithinBudget(candidates, 0);

        assertThat(selected).isEmpty();
    }

    @Test
    void rejectsANullCandidateList() {
        CapexProjectSelector selector = new CapexProjectSelector();

        assertThatThrownBy(() -> selector.selectWithinBudget(null, 10)).isInstanceOf(IllegalArgumentException.class);
    }
}

Ver relatório completo de cobertura JaCoCo →