graph LR
subgraph "Cláusula 1"
A1["x₁"]
A2["x₂"]
A3["x₃"]
end
subgraph "Cláusula 2"
B1["¬x₁"]
B2["¬x₂"]
B3["¬x₃"]
end
subgraph "Cláusula 3"
C1["x₁"]
C2["x₂"]
C3["¬x₃"]
end
A2 -.-> B1
A2 -.-> B3
A3 -.-> B1
A3 -.-> B2
A2 -.-> C1
A2 -.-> C3
A3 -.-> C1
B1 -.-> C2
B1 -.-> C3
B2 -.-> C1
B2 -.-> C3
style A1 fill:#ffcccc
style A2 fill:#ccffcc
style A3 fill:#ccccff
style B1 fill:#ffcccc
style B2 fill:#ccffcc
style B3 fill:#ccccff
style C1 fill:#ffcccc
style C2 fill:#ccffcc
style C3 fill:#ccccff
Problemas NP-Completos
Computabilidade e Complexidade
Objetivos da Aula
- Definir redutibilidade em tempo polinomial (\(\leq_p\)).
- Compreender o conceito de problemas NP-Completos como os “mais difíceis” em NP.
- Apresentar o Teorema de Cook-Levin e a importância do problema SAT.
- Aprender a provar que um problema é NP-Completo através de reduções.
Conteúdo
Redefinindo a Redutibilidade para a Complexidade
Na teoria da computabilidade, usamos a redutibilidade para transferir a propriedade de “indecidibilidade”. Agora, na teoria da complexidade, vamos adaptá-la para transferir a propriedade de “dificuldade computacional” (ou “intratabilidade”).
A chave é garantir que a própria redução seja eficiente. Se a transformação de um problema A para um problema B levasse tempo exponencial, ela não nos diria nada útil sobre a dificuldade relativa deles.
Uma linguagem \(A\) é redutível em tempo polinomial à linguagem \(B\), denotado \(A \leq_p B\), se existe uma função \(f\) computável em tempo polinomial que transforma instâncias de \(A\) em instâncias de \(B\), tal que para toda string \(w\): \[w \in A \iff f(w) \in B\]
Formalmente, existe uma Máquina de Turing determinística \(M\) que computa \(f\) em tempo \(O(n^k)\) para algum \(k \geq 1\), onde \(n = |w|\).
A Lógica: Se \(A \leq_p B\), então \(B\) é pelo menos tão difícil quanto \(A\). Se encontrarmos um algoritmo de tempo polinomial para \(B\), poderemos resolver \(A\) em tempo polinomial também (primeiro executando a redução \(f\), depois o algoritmo para \(B\)).
Propriedades da Redutibilidade Polinomial
A redução polinomial possui propriedades importantes que a tornam uma ferramenta poderosa (Sipser, 2012):
1. Transitividade: Se \(A \leq_p B\) e \(B \leq_p C\), então \(A \leq_p C\).
Prova: Sejam \(f\) e \(g\) as funções de redução computáveis em tempo polinomial \(p(n)\) e \(q(n)\) respectivamente. A composição \(h(w) = g(f(w))\) é computável em tempo \(p(n) + q(p(n))\), que é polinomial. \(\square\)
2. Preservação de P: Se \(A \leq_p B\) e \(B \in \text{P}\), então \(A \in \text{P}\).
Prova: Se \(M_B\) decide \(B\) em tempo \(q(n)\), construímos \(M_A\) que:
- Computa \(f(w)\) em tempo \(p(n)\)
- Executa \(M_B\) em \(f(w)\) em tempo \(q(|f(w)|) \leq q(p(n))\)
O tempo total é \(p(n) + q(p(n))\), que é polinomial. \(\square\)
3. Contrapositiva útil: Se \(A \leq_p B\) e \(A \notin \text{P}\), então \(B \notin \text{P}\).
Isso nos permite usar reduções para provar que problemas são difíceis.
Uma redução \(A \leq_p B\) pode ser visualizada como um “tradutor” eficiente:
Entrada w → [Redução f] → f(w) → [Algoritmo para B] → SIM/NÃO
(tempo poli) (tempo desconhecido)
Se w ∈ A ⟺ f(w) ∈ B
A redução preserva a resposta: instâncias “sim” de \(A\) são mapeadas para instâncias “sim” de \(B\), e instâncias “não” de \(A\) são mapeadas para instâncias “não” de \(B\).
NP-Completude: Os Problemas Mais Difíceis em NP
Dentro da vasta classe NP, alguns problemas se destacam. Eles são os “chefões finais” de NP. Se conseguirmos resolver qualquer um deles de forma eficiente, conseguiremos resolver todos os problemas em NP de forma eficiente.
Uma linguagem \(L\) é NP-Completa se satisfaz duas condições:
- \(L\) está em NP (\(L \in \text{NP}\)).
- Toda outra linguagem em NP é redutível em tempo polinomial a \(L\) (\(\forall A \in \text{NP}, A \leq_p L\)).
Quando um problema satisfaz apenas a condição (2), dizemos que ele é NP-Difícil (NP-Hard).
Um problema NP-Completo (NPC) é, portanto, um problema em NP que é pelo menos tão difícil quanto qualquer outro problema em NP.
Se qualquer problema NP-Completo está em P, então P = NP.
Prova: Seja \(L\) um problema NP-Completo em P. Para qualquer \(A \in \text{NP}\):
- Por definição de NP-Completude: \(A \leq_p L\)
- Como \(L \in \text{P}\), pela propriedade de preservação: \(A \in \text{P}\)
- Como \(A\) era arbitrário: \(\text{NP} \subseteq \text{P}\)
- Já sabemos que \(\text{P} \subseteq \text{NP}\)
- Portanto: \(\text{P} = \text{NP}\) \(\square\)
Contrapositiva: Se P ≠ NP, então nenhum problema NP-Completo está em P!
Este teorema explica por que os problemas NP-Completos são tão importantes: eles são a “fronteira” entre P e NP.
O Teorema de Cook-Levin: O Primeiro Problema NP-Completo
A definição de NP-Completude parece criar um problema de “ovo e galinha”: como provar que o primeiro problema é NP-Completo, se não temos nenhum outro problema NPC para reduzir a partir dele?
A resposta veio em 1971 com o trabalho de Stephen Cook e Leonid Levin.
O problema da Satisfatibilidade Booleana (SAT) é NP-Completo. (Sipser, 2012, p. 312)
Problema SAT: Dada uma fórmula booleana \(\phi\) com variáveis, conectivos ∧ (E), ∨ (OU) e ¬ (NÃO) (ex: \((x_1 \lor \neg x_2) \land (x_2 \lor x_3)\)), existe uma atribuição de Verdadeiro/Falso para as variáveis que torna a fórmula inteira Verdadeira?
Formalmente: SAT = {\(\langle \phi \rangle\) | \(\phi\) é uma fórmula booleana satisfazível}
Esboço da Prova do Teorema de Cook-Levin
A prova é técnica, mas a ideia central é elegante e poderosa (Sipser, 2012):
Objetivo: Mostrar que para qualquer linguagem \(L \in \text{NP}\), temos \(L \leq_p \text{SAT}\).
Estratégia:
- Como \(L \in \text{NP}\), existe uma MTN \(N\) que decide \(L\) em tempo polinomial \(n^k\)
- Para uma entrada \(w\), construímos uma fórmula booleana \(\phi\) que “simula” a computação de \(N\) em \(w\)
- A fórmula \(\phi\) é satisfazível ⟺ \(N\) aceita \(w\) ⟺ \(w \in L\)
Construção da Fórmula \(\phi\):
A fórmula codifica a tabela de computação de \(N\) em \(w\) usando variáveis booleanas:
- \(x_{i,j,s}\) = “na configuração \(i\) (passo de tempo), a célula \(j\) contém o símbolo \(s\)”
- \(x_{i,q}\) = “na configuração \(i\), a máquina está no estado \(q\)”
A fórmula \(\phi\) é a conjunção de subcláusulas que garantem:
- \(\phi_{\text{cell}}\): Cada célula contém exatamente um símbolo em cada momento
- \(\phi_{\text{start}}\): A configuração inicial é correta (contém \(w\))
- \(\phi_{\text{move}}\): Cada configuração segue validamente da anterior (seguindo a função de transição de \(N\))
- \(\phi_{\text{accept}}\): Alguma configuração é de aceitação
Por que funciona:
- Se \(w \in L\): existe um ramo de aceitação em \(N\), que corresponde a uma atribuição satisfazível para \(\phi\)
- Se \(w \notin L\): nenhum ramo aceita, logo \(\phi\) é insatisfazível
- A construção leva tempo polinomial: \(O(n^{2k})\) variáveis e cláusulas
Para uma entrada de tamanho \(n\) e MTN com tempo \(n^k\):
- Número de configurações: \(O(n^k)\)
- Tamanho de cada configuração: \(O(n^k)\)
- Número de variáveis: \(O(n^{2k})\)
- Tamanho da fórmula: \(O(n^{2k})\)
Tudo polinomial! \(\square\)
A Genialidade da Prova: A prova mostra que, para qualquer problema em NP, a computação de sua Máquina de Turing Não-Determinística pode ser traduzida, em tempo polinomial, em uma fórmula SAT. Essa fórmula será satisfatível se, e somente se, a máquina aceitar a entrada. Isso estabelece que SAT é o “problema original” ao qual todos os outros em NP podem ser reduzidos.
Provando que Outros Problemas são NP-Completos
Uma vez que temos o SAT como nosso ponto de partida, a receita para provar que um novo problema \(L\) é NP-Completo se torna muito mais simples:
- Mostre que \(L \in \text{NP}\): Geralmente, esta é a parte fácil. Basta mostrar que, dada uma solução (um certificado), podemos verificá-la rapidamente (em tempo polinomial).
- Mostre que \(L\) é NP-Difícil: Para isso, escolha um problema \(S\) que você já sabe que é NP-Completo (como SAT, 3-SAT, SUBSET-SUM, etc.) e construa uma redução em tempo polinomial \(S \leq_p L\).
Exemplo de Redução 1: SAT ≤ₚ 3-SAT
Um dos exemplos mais importantes é mostrar que 3-SAT (uma versão restrita de SAT) também é NP-Completo (Sipser, 2012).
3-SAT = {\(\langle \phi \rangle\) | \(\phi\) é uma fórmula booleana satisfazível na forma normal conjuntiva (CNF) onde cada cláusula tem exatamente 3 literais}
Exemplo: \((x_1 \lor x_2 \lor \neg x_3) \land (\neg x_1 \lor x_2 \lor x_4) \land (x_3 \lor \neg x_4 \lor x_1)\)
Prova de NP-Completude de 3-SAT:
Passo 1: Mostrar que 3-SAT ∈ NP
- Certificado: Uma atribuição de valores às variáveis
- Verificador: Avalia cada cláusula (tempo linear)
- Portanto, 3-SAT ∈ NP ✓
Passo 2: Mostrar SAT ≤ₚ 3-SAT
Dada uma fórmula SAT arbitrária \(\phi\), construímos \(\phi'\) em 3-CNF:
Converter para CNF (já conhecida, tempo polinomial)
Ajustar cláusulas para ter exatamente 3 literais:
- Cláusula com 1 literal \((l_1)\): substituir por \((l_1 \lor y \lor z) \land (l_1 \lor y \lor \neg z) \land (l_1 \lor \neg y \lor z) \land (l_1 \lor \neg y \lor \neg z)\) onde \(y, z\) são novas variáveis
- Cláusula com 2 literais \((l_1 \lor l_2)\): substituir por \((l_1 \lor l_2 \lor y) \land (l_1 \lor l_2 \lor \neg y)\)
- Cláusula com 3 literais: manter como está
- Cláusula com \(k > 3\) literais \((l_1 \lor l_2 \lor \ldots \lor l_k)\): substituir por \((l_1 \lor l_2 \lor y_1) \land (\neg y_1 \lor l_3 \lor y_2) \land \ldots \land (\neg y_{k-3} \lor l_{k-1} \lor l_k)\)
Esta construção preserva satisfazibilidade e leva tempo polinomial. Portanto, SAT ≤ₚ 3-SAT ✓
Como SAT é NP-Completo e SAT ≤ₚ 3-SAT, concluímos que 3-SAT é NP-Completo. \(\square\)
Exemplo de Redução 2: 3-SAT ≤ₚ CLIQUE
Agora vamos mostrar que o problema CLIQUE é NP-Completo reduzindo de 3-SAT (Sipser, 2012).
CLIQUE = {\(\langle G, k \rangle\) | \(G\) é um grafo não-direcionado contendo uma \(k\)-clique}
Uma \(k\)-clique é um subconjunto de \(k\) vértices onde cada par está conectado por uma aresta.
Prova de NP-Completude de CLIQUE:
Passo 1: CLIQUE ∈ NP (já vimos na aula anterior) ✓
Passo 2: 3-SAT ≤ₚ CLIQUE
Ideia: Dada uma fórmula 3-CNF \(\phi\) com \(k\) cláusulas, construímos um grafo \(G\) tal que:
- \(\phi\) é satisfazível ⟺ \(G\) tem uma \(k\)-clique
Construção:
Para cada cláusula \((l_1 \lor l_2 \lor l_3)\), criar 3 vértices (um para cada literal)
Conectar dois vértices com aresta se:
- Estão em cláusulas diferentes, E
- Não são contraditórios (ex: \(x\) e \(\neg x\))
Exemplo: Para \(\phi = (x_1 \lor x_2 \lor x_3) \land (\neg x_1 \lor \neg x_2 \lor \neg x_3) \land (x_1 \lor x_2 \lor \neg x_3)\)
Correção:
- Se \(\phi\) é satisfazível: escolha um literal verdadeiro de cada cláusula → esses \(k\) vértices formam uma \(k\)-clique
- Se \(G\) tem \(k\)-clique: atribua verdadeiro aos literais correspondentes → todas as \(k\) cláusulas ficam verdadeiras
A construção leva tempo \(O(k^2)\), polinomial. Portanto, 3-SAT ≤ₚ CLIQUE ✓
Como 3-SAT é NP-Completo e 3-SAT ≤ₚ CLIQUE, concluímos que CLIQUE é NP-Completo. \(\square\)
Diagrama: A Árvore de Reduções NP-Completas
graph TD
SAT["<b>SAT</b><br/>(Cook-Levin 1971)"]
TSAT["<b>3-SAT</b><br/>(3 literais por cláusula)"]
CLIQUE["<b>CLIQUE</b><br/>(subgrafo completo)"]
VERTEX["<b>VERTEX-COVER</b><br/>(cobertura de vértices)"]
INDSET["<b>INDEPENDENT-SET</b><br/>(conjunto independente)"]
HAMPATH["<b>HAMPATH</b><br/>(caminho hamiltoniano)"]
TSP["<b>TSP</b><br/>(caixeiro viajante)"]
SUBSETSUM["<b>SUBSET-SUM</b><br/>(soma de subconjunto)"]
PARTITION["<b>PARTITION</b><br/>(partição balanceada)"]
SAT -->|redução| TSAT
TSAT -->|redução| CLIQUE
TSAT -->|redução| VERTEX
CLIQUE -->|complemento| INDSET
TSAT -->|redução| HAMPATH
HAMPATH -->|redução| TSP
TSAT -->|redução| SUBSETSUM
SUBSETSUM -->|redução| PARTITION
style SAT fill:#ff9999,stroke:#b71c1c,stroke-width:3px
style TSAT fill:#ffcccc
style CLIQUE fill:#ffe6e6
style VERTEX fill:#ffe6e6
style INDSET fill:#ffe6e6
style HAMPATH fill:#ffe6e6
style TSP fill:#ffe6e6
style SUBSETSUM fill:#ffe6e6
style PARTITION fill:#ffe6e6
- SAT é o problema fundamental (Teorema de Cook-Levin)
- 3-SAT é frequentemente usado como ponto de partida para outras reduções
- Todos os problemas mostrados são NP-Completos
- Conhecer essas reduções clássicas permite provar NP-Completude de novos problemas
- Existem milhares de problemas NP-Completos conhecidos!
O Significado Prático da NP-Completude
Se você está trabalhando em um problema e descobre que ele é NP-Completo, isso tem implicações imediatas:
Pare de procurar por um algoritmo rápido e exato: É extremamente improvável que você encontre um. Se você o fizesse, teria provado que P = NP e ganharia um milhão de dólares.
Mude sua abordagem: Em vez de uma solução perfeita e rápida, concentre-se em:
- Algoritmos de Aproximação: Encontram uma solução que é “boa o suficiente”, perto da ótima.
- Heurísticas: Algoritmos que funcionam bem na maioria dos casos do “mundo real”, mas sem garantias teóricas.
- Algoritmos Exponenciais para Instâncias Pequenas: Se suas entradas são sempre pequenas, uma solução \(O(2^n)\) pode ser aceitável.
Exemplos em Python: Problemas NP-Completos
Exemplo 1: O Problema CLIQUE
CLIQUE: Dado um grafo \(G\) e um inteiro \(k\), existe um subconjunto de \(k\) vértices em \(G\) onde cada vértice está conectado a todos os outros (formando um “clique” de tamanho \(k\))?
Este problema é um exemplo clássico de NPC.
Resolvedor de força bruta para CLIQUE
import itertools
def verificar_clique(grafo, certificado):
"""
Verificador para CLIQUE (tempo polinomial).
Verifica se o certificado forma um clique.
"""
k = len(certificado)
# Verifica se todos os pares estão conectados
for v1, v2 in itertools.combinations(certificado, 2):
if v2 not in grafo.get(v1, []) or v1 not in grafo.get(v2, []):
return False
return True
def resolver_clique_forca_bruta(grafo, k):
"""
Resolve o problema CLIQUE por força bruta.
Testa todos os subconjuntos de k vértices.
Complexidade: O(C(n,k) * k^2) ≈ O(n^k) - EXPONENCIAL!
"""
vertices = list(grafo.keys())
n = len(vertices)
if k > n:
return None
# Gera todas as combinações de k vértices
num_combinacoes = 0
for combinacao in itertools.combinations(vertices, k):
num_combinacoes += 1
if verificar_clique(grafo, combinacao):
print(f"Testadas {num_combinacoes} combinações")
return combinacao
print(f"Testadas todas as {num_combinacoes} combinações")
return None
# --- Exemplo de Uso ---
# Grafo representado como uma lista de adjacência
meu_grafo = {
'A': ['B', 'C', 'D'],
'B': ['A', 'C', 'D'],
'C': ['A', 'B', 'D', 'E'],
'D': ['A', 'B', 'C'],
'E': ['C']
}
k = 4
print("=== CLIQUE: Buscando um Clique ===\n")
print(f"Grafo: {meu_grafo}")
print(f"Tamanho do clique procurado: {k}\n")
print(f"--- Resolvendo (força bruta) ---")
clique_encontrado = resolver_clique_forca_bruta(meu_grafo, k)
if clique_encontrado:
print(f"Clique de tamanho {k} encontrado: {clique_encontrado}")
else:
print(f"Nenhum clique de tamanho {k} foi encontrado.")
print("\n--- Verificando um certificado ---")
certificado = ['A', 'B', 'C', 'D']
resultado = verificar_clique(meu_grafo, certificado)
print(f"O conjunto {certificado} forma um clique? {resultado}")=== CLIQUE: Buscando um Clique ===
Grafo: {'A': ['B', 'C', 'D'], 'B': ['A', 'C', 'D'], 'C': ['A', 'B', 'D', 'E'], 'D': ['A', 'B', 'C'], 'E': ['C']}
Tamanho do clique procurado: 4
--- Resolvendo (força bruta) ---
Testadas 1 combinações
Clique de tamanho 4 encontrado: ('A', 'B', 'C', 'D')
--- Verificando um certificado ---
O conjunto ['A', 'B', 'C', 'D'] forma um clique? True
Exemplo 2: 3-SAT
Vamos implementar um resolvedor de força bruta para 3-SAT:
Resolvedor e verificador para 3-SAT
def avaliar_clausula(clausula, atribuicao):
"""
Avalia uma cláusula (disjunção de literais).
clausula: lista de tuplas (variável, negado)
Ex: [(0, False), (1, True), (2, False)] representa (x0 ∨ ¬x1 ∨ x2)
"""
for var, negado in clausula:
valor = atribuicao[var]
if negado:
valor = not valor
if valor: # Se algum literal é verdadeiro, a cláusula é verdadeira
return True
return False
def verificar_3sat(formula, atribuicao):
"""
Verificador para 3-SAT (tempo polinomial).
Verifica se a atribuição satisfaz todas as cláusulas.
"""
return all(avaliar_clausula(clausula, atribuicao) for clausula in formula)
def resolver_3sat_forca_bruta(formula, num_variaveis):
"""
Resolve 3-SAT por força bruta.
Testa todas as 2^n atribuições possíveis.
Complexidade: O(2^n * m) onde n = variáveis, m = cláusulas - EXPONENCIAL!
"""
# Testa todas as atribuições possíveis (2^n)
for i in range(1 << num_variaveis):
# Converte o número i em uma atribuição binária
atribuicao = [(i >> j) & 1 for j in range(num_variaveis)]
if verificar_3sat(formula, atribuicao):
return atribuicao
return None
# --- Exemplo de Uso ---
# Fórmula: (x0 ∨ x1 ∨ x2) ∧ (¬x0 ∨ ¬x1 ∨ ¬x2) ∧ (x0 ∨ ¬x1 ∨ x2)
formula_3sat = [
[(0, False), (1, False), (2, False)], # (x0 ∨ x1 ∨ x2)
[(0, True), (1, True), (2, True)], # (¬x0 ∨ ¬x1 ∨ ¬x2)
[(0, False), (1, True), (2, False)] # (x0 ∨ ¬x1 ∨ x2)
]
num_vars = 3
print("\n=== 3-SAT: Satisfatibilidade ===\n")
print("Fórmula: (x0 ∨ x1 ∨ x2) ∧ (¬x0 ∨ ¬x1 ∨ ¬x2) ∧ (x0 ∨ ¬x1 ∨ x2)\n")
print("--- Resolvendo (força bruta) ---")
solucao = resolver_3sat_forca_bruta(formula_3sat, num_vars)
if solucao:
print(f"Fórmula SAT satisfazível!")
print(f"Atribuição: x0={solucao[0]}, x1={solucao[1]}, x2={solucao[2]}")
else:
print("Fórmula UNSAT (insatisfazível)")
print("\n--- Verificando uma atribuição ---")
atrib_teste = [1, 0, 1] # x0=True, x1=False, x2=True
resultado = verificar_3sat(formula_3sat, atrib_teste)
print(f"Atribuição {atrib_teste} satisfaz a fórmula? {resultado}")
print("\n--- Análise de Complexidade ---")
print(f"Número de variáveis: {num_vars}")
print(f"Número de cláusulas: {len(formula_3sat)}")
print(f"Atribuições possíveis: 2^{num_vars} = {2**num_vars}")
print(f"Tempo do verificador: O(m) = O({len(formula_3sat)})")
print(f"Tempo do resolvedor força-bruta: O(2^n * m) = O({2**num_vars * len(formula_3sat)})")
=== 3-SAT: Satisfatibilidade ===
Fórmula: (x0 ∨ x1 ∨ x2) ∧ (¬x0 ∨ ¬x1 ∨ ¬x2) ∧ (x0 ∨ ¬x1 ∨ x2)
--- Resolvendo (força bruta) ---
Fórmula SAT satisfazível!
Atribuição: x0=1, x1=0, x2=0
--- Verificando uma atribuição ---
Atribuição [1, 0, 1] satisfaz a fórmula? True
--- Análise de Complexidade ---
Número de variáveis: 3
Número de cláusulas: 3
Atribuições possíveis: 2^3 = 8
Tempo do verificador: O(m) = O(3)
Tempo do resolvedor força-bruta: O(2^n * m) = O(24)
Observe a diferença dramática:
- Verificar uma solução: O(m) - linear no número de cláusulas
- Encontrar uma solução: O(2^n · m) - exponencial no número de variáveis
Para n=50 variáveis: 2^50 ≈ 10^15 atribuições para testar!
O código acima usa força bruta, que é ineficiente. A NP-Completude desses problemas sugere que não existe um algoritmo significativamente melhor (em tempo polinomial).
Exercícios de Verificação
Definições: Qual a diferença entre um problema ser NP e ser NP-Completo? Um problema pode ser NP-Completo mas não ser NP?
Prova de NP-Completude: Você quer provar que um novo problema,
PROB_X, é NP-Completo. Você já demonstrou quePROB_Xestá em NP. Agora, você constrói uma redução \(PROB\_X \leq_p \text{SAT}\). Esta redução ajuda a completar sua prova? Se não, o que você deveria ter feito?Análise de Problema: Considere o problema COBERTURA DE VÉRTICES (VERTEX-COVER): “Dado um grafo \(G\) e um inteiro \(k\), existe um subconjunto de \(k\) vértices tal que toda aresta do grafo toca em pelo menos um desses vértices?”. Descreva por que este problema está em NP.
Transitividade: Se sabemos que 3-SAT é NP-Completo e construímos uma redução 3-SAT ≤ₚ HAMPATH, o que podemos concluir sobre HAMPATH (assumindo que já mostramos HAMPATH ∈ NP)?
Redução Reversa: Suponha que você está tentando provar que um problema L é NP-Completo. Você mostra que L ∈ NP e depois constrói uma redução L ≤ₚ 3-SAT. Isso prova que L é NP-Completo? Por quê?
Complemento: Se um problema L é NP-Completo, o que podemos dizer sobre seu complemento \(\overline{L}\)? Ele também é NP-Completo?
Construção de Redução: Esboce uma ideia de como construir uma redução de CLIQUE para INDEPENDENT-SET. (Dica: pense no grafo complemento)
Diferença entre NP e NPC:
- NP é uma classe de problemas que possuem uma verificação rápida (em tempo polinomial).
- NP-Completo (NPC) é uma subclasse de NP. Problemas NPC não são apenas “verificáveis” (a primeira condição, estarem em NP), mas também são os “mais difíceis” de toda a classe NP (a segunda condição, de que todo outro problema em NP se reduz a eles).
- Não, um problema não pode ser NP-Completo sem ser NP. A primeira regra para ser NPC é pertencer à classe NP.
- Diagrama: NPC = NP ∩ NP-Difícil
Erro na Redução: A redução \(PROB\_X \leq_p \text{SAT}\) não ajuda a provar que
PROB_Xé NP-Completo. Esta redução mostra quePROB_Xnão é mais difícil que SAT (na verdade, mostra que PROB_X ∈ P se SAT ∈ P). Para provar quePROB_Xé NP-Difícil (a segunda parte da prova de NPC), você precisa fazer a redução na direção oposta: encontrar um problema conhecido por ser NP-Completo, como SAT, e provar que \(\text{SAT} \leq_p PROB\_X\). Isso mostraria quePROB_Xé pelo menos tão difícil quanto SAT.VERTEX-COVER está em NP: O problema está em NP porque podemos verificar uma solução proposta de forma eficiente.
- Certificado: Um subconjunto \(C \subseteq V\) de \(k\) vértices
- Verificador: Um algoritmo que recebe o grafo \(G = (V, E)\) e o certificado \(C\):
- Verifica se \(|C| \leq k\)
- Para cada aresta \((u, v) \in E\), verifica se \(u \in C\) ou \(v \in C\)
- Aceita se todas as arestas são cobertas
- Tempo: \(O(|E|)\) - linear no número de arestas
- Este processo é polinomial, logo VERTEX-COVER ∈ NP ✓
Transitividade e HAMPATH:
- Sabemos que 3-SAT é NP-Completo
- Construímos 3-SAT ≤ₚ HAMPATH
- Já sabemos que HAMPATH ∈ NP
- Conclusão: HAMPATH é NP-Completo!
- Justificativa: Por transitividade, se todo problema em NP se reduz a 3-SAT, e 3-SAT se reduz a HAMPATH, então todo problema em NP se reduz a HAMPATH. Como HAMPATH ∈ NP, ele satisfaz ambas as condições de NP-Completude.
Redução Reversa NÃO funciona:
- NÃO, isso não prova que L é NP-Completo
- A redução L ≤ₚ 3-SAT mostra apenas que L não é mais difícil que 3-SAT
- Na verdade, todo problema em P também se reduz a 3-SAT!
- O que precisamos: Mostrar 3-SAT ≤ₚ L (ou qualquer problema NPC ≤ₚ L)
- Analogia: “Provar que você é mais forte que o campeão não significa provar que o campeão é mais fraco que você”
Complemento de NP-Completo:
- Se L é NP-Completo, então \(\overline{L}\) está em co-NP
- \(\overline{L}\) é co-NP-Completo (o mais difícil problema em co-NP)
- Mas: Não sabemos se \(\overline{L}\) é NP-Completo!
- Se conseguíssemos provar que algum problema co-NP-Completo também é NP-Completo, teríamos NP = co-NP (questão em aberto!)
- Exemplo: SAT é NP-Completo, mas UNSAT (seu complemento) é co-NP-Completo
CLIQUE ≤ₚ INDEPENDENT-SET: Ideia da redução:
- Dado \(\langle G, k \rangle\) para CLIQUE, construir \(\langle G', k' \rangle\) para INDEPENDENT-SET
- Construção:
- \(G' = \overline{G}\) (grafo complemento: arestas viram não-arestas e vice-versa)
- \(k' = k\)
- Correção: \(G\) tem \(k\)-clique ⟺ \(\overline{G}\) tem \(k\)-independent-set
- Justificativa: Vértices que formam clique em \(G\) (todos conectados) formam conjunto independente em \(\overline{G}\) (nenhum conectado)
- Tempo: \(O(n^2)\) para construir o grafo complemento - polinomial!
- Logo, CLIQUE ≤ₚ INDEPENDENT-SET ✓
Aplicações Práticas e Perspectivas
A teoria da NP-Completude não é apenas matemática abstrata - ela tem profundas implicações práticas.
Problemas NP-Completos no Mundo Real
Milhares de problemas práticos importantes são NP-Completos (Sipser, 2012):
- Otimização de Recursos
- Alocação de tarefas a processadores
- Escalonamento de horários (escola, aeroportos)
- Empacotamento de itens (bin packing)
- Design de Circuitos
- Roteamento de circuitos VLSI
- Verificação de circuitos
- Bioinformática
- Alinhamento de sequências de DNA
- Dobramento de proteínas
- Redes e Logística
- Roteamento de veículos
- Planejamento de rotas de entrega
Lidando com NP-Completude na Prática
Quando você identifica que seu problema é NP-Completo, você tem várias opções (Sipser, 2012):
- Algoritmos de Aproximação
- Garantem solução “próxima” da ótima em tempo polinomial
- Ex: Algoritmo 2-aproximação para VERTEX-COVER
- Heurísticas e Metaheurísticas
- Algoritmos genéticos, simulated annealing, busca tabu
- Funcionam bem na prática, mas sem garantias teóricas
- Casos Especiais
- Muitos problemas NP-Completos têm subclasses tratáveis
- Ex: 2-SAT está em P (mas 3-SAT é NP-Completo!)
- Algoritmos Parametrizados
- Exponencial em um parâmetro, polinomial nos outros
- Ex: VERTEX-COVER é O(2^k · n) usando kernel
- Instâncias Pequenas
- Se suas entradas são sempre pequenas (n ≤ 30), exponencial pode ser aceitável
- Técnicas: backtracking inteligente, programação dinâmica
- Aproximação Física
- Computação quântica, computação por DNA
- Ainda em pesquisa!
Contexto Histórico e Impacto
- 1971: Stephen Cook publica o Teorema de Cook-Levin
- 1972: Richard Karp identifica 21 problemas NP-Completos clássicos
- Anos 1970-1980: Explosão de descobertas - milhares de problemas NP-Completos
- Hoje: Mais de 10.000 problemas NP-Completos conhecidos!
O trabalho de Karp (1972) foi fundamental porque mostrou que NP-Completude não é uma curiosidade matemática, mas aparece em problemas práticos de todas as áreas.
Alguns dos 21 problemas originais de Karp:
- SAT e 3-SAT
- CLIQUE
- VERTEX-COVER
- HAMPATH e TSP
- SUBSET-SUM
- PARTITION
- Graph Coloring
- Knapsack
- Job Scheduling
- … e outros
Todos esses problemas práticos compartilham a mesma dificuldade fundamental!
O Futuro da NP-Completude
Perguntas ainda em aberto:
- P vs NP permanece sem resposta
- Novos paradigmas (computação quântica) podem ajudar?
- Teorias de complexidade avançadas (PH, PSPACE, etc.)
Impacto contínuo:
- Toda nova área da computação encontra problemas NP-Completos
- Machine Learning: treinamento ótimo de redes neurais é NP-Completo
- Blockchain: mineração de criptomoedas usa problemas computacionalmente difíceis
- Verificação formal: muitos problemas de verificação são NP-Completos
A teoria da NP-Completude nos ensina limites fundamentais do que podemos computar eficientemente, guiando o desenvolvimento de algoritmos práticos e teorias de complexidade mais profundas.