"The greatest shortcoming of the human race is
our inability to understand the exponential function."
— Albert A. Bartlett, "Arithmetic, Population and Energy", YouTube video
Outras páginas deste sítio trataram da complexidade de algoritmos. Esta página tratará de algo mais abstrato: a complexidade computacional intrínseca de problemas.
A complexidade de um problema é o consumo de tempo do melhor algoritmo possível para o problema. (O melhor algoritmo conhecido atualmente não é, em geral, o melhor algoritmo possível.) É claro que estamos nos referindo ao consumo de tempo no pior caso, ou seja, ao consumo de tempo para as instâncias mais "difíceis" do problema.
O assunto será discutido aqui de maneira informal e portanto imprecisa. Para uma exposição mais técnica, veja a página Problemas NP-completos. Veja também o verbete NP-complete na Wikipedia.
Precisamos de um repertório de exemplos. Vários exemplos de problemas já foram arrolados em outra página. Seguem mais alguns:
(Observe que há uma relação íntima entre o problema do ciclo hamiltoniano e o problema do ciclo longo. Há relações análogas entre vários outros pares de problemas.)
Dizemos que um algoritmo resolve um dado problema se, ao receber qualquer instância do problema, devolve uma solução da instância ou diz que a instância não tem solução.
Um algoritmo que resolve um dado problema é polinomial se o seu consumo de tempo no pior caso é limitado por uma função polinomial dos tamanhos das instâncias do problema.
É polinomial, por exemplo, todo algoritmo que consome no máximo 100N4 + 300N2 + 5000 unidades de tempo, sendo N o tamanho da instância. Também é polinomial todo algoritmo que consome no máximo 200N9 log N unidades de tempo, por exemplo (pois 200N9 log N < 200N10).
Algoritmos polinomiais são considerados rápidos (ainda que seja difícil aceitar como rápido um algoritmo que consome tempo proporcional a N500, por exemplo). Algoritmos não polinomiais — como, por exemplo, os que consomem tempo proporcional a 2N — são considerados inaceitavelmente lentos (ainda que possam ser úteis para valores muito modestos de N).
Um problema computacional é polinomial se existe um algoritmo polinomial para o problema. Problemas desse tipo são considerados tratáveis do ponto de vista computacional.
Exemplos de problemas polinomiais: o problema da equação do segundo grau, o problema do máximo divisor comum, o problema do caminho mínimo, o problema da subsequência crescente máxima.
A classe P de problemas é o conjunto de todos os problemas polinomiais. [A rigor, esta definição está incorreta, pois a classe P contém apenas os problemas polinomiais de decisão.]
Um problema é não polinomial se nenhum algoritmo polinomial resolve o problema. (Cuidado: não se trata de problemas para os quais algoritmos polinomiais não são conhecidos atualmente, pois tais algoritmos podem vir a ser descobertos no futuro.) Problemas desse tipo são considerados computacionalmente intratáveis.
O status de muitos problemas é desconhecido: não se sabe se o problema é polinomial ou não. Diante disso, é uma boa ideia investigar a complexidade relativa dos problemas. Trata-se de verificar se um dado problema Y é computacionalmente mais fácil ou mais difícil que um outro problema X (talvez mais bem-conhecido). Antes que isso possa ser feito, entretanto, é preciso restringir um pouco o universo dos problemas sob estudo.
Quais problemas devemos considerar "razoáveis"? Diremos que um problema é "razoável" se é fácil reconhecer uma solução do problema quando se está diante de uma. Mais precisamente, um problema computacional X é "razoável" se toda instância I de X satisfaz a seguinte condição:
é possível verificar, em tempo polinomial, se uma suposta solução da instância I é, de fato, uma solução de I.
Muitos dos problemas mencionados acima são "razoáveis". Considere os seguintes exemplos:
O conjunto de todos os problemas "razoáveis" é (essencialmente) igual à classe NP de problemas. [Cuidado: "NP" não é abreviatura de "não polinomial" mas sim de "nondeterministic polynomial".]
(A rigor, não é correto confundir o conjunto dos problemas "razoáveis" com classe NP. A questão é que o conceito de solução — que serve de base para a ideia de problema "razoável" — não é suficientemente preciso. Considere, por exemplo, o problema do ciclo máximo. Por um lado, parece que deveríamos aceitar o problema como "razoável" uma vez que o problema do ciclo longo é "razoável". Por outro lado, o problema não parece "razoável" pois não está claro como verificar se um dado ciclo é máximo, ou seja, se não existe ciclo mais longo. Para contornar essas dificuldades, é preciso restringir nossa atenção a problemas "de decisão" e trocar o conceito de "solução" pelo de "certificado".)
Não é difícil entender que a classe NP inclui a classe P, ou seja, que todo problema polinomial é "razoável". O bom senso sugere que P é apenas uma pequena parte de NP. Surpreendentemente, ninguém conseguiu ainda encontrar um problema de NP que comprovadamente não esteja em P, isto é, um problema "razoável" para o qual comprovadamente não existe algoritmo polinomial.
Esta situação abre caminho para a suspeita de que talvez P seja igual a NP . A maioria dos especialistas não acredita nessa possibilidade, entretanto. [O Instituto Clay de Matemática oferece um prêmio de um milhão de dólares pela solução da questão "P=NP?".]
A pergunta "P = NP?" por ser reformulada assim: "É verdade que todo problema cujas soluções podem ser conferidas por um algoritmo polinomial pode também ser resolvido por um algoritmo polinomial?"
Podemos agora dizer algo sobre a complexidade relativa de problemas em NP. Um problema Y não é mais difícil que um outro problema X se Y for um "subproblema", ou "caso particular", de X. Exemplos:
Para explicar um pouco melhor o conceito, diremos que um problema Y não é mais difícil que um problema X se qualquer algoritmo polinomial para X pode ser transformado num algoritmo polinomial para Y. O algoritmo para Y tem três etapas: primeiro, qualquer instância J de Y é transformada, em tempo polinomial, numa instância "equivalente" I de X; depois, a instância I é submetida ao algoritmo polinomial para X, que produz uma solução S; finalmente, S é transformada, em tempo polinomial, numa solução T de J. Assim, se X está em P e Y não é mais difícil que X então Y também está em P.
Um problema X é completo em NP, ou NP-completo, se X está em NP e todos os demais problemas em NP não são mais difíceis que X.
A existência de problemas completos em NP é um fato surpreendente e fundamental. O fato foi demonstrado, por volta de 1970, por S. Cook e L. Levin (independentemente).
Para mostrar que P = NP basta encontrar um algoritmo polinomial para um único problema NP-completo. Portanto, os problemas NP-completos são os mais "difíceis" de NP.
Por exemplo, são NP-completos os problemas (de decisão derivados dos) seguintes: ciclo hamiltoniano, ciclo longo, clique grande, cobertura pequena, mochila. Uma lista com centenas de outros problemas NP-completos pode ser encontrada no livro de Garey e Johnson.
Nossa tentativa grosseira de definir a classe NP de problemas, trouxe à baila uma questão importante. Muitos problemas têm instâncias que não admitem solução. Como é possível "provar" ou "certificar" que uma dada instância de um problema não admite solução? Não estamos tratando aqui de como descobrir que uma dada instância não tem solução, mas apenas de, uma vez descoberta a inexistência de solução, como tornar este fato "evidente".
Para muitos problemas, existem certificados naturais e elegantes para a inexistência de solução. (Todos consistem em trocar um "não existe x" por um "existe y" apropriado.) Eis alguns exemplos:
Cada um desses exemplos mostra que o correspondente problema está na classe coNP.