Força bruta
Enumeração de subconjuntos, intervalos e circuitos, com contagem do trabalho e reconstrução do melhor candidato.
No vetor [-4, 3, -1, 5, -6, 2], o bloco contíguo de maior soma não é o conjunto dos positivos. Somar todos os positivos dá 10, mas esses elementos estão separados por negativos e não formam um intervalo. O melhor intervalo é [3, -1, 5], com soma 7. Para garantir esse resultado por força bruta, enumeramos cada início e cada fim, calculamos a soma e guardamos o melhor.
A força bruta compara todos os candidatos válidos. A prova de correção tem duas partes: mostrar que nenhum candidato fica por gerar e mostrar que a validade é testada antes de comparar valores. O custo depende da quantidade de candidatos e do trabalho para avaliar cada um. Enumerar intervalos dá um custo polinomial. Enumerar todos os subconjuntos dá candidatos. Em entradas pequenas, a mesma enumeração serve para conferir um algoritmo mais rápido.
Subvetor de soma máxima
Dado um vetor com inteiros, procuramos um intervalo contíguo e não vazio de soma máxima. A condição de contiguidade distingue este problema de escolher um subconjunto qualquer.
Há intervalos, um por cada par . A forma direta recalcula cada soma com um terceiro ciclo e custa . Podemos fixar o início , começar com soma=0 e acrescentar à medida que o fim avança. Cada intervalo passa a ser avaliado com uma adição:
melhor = A[0]
para i = 0 até n-1:
soma = 0
para j = i até n-1:
soma += A[j]
se soma > melhor:
guardar soma, i e j
Antes de avaliar , soma é a soma de . Como os dois ciclos visitam todos os pares , nenhum intervalo fica por avaliar. O custo passa a tempo e memória auxiliar.
O programa lê n e os elementos, e devolve soma e extremos com índices a partir de zero. Em empate conserva o primeiro intervalo visitado. Aceita de 1 a 2000 valores, cada um entre menos um milhão e um milhão.
#include <iostream>
#include <vector>
int main() {
int n;
if (!(std::cin >> n) || n < 1 || n > 2000) {
std::cout << "entrada invalida\n";
return 0;
}
std::vector<long long> a(n);
for (auto &x : a) {
if (!(std::cin >> x) || x < -1000000 || x > 1000000) {
std::cout << "entrada invalida\n";
return 0;
}
}
long long melhor = a[0];
int inicio = 0, fim = 0;
for (int i = 0; i < n; ++i) {
long long soma = 0;
for (int j = i; j < n; ++j) {
soma += a[j];
if (soma <= melhor) continue;
melhor = soma;
inicio = i;
fim = j;
}
}
std::cout << "soma " << melhor << "\nintervalo " << inicio << ' ' << fim << '\n';
}Executa o código para ver a saída e os gráficos.
Experimenta 3\n-8 -2 -5. A resposta deve ser , intervalo . Inicializar o melhor a zero devolveria um intervalo vazio que o enunciado não permite. O mesmo problema reaparece por divisão e conquista e por programação dinâmica, com estados diferentes e custos menores.
Subconjuntos e mochila 0-1
Com objetos, cada objeto tem duas decisões: incluir ou excluir. Existem subconjuntos, incluindo o vazio. Na mochila 0-1, um candidato é válido quando a soma dos pesos não excede a capacidade . O objetivo é maximizar a soma dos valores. Cada objeto pode aparecer uma única vez.
Considera capacidade 7 e objetos , , e , onde cada par é (peso, valor):
| Subconjunto | Peso | Valor | Válido? |
|---|---|---|---|
| vazio | 0 | 0 | Sim |
| 5 | 7 | Sim | |
| 7 | 9 | Sim | |
| 7 | 11 | Sim, ótimo | |
| 9 | 13 | Não | |
| 9 | 12 | Não |
A tabela mostra o erro típico: tem valor 13, mas excede a capacidade e não pode ser escolhido. Validar antes de comparar evita guardar um candidato impossível.
Uma máscara de bits representa um subconjunto. A máscara facilita a correspondência com índices. Uma recursão com peso e valor acumulados representa o mesmo conjunto sem recalcular somas, facilita a poda por capacidade e a reconstrução incremental. Percorrer máscaras e somar os objetos em cada uma custa . Uma recursão que mantém os acumulados faz duas extensões de custo constante por nó e visita nós, com custo antes de copiar soluções. Guardar a melhor seleção pode acrescentar por cópia, por isso guarda apenas a melhor máscara quando só o valor final interessa.
Não uses 1 << n sem respeitar a largura do tipo. Deslocar por um número de bits igual ou superior à largura do inteiro não é uma enumeração válida em C++.
Soma de subconjuntos
Aqui o objetivo é decidir se algum subconjunto soma exatamente . Para valores e , existem duas soluções: e . Devolver uma delas resolve a tarefa de existência. Enumerar todas é outra tarefa, com saída maior.
Uma recursão procura(i, soma) decide incluir ou excluir o elemento . Quando , aceita se soma == T. Para reconstruir, mantém as escolhas no caminho atual e copia-as quando encontra uma solução. O pior caso visita nós e usa memória para a pilha e a seleção atual.
Se todos os valores forem não negativos, soma > T permite parar esse ramo, porque acrescentar mais valores nunca reduz a soma. Com negativos, essa poda é inválida: atinge um alvo de 5 depois de o ultrapassar. A condição dos valores tem de acompanhar a poda. O retrocesso mostra as duas podas que tornam essa rejeição segura.
Trocos com stock
Para denominações e stocks , cada decisão é uma quantidade . Um candidato paga quando . O objetivo é minimizar .
Com moedas de 1, 3 e 4, stocks 2, 2 e 1, e alvo 6, há duas formas válidas: duas moedas de 3, ou uma de 4 com duas de 1. A solução ótima usa duas moedas. Escolher sempre a maior que cabe produz três moedas, embora encontre uma solução válida. O exemplo separa encontrar uma solução de provar que é mínima.
O número de combinações de quantidades é , limitado por quando é o stock máximo. Recalcular soma e contagem em cada folha acrescenta um fator . Manter os totais durante a recursão evita esse recálculo. A pilha usa memória, sem guardar todos os candidatos.
Caixeiro-viajante
No TSP, procuramos um circuito que visita cada vértice uma vez e regressa ao início com custo mínimo. Fixar a cidade inicial remove rotações da mesma sequência. Restam permutações. Cada uma tem arestas para avaliar, pelo que o custo direto é .
Numa instância não dirigida e simétrica, uma rota e a sua inversa têm o mesmo custo. Podemos eliminar essa duplicação com uma regra explícita, por exemplo exigir que a segunda cidade tenha menor identificador que a última. Num grafo dirigido, essa regra pode eliminar a solução ótima, porque inverter a rota muda os custos e pode usar arestas inexistentes. A regra de simetria pertence ao modelo não dirigido.
A existência de um percurso barato entre duas cidades não resolve o TSP. É preciso visitar todas e regressar. Não confundas circuito hamiltoniano, que visita vértices, com circuito euleriano, que visita arestas.
O que a enumeração prova
Para provar um algoritmo de força bruta, descreve a correspondência entre soluções e candidatos gerados. Depois justifica o teste de validade e o registo do melhor. Num problema de otimização, atualizar o melhor antes de testar a capacidade guarda uma solução impossível. Num problema de existência, devolver falso só é correto depois de esgotar todos os ramos que ainda poderiam produzir uma solução.
Exercícios
Intervalo não vazio
Qual é a maior soma de um intervalo contíguo não vazio de [-7, -3, -9, -1, -5]?
Pista
Somar dois números negativos torna o resultado menor que cada parcela.
Solução
O intervalo formado apenas pelo -1, no índice 3, tem soma -1. Qualquer intervalo com mais de um elemento soma valores negativos e não melhora o maior elemento. Zero seria o valor de um intervalo vazio, proibido pelo enunciado.
Erros frequentes
Inicializar o melhor a zero altera o problema. Escolher posições não adjacentes não seria um intervalo contíguo.
Contar circuitos candidatos
Um TSP dirigido completo tem 7 cidades. Fixamos a cidade inicial e enumeramos todas as ordens das restantes. Quantos candidatos avaliamos, sem identificar uma rota com a inversa?
Pista
A origem fixa retira uma escolha; as restantes cidades ainda podem aparecer em qualquer ordem.
Solução
São 6! = 720 permutações; cada uma visita as outras seis cidades numa ordem e regressa à origem. Como os custos são dirigidos, a rota inversa pode ter custo diferente e não pode ser descartada por simetria. Avaliar cada circuito exige somar sete arestas.
Erros frequentes
Dividir por dois só é válido com a simetria apropriada. Esquecer a aresta de regresso avalia caminhos abertos.