Lo primero que hay que entender es que P y NP clasifican lenguajes , no problemas . Para entender lo que esto significa, necesitamos algunas otras definiciones primero.
Un alfabeto es un conjunto finito no vacío de símbolos.
{ 0
, 1
} es un alfabeto como es el conjunto de caracteres ASCII. {} no es un alfabeto porque está vacío. N (los enteros) no es un alfabeto porque no es finito.
Deje Σ ser un alfabeto. Una concatenación ordenada de un número finito de símbolos de Σ se llama palabra sobre Σ .
La cadena 101
es una palabra sobre el alfabeto { 0
, 1
}. La palabra vacía (a menudo escrita como ε ) es una palabra sobre cualquier alfabeto. La cadena penguin
es una palabra sobre el alfabeto que contiene los caracteres ASCII. La notación decimal del número π no es una palabra sobre el alfabeto { .
, 0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
} porque no es finito.
La longitud de una palabra w , escrita como | w |, es el número de símbolos que contiene.
Por ejemplo, | hello
El | = 5 y | ε | = 0. Para cualquier palabra w , | w | ∈ N y por lo tanto finito.
Deje Σ ser un alfabeto. El conjunto Σ * contiene todas las palabras sobre Σ , incluido ε . El conjunto Σ + contiene todas las palabras sobre Σ , excluyendo ε . Para n ∈ N , Σ n es el conjunto de palabras de longitud n .
Para cada alfabeto Σ , Σ * y Σ + son conjuntos contables infinitos . Para el conjunto de caracteres ASCII Σ ASCII , las expresiones regulares .*
y .+
denotan Σ ASCII * y Σ ASCII + respectivamente.
{ 0
, 1
} 7 es el conjunto de códigos ASCII de 7 bits { 0000000
, 0000001
..., 1111111
}. { 0
, 1
} 32 es el conjunto de valores enteros de 32 bits.
Deje Σ ser un alfabeto y L ⊆ Σ * . L se llama lenguaje sobre Σ .
Para un alfabeto Σ , el conjunto vacío y Σ * son idiomas triviales sobre Σ . El primero a menudo se conoce como el lenguaje vacío . El idioma vacío {} y el idioma que contiene solo la palabra vacía { ε } son diferentes.
El subconjunto de { 0
, 1
} 32 que corresponde a valores de punto flotante no NaN IEEE 754 es un lenguaje finito.
Los idiomas pueden tener un número infinito de palabras, pero cada idioma es contable. El conjunto de cadenas { 1
, 2
, ...} denota los números enteros en notación decimal es un lenguaje infinito sobre el alfabeto { 0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
}. El conjunto infinito de cadenas { 2
, 3
, 5
, 7
, 11
, 13
, ...} denota los números primos en notación decimal es un subconjunto propio de los mismos. El lenguaje que contiene todas las palabras que coinciden con la expresión regular [+-]?\d+\.\d*([eE][+-]?\d+)?
es un lenguaje sobre el conjunto de caracteres ASCII (que denota un subconjunto de las expresiones de coma flotante válidas según lo definido por el lenguaje de programación C).
No hay lenguaje que contenga todos los números reales (en cualquier notación) porque el conjunto de números reales no es contable.
Deje Σ ser un alfabeto y L ⊆ Σ * . Una máquina D decide L si para cada entrada w ∈ Σ * calcula la función característica χ L ( w ) en tiempo finito. La función característica se define como
χ L : Σ * → {0, 1}
w ↦ 1, w ∈ L
0, de lo contrario.
Una máquina de este tipo se llama un decisivo para L . Escribimos " D ( w ) = x " para "dado w , D salidas x ".
Hay muchos modelos de máquinas. El más general que se usa actualmente en la práctica es el modelo de una máquina de Turing . Una máquina Turing tiene almacenamiento lineal ilimitado agrupado en celdas. Cada celda puede contener exactamente un símbolo de un alfabeto en cualquier momento. La máquina de Turing realiza su cálculo como una secuencia de pasos de cálculo. En cada paso, puede leer una celda, posiblemente sobrescribir su valor y mover el cabezal de lectura / escritura en una posición a la celda izquierda o derecha. La acción que realizará la máquina está controlada por un autómata de estado finito.
Una máquina de acceso aleatorio con un conjunto finito de instrucciones y almacenamiento ilimitado es otro modelo de máquina que es tan poderoso como el modelo de máquina de Turing.
En aras de esta discusión, no nos molestaremos con el modelo de máquina preciso que usamos, sino que será suficiente decir que la máquina tiene una unidad de control determinista finita, almacenamiento ilimitado y realiza un cálculo como una secuencia de pasos que se pueden contar.
Como lo ha usado en su pregunta, supongo que ya está familiarizado con la notación "big-O", así que aquí solo le ofrecemos un breve repaso.
Deje f : N → ser una función. El conjunto O ( f ) contiene todas las funciones g : N → N para las cuales existen constantes n 0 ∈ N y c ∈ N de manera que por cada n ∈ N con n > n 0 es cierto que g ( n ) ≤ c f ( n )
Ahora estamos preparados para abordar la verdadera pregunta.
La clase P contiene todos los lenguajes L para los cuales existe una máquina Turing D que decide L y una constante k ∈ N tal que para cada entrada w , D se detiene después de a lo sumo T (| w |) pasos para una función T ∈ O ( n ↦ n k ).
Dado que O ( n ↦ n k ), aunque matemáticamente correcto, es inconveniente para escribir y leer, la mayoría de las personas, para ser honesto, todos excepto yo, generalmente escriben simplemente O ( n k ).
Tenga en cuenta que el límite depende de la longitud de w . Por lo tanto, el argumento que hace para el lenguaje de los números primos solo es correcto para los números en codificaciones sin matriz , donde para la codificación w de un número n , la longitud de la codificación | w | es proporcional a n . Nadie usaría tal codificación en la práctica. Sin embargo, utilizando un algoritmo más avanzado que simplemente probando todos los factores posibles, se puede demostrar que el lenguaje de los números primos permanece en P si las entradas están codificadas en binario (o en cualquier otra base). (A pesar del gran interés, esto solo pudo ser probado por Manindra Agrawal, Neeraj Kayal y Nitin Saxena en un artículo galardonado en 2004, por lo que puede adivinar que el algoritmo no es muy simple).
Los idiomas triviales {} y Σ * y el lenguaje no trivial { ε } están obviamente en P (para cualquier alfabeto Σ ). ¿Puede escribir funciones en su lenguaje de programación favorito que tome una cadena como entrada y devuelva un valor booleano que indique si la cadena es una palabra del lenguaje para cada una de ellas y demuestre que su función tiene una complejidad de tiempo de ejecución polinómica?
Cada regular de idioma (un lenguaje descrito por una expresión regular) está en P .
Deje Σ ser un alfabeto y L ⊆ Σ * . Una máquina V que toma una tupla codificada de dos palabras w , c ∈ Σ * y genera 0 o 1 después de un número finito de pasos es un verificador para L si tiene las siguientes propiedades.
- Dada ( w , c ), V salidas 1 sólo si w ∈ L .
- Para cada w ∈ L , existe una c ∈ Σ * tal que V ( w , c ) = 1.
La c en la definición anterior se llama testigo (o certificado ).
Se permite un verificador para dar falsos negativos para el testigo mal, incluso si w realidad está en L . Sin embargo, no está permitido dar falsos positivos. También se requiere que para cada palabra en el idioma, exista al menos un testigo.
Para el lenguaje COMPUESTO, que contiene las codificaciones decimales de todos los enteros que no son primos, un testigo podría ser una factorización. Por ejemplo, (659, 709)
es testigo de 467231
∈ COMPOSITE. Puede verificarlo fácilmente en una hoja de papel sin contar con el testigo, demostrando que 467231 no es primo sería difícil sin usar una computadora.
No dijimos nada sobre cómo se puede encontrar un testigo apropiado. Esta es la parte no determinista.
La clase NP contiene todos los lenguajes L para los que existe una máquina de Turing V que verifica L y una constante k ∈ N tal que para cada entrada ( w , c ), V se detiene después de la mayoría de los pasos T (| w |) para una función T ∈ O ( n ↦ n k ).
Tenga en cuenta que la definición anterior implica que para cada w ∈ L existe un testigo c con | c | ≤ T (| w |). (La máquina de Turing no puede mirar más símbolos del testigo).
NP es un superconjunto de P (¿por qué?). No se sabe si existen lenguas que están en NP , pero no en P .
La factorización de enteros no es un lenguaje per se. Sin embargo, podemos construir un lenguaje que represente el problema de decisión asociado con él. Es decir, un lenguaje que contiene todas las tuplas ( n , m ) de modo que n tiene un factor d con d ≤ m . Llamemos a este idioma FACTOR. Si tiene un algoritmo para decidir FACTOR, se puede usar para calcular una factorización completa con solo una sobrecarga polinómica realizando una búsqueda binaria recursiva para cada factor primo.
Es fácil demostrar que FACTOR está en NP . Un testigo apropiado sería simplemente el factor d sí mismo y todo el verificador tendría que hacer es verificar que d ≤ m y n mod d = 0. Todo esto se puede hacer en tiempo polinómico. (Recuerde, de nuevo, que lo que cuenta es la longitud de la codificación y eso es logarítmico en n .)
Si puede demostrar que FACTOR también está en P , puede estar seguro de obtener muchos premios geniales. (Y has roto una porción significativa de la criptografía de hoy).
Para cada lenguaje en NP , hay un algoritmo de fuerza bruta que lo decide de manera determinista. Simplemente realiza una búsqueda exhaustiva de todos los testigos. (Tenga en cuenta que la longitud máxima de un testigo está limitada por un polinomio). Entonces, su algoritmo para decidir PRIMES fue en realidad un algoritmo de fuerza bruta para decidir COMPUESTO.
Para abordar su pregunta final, necesitamos introducir la reducción . Las reducciones son un concepto muy poderoso de la informática teórica. Reducir un problema a otro básicamente significa resolver un problema mediante la resolución de otro problema.
Deje Σ sea un alfabeto y A y B sea más idiomas Σ . A es polinomial en tiempo múltiple reducible a B si existe una función f : Σ * → Σ * con las siguientes propiedades.
- w ∈ A ⇔ f ( w ) ∈ B para todo w ∈ Σ * .
- La función f puede ser calculada por una máquina de Turing para cada entrada w en varios pasos delimitados por un polinomio en | w |.
En este caso, escribimos A ≤ p B .
Por ejemplo, deje que A sea el lenguaje que contiene todos los gráficos (codificados como matriz de adyacencia) que contienen un triángulo. (Un triángulo es un ciclo de longitud 3.) Sea más B el lenguaje que contiene todas las matrices con trazas distintas de cero. (La traza de una matriz es la suma de sus principales elementos de la diagonal.) Entonces A es de tiempo polinómico mucho-uno reducible a B . Para probar esto, necesitamos encontrar una función de transformación apropiada f . En este caso, podemos establecer f para calcular la 3 rd poder de la matriz de adyacencia. Esto requiere dos productos de matriz de matriz, cada uno de los cuales tiene una complejidad polinómica.
Es trivialmente cierto que L ≤ p L . (¿Puedes probarlo formalmente?)
Aplicaremos esto a NP ahora.
Un lenguaje L es NP- duro si y solo si L '≤ p L para cada idioma L ' ∈ NP .
Un lenguaje duro NP puede o no estar en el propio NP .
Un lenguaje L es NP- completo si y solo si
El lenguaje completo NP más famoso es SAT. Contiene todas las fórmulas booleanas que se pueden satisfacer. Por ejemplo, ( a ∨ b ) ∧ (¬ a ∨ ¬ b ) ∈ SAT. Un testigo válido es { a = 1, b = 0}. La fórmula ( a ∨ b ) ∧ (¬ a ∨ b ) ∧ ¬ b ∉ SAT. (¿Cómo probarías eso?)
No es difícil demostrar que SAT ∈ NP . Para mostrar la dureza NP de SAT es un trabajo, pero fue realizado en 1971 por Stephen Cook .
Una vez que se conocía ese lenguaje completo NP , era relativamente simple mostrar la completitud NP de otros idiomas mediante reducción. Si se sabe que el lenguaje A es NP- duro, entonces mostrar que A ≤ p B muestra que B también es NP- duro (a través de la transitividad de "≤ p "). En 1972 Richard Karp publicó una lista de 21 idiomas que podía mostrar eran NP-completo a través de la reducción (transitiva) de SAT. (Este es el único artículo en esta respuesta que realmente recomiendo que lea. A diferencia de los otros, no es difícil de entender y da una muy buena idea de cómo funciona probar la completitud NP mediante la reducción).
Finalmente, un breve resumen. Usaremos los símbolos NPH y NPC para denotar las clases de los idiomas NP- hard y NP -complete respectivamente.
- P ⊆ NP
- NPC ⊂ NP y NPC ⊂ NPH , en realidad NPC = NP ∩ NPH por definición
- ( A ∈ NP ) ∧ ( B ∈ NPH ) ⇒ A ≤ p B
Tenga en cuenta que la inclusión NPC ⊂ NP es adecuada incluso en el caso de que P = NP . Para ver esto, aclare que ningún lenguaje no trivial puede reducirse a uno trivial y que hay idiomas triviales en P , así como idiomas no triviales en NP . Sin embargo, este es un caso de esquina (no muy interesante).
Apéndice
Su principal fuente de confusión parece ser que estaba pensando en la " n " en " O ( n ↦ f ( n ))" como la interpretación de la entrada de un algoritmo cuando en realidad se refiere a la longitud de la entrada. Esta es una distinción importante porque significa que la complejidad asintótica de un algoritmo depende de la codificación utilizada para la entrada.
Esta semana, se logró un nuevo récord para el mayor premio Mersenne conocido . El número primo más grande actualmente conocido es 2 74 207 281 - 1. Este número es tan grande que me da dolor de cabeza, así que usaré uno más pequeño en el siguiente ejemplo: 2 31 - 1 = 2 147 483 647. Puede ser codificado de diferentes maneras.
- por su exponente de Mersenne como número decimal:
31
(2 bytes)
- como número decimal:
2147483647
(10 bytes)
- como número unario:
11111…11
donde …
será reemplazado por 2 147 483 640 más 1
s (casi 2 GiB)
Todas estas cadenas codifican el mismo número y, dado cualquiera de estos, podemos construir fácilmente cualquier otra codificación del mismo número. (Si lo desea, puede reemplazar la codificación decimal por binaria, octal o hexadecimal. Solo cambia la longitud por un factor constante).
El algoritmo ingenuo para probar la primalidad es solo polinomial para codificaciones unarias. La prueba de primalidad AKS es polinomial para decimal (o cualquier otra base b ≥ 2). La prueba de primalidad de Lucas-Lehmer es el algoritmo más conocido para los primos de Mersenne M p con p un primo impar, pero sigue siendo exponencial en la longitud de la codificación binaria del exponente de Mersenne p (polinomio en p ).
Si queremos hablar sobre la complejidad de un algoritmo, es muy importante que tengamos muy claro qué representación usamos. En general, se puede suponer que se utiliza la codificación más eficiente. Es decir, binario para enteros. (Tenga en cuenta que no todos los números primos son primos de Mersenne, por lo que usar el exponente de Mersenne no es un esquema de codificación general).
En la criptografía teórica, a muchos algoritmos se les pasa formalmente una cadena de k 1
s completamente inútil como primer parámetro. El algoritmo nunca analiza este parámetro, pero le permite ser formalmente polinomial en k , que es el parámetro de seguridad utilizado para ajustar la seguridad del procedimiento.
Para algunos problemas para los cuales el lenguaje de decisión en la codificación binaria es NP- completo, el lenguaje de decisión ya no es NP- completo si la codificación de números incrustados se cambia a unario. Los lenguajes de decisión para otros problemas siguen siendo NP completos incluso entonces. Estos últimos se denominan fuertemente NP- completo . El ejemplo más conocido es el embalaje de contenedores .
También es (y quizás más) interesante ver cómo cambia la complejidad de un algoritmo si la entrada se comprime . Para el ejemplo de los primos de Mersenne, hemos visto tres codificaciones, cada una de las cuales está logarítmicamente más comprimida que su predecesora.
En 1983, Hana Galperin y Avi Wigderson escribieron un artículo interesante sobre la complejidad de los algoritmos de gráficos comunes cuando la codificación de entrada del gráfico se comprime logarítmicamente. Para estas entradas, el lenguaje de los gráficos que contienen un triángulo desde arriba (donde estaba claramente en P ) de repente se convierte en NP- completo.
Y eso se debe a que las clases de idiomas como P y NP están definidas para idiomas , no para problemas .