A primeira coisa a entender é que P e NP classificam linguagens , não problemas . Para entender o que isso significa, precisamos primeiro de algumas outras definições.
Um alfabeto é um conjunto finito não vazio de símbolos.
{ 0
, 1
} é um alfabeto, assim como o conjunto de caracteres ASCII. {} não é um alfabeto porque está vazio. N (os números inteiros) não é um alfabeto porque não é finito.
Vamos Σ ser um alfabeto. Uma concatenação ordenada de um número finito de símbolos de Σ é chamada de palavra sobre Σ .
A string 101
é uma palavra sobre o alfabeto { 0
, 1
}. A palavra vazia (geralmente escrita como ε ) é uma palavra sobre qualquer alfabeto. A sequência penguin
é uma palavra sobre o alfabeto que contém os caracteres ASCII. A notação decimal do número π não é uma palavra sobre o alfabeto { .
, 0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
} porque não é finito.
O comprimento de uma palavra w , escrita como | w |, é o número de símbolos nele.
Por exemplo, | hello
| = 5 e | £ | = 0. Para qualquer palavra w , | w | ∈ N e, portanto, finito.
Vamos Σ ser um alfabeto. O conjunto Σ * contém todas as palavras sobre Σ , incluindo ε . O conjunto Σ + contém todas as palavras sobre Σ , excluindo ε . Para n ∈ N , Σ n é o conjunto de palavras com comprimento n .
Para cada alfabeto Σ , Σ * e Σ + são infinitas conjunto contável . Para o conjunto de caracteres ASCII Σ ASCII , as expressões regulares .*
e .+
denotam Σ ASCII * e Σ ASCII +, respectivamente.
{ 0
, 1
} 7 é o conjunto de códigos ASCII de 7 bits { 0000000
, 0000001
, ..., 1111111
}. { 0
, 1
} 32 é o conjunto de valores inteiros de 32 bits.
Vamos Σ ser um alfabeto e L ⊆ Σ * . L é chamado de idioma acima de Σ .
Para um alfabeto Σ , o conjunto vazio e Σ * são línguas triviais mais de Σ . O primeiro é frequentemente chamado de idioma vazio . O idioma vazio {} e o idioma que contém apenas a palavra vazia { ε } são diferentes.
O subconjunto de { 0
, 1
} 32 que corresponde a valores de ponto flutuante não NaN IEEE 754 é um idioma finito.
Os idiomas podem ter um número infinito de palavras, mas todos os idiomas são contáveis. O conjunto de cordas { 1
, 2
, ...} denotando os inteiros em notação decimal é uma linguagem infinita sobre o alfabeto { 0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
}. O conjunto infinito de seqüências { 2
, 3
, 5
, 7
, 11
, 13
, ...} denotando os números primos em notação decimal é um subconjunto adequado dos mesmos. O idioma que contém todas as palavras correspondentes à expressão regular [+-]?\d+\.\d*([eE][+-]?\d+)?
é um idioma sobre o conjunto de caracteres ASCII (denotando um subconjunto das expressões de ponto flutuante válidas, conforme definido pela linguagem de programação C).
Não há idioma que contenha todos os números reais (em qualquer notação) porque o conjunto de números reais não é contável.
Vamos Σ ser um alfabeto e L ⊆ Σ * . Uma máquina D decide G se para cada entrada w ∈ Σ * que calcula a função característica χ G ( w ) em tempo finito. A função característica é definida como
χ G : Σ * → {0, 1}
w ↦ 1, w ∈ L
0, caso contrário.
Tal máquina é chamado um decisivo para L . Escrevemos “ D ( w ) = x ” para “dado w , D produz x ”.
Existem muitos modelos de máquinas. O mais geral atualmente em uso prático é o modelo de uma máquina de Turing . Uma máquina de Turing possui armazenamento linear ilimitado agrupado em células. Cada célula pode conter exatamente um símbolo de um alfabeto a qualquer momento. A máquina de Turing executa seu cálculo como uma sequência de etapas de cálculo. Em cada etapa, ele pode ler uma célula, possivelmente substitui seu valor e move a cabeça de leitura / gravação em uma posição para a célula esquerda ou direita. A ação que a máquina executará é controlada por um autômato de estado finito.
Uma máquina de acesso aleatório com um conjunto finito de instruções e armazenamento ilimitado é outro modelo de máquina que é tão poderoso quanto o modelo de máquina de Turing.
Para o bem desta discussão, não devemos nos incomodar com o modelo preciso da máquina que usamos, mas basta dizer que a máquina possui uma unidade de controle determinística finita, armazenamento ilimitado e executa um cálculo como uma sequência de etapas que podem ser contadas.
Como você o usou em sua pergunta, suponho que você já esteja familiarizado com a notação "big-O", então aqui está apenas uma atualização rápida.
Seja f : N → uma função. O conjunto O ( f ) contém todas as funções g : N → N para as quais existem constantes n 0 ∈ N e c ∈ N de modo que para cada n ∈ N com n > n 0 é verdade que g ( n ) ≤ c f ( n )
Agora estamos preparados para abordar a questão real.
A classe P contém todos os idiomas L para os quais existe uma máquina de Turing D que decide L e uma constante k ∈ N tal que para cada entrada w , D pára após no máximo T (| w |) etapas para uma função T ∈ O ( n ↦ n k ).
Como O ( n ↦ n k ), embora matematicamente correto, seja inconveniente para escrever e ler, a maioria das pessoas - para ser honesto, todos , exceto eu - geralmente escreve simplesmente O ( n k ).
Observe que o limite depende do comprimento de w . Portanto, o argumento que você cria para o idioma dos números primos é correto apenas para números em codificações desaray , em que para a codificação w de um número n , o comprimento da codificação | w | é proporcional a n . Ninguém jamais usaria essa codificação na prática. Usando um algoritmo mais avançado do que simplesmente tentar todos os fatores possíveis, pode ser mostrado, no entanto, que a linguagem dos números primos permanece em P se as entradas forem codificadas em binário (ou em qualquer outra base). (Apesar do grande interesse, isso só pôde ser comprovado por Manindra Agrawal, Neeraj Kayal e Nitin Saxena em um artigo premiado em 2004, para que você possa adivinhar que o algoritmo não é muito simples.)
Os idiomas triviais {} e Σ * e o idioma não trivial { ε } estão obviamente em P (para qualquer alfabeto Σ ). Você pode escrever funções em sua linguagem de programação favorita que usam uma string como entrada e retornam um booleano informando se a string é uma palavra do idioma de cada uma delas e provar que sua função possui complexidade de tempo de execução polinomial?
Cada regulares linguagem (a linguagem descrita por uma expressão regular) está em P .
Vamos Σ ser um alfabeto e L ⊆ Σ * . Uma máquina V que pega uma tupla codificada de duas palavras w , c ∈ Σ * e gera 0 ou 1 após um número finito de etapas é um verificador para L se tiver as seguintes propriedades.
- Dado ( w , c ), V saídas 1 somente se W ∈ L .
- Para cada w ∈ G , existe um c ∈ Σ * tal que V ( w , c ) = 1.
O C na definição acima é chamado um testemunho (ou certificado ).
Um verificador é permitido dar falsos negativos para a testemunha errado mesmo se w realmente está em L . No entanto, não é permitido dar falsos positivos. Também é necessário que, para cada palavra no idioma, exista pelo menos uma testemunha.
Para o idioma COMPOSITE, que contém as codificações decimais de todos os números inteiros que não são primos, uma testemunha pode ser uma fatoração. Por exemplo, (659, 709)
é uma testemunha de 467231
∈ COMPOSITE. Você pode verificar facilmente que, em uma folha de papel, sem a testemunha, provar que 467231 não é primo seria difícil sem o uso de um computador.
Não dissemos nada sobre como uma testemunha apropriada pode ser encontrada. Esta é a parte não determinística.
A classe NP contém todos os idiomas L para os quais existe uma máquina de Turing V que verifica L e uma constante k ∈ N, de modo que, para cada entrada ( w , c ), V pára após no máximo T (| w |) etapas de uma função T ∈ O ( n ↦ n k ).
Observe que a definição acima implica que para cada w ∈ L existe uma testemunha c com | c | ≤ T (| w |). (A máquina de Turing não pode olhar para mais símbolos da testemunha.)
NP é um superconjunto de P (por quê?). Não se sabe se existem línguas que estão em NP mas não em P .
A fatoração inteira não é um idioma em si. No entanto, podemos construir uma linguagem que represente o problema de decisão associado a ela. Ou seja, uma linguagem que contém todas as tuplas ( n , m ) de modo que n tenha um fator d com d ≤ m . Vamos chamar esse idioma de FACTOR. Se você tiver um algoritmo para decidir o FACTOR, ele poderá ser usado para calcular uma fatoração completa com apenas sobrecarga polinomial, realizando uma pesquisa binária recursiva para cada fator primo.
É fácil mostrar que o FACTOR está em NP . Uma testemunha apropriada seria simplesmente o fator d si e todo o verificador teria que fazer é verificar se d ≤ m e n mod d = 0. Tudo isso pode ser feito em tempo polinomial. (Lembre-se, novamente, que é o comprimento da codificação que conta e que é logarítmico em n .)
Se você pode mostrar que o FACTOR também está em P , pode ter certeza de receber muitos prêmios legais. (E você quebrou uma parte significativa da criptografia de hoje.)
Para cada idioma no NP , existe um algoritmo de força bruta que o decide deterministicamente. Simplesmente realiza uma pesquisa exaustiva sobre todas as testemunhas. (Observe que o comprimento máximo de uma testemunha é limitado por um polinômio.) Portanto, seu algoritmo para decidir PRIMES era na verdade um algoritmo de força bruta para decidir COMPOSITE.
Para responder à sua pergunta final, precisamos introduzir redução . Reduções são um conceito muito poderoso de ciência da computação teórica. Reduzir um problema para outro basicamente significa resolver um problema por meio da resolução de outro problema.
Vamos Σ ser um alfabeto e A e B ser línguas mais de Σ . Uma é de tempo polinomial muitos-ona redutível a B se existe uma função f : Σ * → Σ * com as seguintes propriedades.
- w ∈ A ⇔ f ( w ) ∈ B para todos os w ∈ Σ * .
- A função f pode ser calculada por uma máquina de Turing para cada entrada w em um número de etapas delimitadas por um polinômio em | w |
Neste caso, podemos escrever A ≤ p B .
Por exemplo, seja A o idioma que contém todos os gráficos (codificados como matriz de adjacência) que contêm um triângulo. (Um triângulo é um ciclo de comprimento 3.) Seja B ainda o idioma que contém todas as matrizes com traço diferente de zero. (O traço de uma matriz é a soma dos seus principais elementos da diagonal.) Então Uma é de tempo polinomial muitos-redutível a um B . Para provar isso, precisamos encontrar uma função de transformação apropriada f . Neste caso, podemos definir f para calcular a 3 rd poder da matriz de adjacência. Isso requer dois produtos matriz-matriz, cada um com complexidade polinomial.
É trivialmente verdade que L ≤ p L . (Você pode provar isso formalmente?)
Vamos aplicar isso ao NP agora.
Uma linguagem L é NP - difícil se e somente se L '≤ p L para cada idioma L ' ∈ NP .
Um idioma NP- difícil pode ou não estar no próprio NP .
Um idioma L é NP - completo se e somente se
A linguagem mais famosa de NP- completo é SAT. Ele contém todas as fórmulas booleanas que podem ser satisfeitas. Por exemplo, ( a ∨ b ) ∧ (¬ a ∨ ¬ b ) AT SAT. Uma testemunha válida é { a = 1, b = 0}. A fórmula ( a ∨ b ) ∧ (¬ a ∨ b ) ¬ b ∉ SAT. (Como você provaria isso?)
Não é difícil mostrar que SAT ∈ NP . Mostrar a dureza NP do SAT é um trabalho, mas foi feito em 1971 por Stephen Cook .
Uma vez conhecido esse idioma completo NP , era relativamente simples mostrar a completude NP de outros idiomas via redução. Se a linguagem A é conhecida por ser NP- difícil, então mostra que A ≤ p B mostra que B também é NP- difícil (através da transitividade de "≤ p "). Em 1972, Richard Karp publicou uma lista de 21 idiomas que ele poderia mostrar como NP-completa via redução (transitiva) de SAT. (Este é o único documento nesta resposta que eu realmente recomendo que você leia. Ao contrário dos outros, não é difícil de entender e dá uma idéia muito boa de como funciona a comprovação da completude do NP via redução.)
Finalmente, um breve resumo. Usaremos os símbolos NPH e NPC para denotar as classes dos idiomas NP- hard e NP- complete respectivamente.
- P ⊆ NP
- NPC ⊂ NP e NPC ⊂ NPH , na verdade NPC = NP ∩ NPH por definição
- ( A ∈ NP ) ∧ ( B ∈ NPH ) ⇒ A ≤ p B
Observe que a inclusão NPC ⊂ NP é adequada mesmo no caso de P = NP . Para ver isso, deixe claro que nenhuma linguagem não trivial pode ser reduzida a uma linguagem trivial e existem idiomas triviais em P , bem como idiomas não triviais em NP . Este é um argumento (não muito interessante).
Termo aditivo
Sua principal fonte de confusão parece ser que você estava pensando no " n " em " O ( n ↦ f ( n ))" como a interpretação da entrada de um algoritmo quando na verdade se refere ao comprimento da entrada. Essa é uma distinção importante porque significa que a complexidade assintótica de um algoritmo depende da codificação usada para a entrada.
Nesta semana, foi alcançado um novo recorde para o maior primo conhecido de Mersenne . O maior número primo atualmente conhecido é 2 74 207 281 - 1. Esse número é tão grande que me causa dor de cabeça; portanto, usarei um número menor no exemplo a seguir: 2 31 - 1 = 2 147 483 647. Ele pode ser codificado de maneiras diferentes.
- pelo seu expoente Mersenne como número decimal:
31
(2 bytes)
- como número decimal:
2147483647
(10 bytes)
- como número unário: em
11111…11
que o …
deve ser substituído por 2 147 483 640 mais 1
s (quase 2 GiB)
Todas essas cadeias codificam o mesmo número e, dado qualquer uma delas, podemos facilmente construir qualquer outra codificação do mesmo número. (Você pode substituir a codificação decimal por binária, octal ou hexadecimal, se desejar. Isso altera apenas o comprimento por um fator constante.)
O algoritmo ingênuo para testar a primalidade é apenas polinomial para codificações unárias. O teste de primalidade AKS é polinomial para decimal (ou qualquer outra base b ≥ 2). O teste de primalidade de Lucas-Lehmer é o algoritmo mais conhecido para Mersenne inicia M p com p um primo ímpar, mas ainda é exponencial no comprimento da codificação binária do expoente de Mersenne p (polinômio em p ).
Se quisermos falar sobre a complexidade de um algoritmo, é muito importante esclarecermos com precisão a representação que usamos. Em geral, pode-se supor que a codificação mais eficiente seja usada. Ou seja, binário para números inteiros. (Observe que nem todo número primo é um primo Mersenne, portanto, o uso do expoente Mersenne não é um esquema geral de codificação.)
Na criptografia teórica, muitos algoritmos passam formalmente por uma sequência completamente inútil de k 1
s como o primeiro parâmetro. O algoritmo nunca analisa esse parâmetro, mas permite que ele seja formalmente polinomial em k , que é o parâmetro de segurança usado para ajustar a segurança do procedimento.
Para alguns problemas para os quais a linguagem de decisão na codificação binária é NP- completa, a linguagem de decisão não é mais NP- completa se a codificação de números incorporados for alterada para unária. As linguagens de decisão para outros problemas permanecem NP- completas mesmo assim. Estes últimos são chamados fortemente de NP- completos . O exemplo mais conhecido é o empacotamento de caixas .
Também é (e talvez mais) interessante ver como a complexidade de um algoritmo muda se a entrada é compactada . Para o exemplo de primos de Mersenne, vimos três codificações, cada uma das quais é logaritmicamente mais compactada que seu antecessor.
Em 1983, Hana Galperin e Avi Wigderson escreveram um artigo interessante sobre a complexidade de algoritmos comuns de gráfico quando a codificação de entrada do gráfico é compactada logaritmicamente. Para essas entradas, a linguagem dos gráficos contendo um triângulo de cima (onde estava claramente em P ) repentinamente se torna NP .
E isso ocorre porque classes de idiomas como P e NP são definidas para idiomas , não para problemas .