Subset Sum: Um Esquema de Aproximação Totalmente Polinomial
Objetivo
Os concepts irmãos approximation-algorithms-vertex-cover (Seções 35.1-35.2) e set-covering-and-lp-rounding (Seções 35.3-35.4) entregam todos algoritmos de aproximação com uma razão fixa embutida no algoritmo: 2, ou O(lg |X|), ou 8/7. A Seção 35.5 fecha o capítulo com algo qualitativamente diferente — um esquema de aproximação, onde a precisão é um parâmetro de entrada ε que quem chama pode ajustar, e o tempo de execução do algoritmo é polinomial tanto no tamanho da entrada quanto em 1/ε. Isso é um esquema de aproximação totalmente polinomial (FPTAS), e Cormen et al. constroem um para a versão de otimização do problema de subset sum: dado um conjunto S de inteiros positivos e um alvo t, encontre um subconjunto cuja soma seja a maior possível sem exceder t. A versão de decisão é NP-completa (provada na Seção 34.5.5 e coberta pelo concept irmão np-completeness-proofs-and-problem-catalog), então o caminho é: começar de um algoritmo exato que constrói incrementalmente a lista de todas as somas de subconjunto alcançáveis — uma abordagem no mesmo espírito de recorrência incremental do concept irmão dynamic-programming-fundamentals, mas exponencial aqui porque a própria lista pode dobrar a cada passo — e depois torná-lo polinomial recortando a lista, descartando deliberadamente qualquer valor que outro valor sobrevivente já aproxime de perto o suficiente.
Casos de Uso
- Resolver a forma prática de otimização de subset sum: o enquadramento da fonte é um caminhão que carrega no máximo
tlibras e aténcaixas com pesosx1, ..., xn, e a pergunta é quão pesada uma carga ele pode levar sem exceder o limite. - Recorrer a
EXACT-SUBSET-SUMsem modificação quando os números cooperam — a fonte observa que ele é genuinamente polinomial nos casos especiais em queté polinomial em|S|ou todos os números emSsão limitados por um polinômio em|S|. Só quando os valores são grandes o crescimento exponencial da lista morde. - Trocar precisão por tempo numa escala contínua em vez de aceitar uma razão fixa:
APPROX-SUBSET-SUMrecebeεcom0 < ε < 1e garante que o valor retornado está dentro de um fator de1 + εdo ótimo, então o mesmo código cobre "aproximadamente certo e rápido" e "quase exato e mais lento". - Reconhecer o formato FPTAS quando ele aparece em outro lugar: uma família de algoritmos indexada por
ε, com tempo de execução polinomial em1/εalém de no tamanho da entrada — em oposição a um esquema que só é polinomial no tamanho da entrada para cadaεfixo. - Aprender o recorte de listas (trimming) como técnica reutilizável: sempre que um espaço de estados é uma lista de valores numéricos e quase-duplicatas são inofensivas, manter um representante por faixa multiplicativa colapsa uma lista exponencial numa de tamanho logarítmico.
Aprofundamento
O algoritmo exato: mantenha a lista de todas as somas alcançáveis, limitada a t
Seja Pi o conjunto de valores obtidos somando os membros de cada subconjunto (possivelmente vazio) de {x1, x2, ..., xi}. Para S = {1, 4, 5} isso dá P1 = {0, 1}, P2 = {0, 1, 4, 5}, e P3 = {0, 1, 4, 5, 6, 9, 10}. Todo o algoritmo exato repousa sobre uma identidade:
Pi = P(i-1) ∪ (P(i-1) + xi)
onde L + x significa a lista construída somando x a cada elemento de L — por exemplo, se L = ⟨1, 2, 3, 5, 9⟩ então L + 2 = ⟨3, 4, 5, 7, 11⟩. A outra observação que torna o algoritmo prático é que, uma vez que a soma de um subconjunto excede t, nenhum superconjunto dele pode nunca ser ótimo, então pode ser descartado imediatamente.
java// Tradução fiel de EXACT-SUBSET-SUM(S, n, t) (CLRS, Seção 35.5).
// Li é a lista ordenada de somas de subconjuntos de {x1..xi} que não excedem t.
long exactSubsetSum(long[] x, int n, long t) {
List<Long> l = new ArrayList<>(List.of(0L)); // linha 1: L0 = <0>
for (int i = 1; i <= n; i++) { // linha 2
l = mergeLists(l, addToEach(l, x[i - 1])); // linha 3: merge, duplicatas removidas
removeGreaterThan(l, t); // linha 4
}
return l.get(l.size() - 1); // linha 5: maior elemento em Ln
}MERGE-LISTS(L, L') é o merge do merge-sort com duplicatas descartadas, rodando em tempo proporcional ao comprimento combinado, então mantém cada Li ordenada. Por indução em i (Exercício 35.5-1), Li é exatamente a lista ordenada de todo elemento de Pi que é no máximo t. O problema é o comprimento: |Li| pode chegar a 2^i, então EXACT-SUBSET-SUM é exponencial em geral.
TRIM: mantenha um representante por faixa multiplicativa
A chave para transformar isso num FPTAS é recortar (trim) cada Li logo após construí-la. A ideia: se dois valores numa lista estão próximos um do outro, não vale a pena manter os dois quando o objetivo é só uma resposta aproximada. Dado um parâmetro de corte δ com 0 < δ < 1, recortar uma lista L remove o máximo de elementos possível tal que todo elemento removido y ainda tenha um elemento sobrevivente z representando-o, significando que z não é maior que y, mas está dentro de um fator de 1 + δ:
y / (1 + δ) ≤ z ≤ y
Como todo valor sobrevivente já era um elemento real da lista original, o recorte só pode tornar a resposta menor, nunca ilegal — uma propriedade crucial para a prova de corretude mais adiante.
java// Tradução fiel de TRIM(L, delta) (CLRS, Seção 35.5).
// L precisa estar ordenada em ordem crescente. Roda em tempo Theta(m).
List<Long> trim(List<Long> l, double delta) {
int m = l.size();
List<Long> out = new ArrayList<>();
out.add(l.get(0)); // linha 2: L' = <y1>
long last = l.get(0); // linha 3
for (int i = 1; i < m; i++) { // linha 4: i = 2 até m
long yi = l.get(i);
if (yi > last * (1 + delta)) { // linha 5: yi >= last, L está ordenada
out.add(yi); // linha 6
last = yi; // linha 7
}
}
return out; // linha 8
}O procedimento faz uma única passada crescente, anexando um valor só quando é o primeiro elemento ou quando o valor mais recente colocado na saída não consegue representá-lo. O trace abaixo é o próprio exemplo de recorte da fonte: L = ⟨10, 11, 12, 15, 20, 21, 22, 23, 24, 29⟩ com δ = 0.1. Cada token é um elemento de L, e desaparece da linha no momento em que TRIM decide não anexá-lo:
A lista sobrevivente é L' = ⟨10, 12, 15, 20, 23, 29⟩: 11 é representado por 10, tanto 21 quanto 22 por 20, e 24 por 23. Dez elementos viraram seis, e todo valor descartado ainda tem um substituto próximo, ligeiramente menor.
APPROX-SUBSET-SUM: algoritmo exato mais um recorte por iteração
java// Tradução fiel de APPROX-SUBSET-SUM(S, n, t, eps) (CLRS, Seção 35.5).
// Requer 0 < eps < 1. Retorna um valor dentro de um fator de (1 + eps) do ótimo.
long approxSubsetSum(long[] x, int n, long t, double eps) {
List<Long> l = new ArrayList<>(List.of(0L)); // linha 1: L0 = <0>
for (int i = 1; i <= n; i++) { // linha 2
l = mergeLists(l, addToEach(l, x[i - 1])); // linha 3
l = trim(l, eps / (2.0 * n)); // linha 4: note eps/2n, NÃO eps
removeGreaterThan(l, t); // linha 5
}
return l.get(l.size() - 1); // linhas 6-7: maior valor em Ln
}A única diferença estrutural em relação a EXACT-SUBSET-SUM é a linha 4. O parâmetro de recorte é ε/2n em vez de ε precisamente porque o recorte acontece n vezes e as imprecisões se acumulam: cada passada pode reduzir os valores sobreviventes por um fator de 1 + ε/2n, então depois de n passadas o dano é (1 + ε/2n)^n, e encolher o parâmetro por passada por 2n é o que mantém esse produto abaixo de 1 + ε.
Aqui está a própria instância trabalhada pela fonte: S = ⟨104, 102, 201, 101⟩, t = 308, ε = 0.40, então o parâmetro de recorte é δ = ε/2n = 0.40/8 = 0.05.
| i | linha 3 — merge de L(i-1) com L(i-1) + xi |
linha 4 — recorte por 0.05 | linha 5 — descarte valores > 308 |
|---|---|---|---|
1 (x1 = 104) |
⟨0, 104⟩ |
⟨0, 104⟩ |
⟨0, 104⟩ |
2 (x2 = 102) |
⟨0, 102, 104, 206⟩ |
⟨0, 102, 206⟩ |
⟨0, 102, 206⟩ |
3 (x3 = 201) |
⟨0, 102, 201, 206, 303, 407⟩ |
⟨0, 102, 201, 303, 407⟩ |
⟨0, 102, 201, 303⟩ |
4 (x4 = 101) |
⟨0, 101, 102, 201, 203, 302, 303, 404⟩ |
⟨0, 101, 201, 302, 404⟩ |
⟨0, 101, 201, 302⟩ |
Lendo os recortes: em i = 2, 104 é descartado porque está dentro de um fator de 1.05 de 102. Em i = 3, 206 é descartado por estar dentro de 1.05 de 201, e 407 sobrevive ao recorte só para ser cortado pela linha 5 por exceder t = 308. Em i = 4, 102 é descartado (dentro de 1.05 de 101), 203 é descartado (dentro de 1.05 de 201), e 303 é descartado (dentro de 1.05 de 302).
O procedimento retorna z = 302. O verdadeiro ótimo é 307 = 104 + 102 + 101, então a resposta está dentro de 2% — confortavelmente dentro da margem prometida de ε = 40%. Essa diferença entre a garantia e o erro observado é típica: o limite é de pior caso.
Por que é totalmente polinomial: o limite de precisão e o limite de comprimento da lista
Teorema 35.7: APPROX-SUBSET-SUM é um esquema de aproximação totalmente polinomial para o problema de subset sum. Duas coisas precisam ser provadas.
A resposta é precisa. Tanto a linha 4 quanto a linha 5 só deletam elementos, nunca inventam nenhum, então todo elemento de Li é um membro genuíno de Pi — o z retornado é verdadeiramente a soma de algum subconjunto de S, e a linha 5 garante z ≤ t. Seja y* o valor ótimo (o maior elemento de Pn que é no máximo t); então z ≤ y*, e o que resta é mostrar que y*/z ≤ 1 + ε.
- O Exercício 35.5-2 estabelece por indução em
ique para todoy ∈ Picomy ≤ t, algumz ∈ Lisobrevivente satisfazy / (1 + ε/2n)^i ≤ z ≤ y— isto é, um recorte deε/2npor iteração, acumuladoivezes. - Aplicando isso a
y*emi = ndá um elemento deLncomy*/z ≤ (1 + ε/2n)^n, e como ozretornado é o maior elemento deLn, o mesmo limite vale para ele. - Resta mostrar que
(1 + ε/2n)^n ≤ 1 + ε. A função(1 + ε/2n)^né crescente emn(Exercício 35.5-3) e se aproxima de seu limitee^(ε/2), então é limitada pore^(ε/2) ≤ 1 + ε/2 + (ε/2)². Como0 < ε < 1força(ε/2)² ≤ ε/2, essa última expressão é no máximo1 + ε.
O tempo de execução é polinomial no tamanho da entrada e em 1/ε. Depois do recorte, quaisquer dois elementos sucessivos z e z' de Li precisam diferir por um fator maior que 1 + ε/2n — isso é exatamente o que sobreviver ao recorte significa. Então cada lista contém o valor 0, possivelmente o valor 1, e no máximo log_(1+ε/2n) t valores adicionais, dando um limite de comprimento de
log_(1+ε/2n) t + 2 = ln t / ln(1 + ε/2n) + 2 ≤ 2n(1 + ε/2n)·ln t / ε + 2 < 3n·ln t / ε + 2
usando 0 < ε < 1 no último passo. Esse limite é polinomial em 1/ε e no tamanho da entrada — que é os lg t bits necessários para escrever t mais os bits necessários para escrever S, ele próprio polinomial em n. Como o tempo de execução é polinomial nos comprimentos das listas, o esquema inteiro é totalmente polinomial. É esse o cerne: o recorte converte uma lista que poderia guardar 2^i valores em uma que guarda O(n·ln t / ε) valores, e o erro acumulado permanece limitado porque o parâmetro por recorte foi dividido por 2n desde o início.
Trade-offs
- Um esquema, não um algoritmo — diferente dos algoritmos de razão fixa dos concepts irmãos, quem chama escolhe a precisão. Essa flexibilidade tem um preço:
1/εestá no limite do tempo de execução (3n·ln t / ε + 2elementos por lista), então reduzir pela metade o erro permitido aproximadamente dobra o trabalho. Perseguir umεmuito pequeno vai reconduzindo de volta ao algoritmo exato exponencial. - A resposta recortada é sempre uma subestimativa, nunca uma superestimativa — o recorte mantém um representante
z ≤ ypara todoydescartado, e a linha 5 impõez ≤ t. Então o valor retornado é uma soma de subconjunto genuinamente alcançável que nunca excede o alvo, exatamente o que o enquadramento de carregamento do caminhão precisa. O Exercício 35.5-4 pergunta como modificar o esquema para aproximar a menor soma de subconjunto não menor quet. - Você obtém um valor, não um subconjunto — como está escrito,
APPROX-SUBSET-SUMretorna só o númeroz. Recuperar quais elementos somam a ele exige contabilidade extra (Exercício 35.5-5), da mesma forma que uma tabela de programação dinâmica entrega o custo ótimo antes de entregar a solução ótima. - O algoritmo exato às vezes já é bom o suficiente —
EXACT-SUBSET-SUMroda em tempo polinomial sempre queté polinomial em|S|ou todos os números de entrada são polinomialmente limitados. O FPTAS só compensa quando os valores numéricos são genuinamente grandes, que é precisamente o regime onde a codificação binária da entrada torna o problema NP-completo em primeiro lugar. - A escolha de
ε/2né estrutural, não cosmética — recortar diretamente comεseria a implementação óbvia e estaria errada:nrecortes deεacumulados estouram muito além de um fator de1 + ε. Todo o argumento de corretude repousa em(1 + ε/2n)^n ≤ 1 + ε, que por sua vez precisa da precondição0 < ε < 1para se fechar.