La prima cosa da capire è che P e NP classificano le lingue , non i problemi . Per capire cosa significhi, abbiamo prima bisogno di alcune altre definizioni.
Un alfabeto è un insieme finito di simboli non vuoto.
{ 0
, 1
} è un alfabeto così come il set di caratteri ASCII. {} non è un alfabeto perché è vuoto. N (numeri interi) non è un alfabeto perché non è finito.
Sia Σ un alfabeto. Una concatenazione ordinata di un numero finito di simboli da Σ è chiamata parola sopra Σ .
La stringa 101
è una parola sopra l'alfabeto { 0
, 1
}. La parola vuota (spesso scritta come ε ) è una parola su qualsiasi alfabeto. La stringa penguin
è una parola sopra l'alfabeto contenente i caratteri ASCII. La notazione decimale del numero π non è una parola sopra l'alfabeto { .
, 0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
} perché non è finita.
La lunghezza di una parola w , scritta come | w |, è il numero di simboli in essa contenuti.
Ad esempio, | hello
| = 5 e | ε | = 0. Per ogni parola w , | w | ∈ N e quindi finito.
Sia Σ un alfabeto. L'insieme Σ * contiene tutte le parole sopra Σ , incluso ε . L'insieme Σ + contiene tutte le parole sopra Σ , escluso ε . Per n ∈ N , Σ n è l'insieme di parole di lunghezza n .
Per ogni alfabeto Σ , Σ * e Σ + sono insiemi numerabili infiniti . Per il set di caratteri ASCII Σ ASCII , le espressioni regolari .*
e .+
denotano Σ ASCII * e Σ ASCII + rispettivamente.
{ 0
, 1
} 7 è l'insieme di codici ASCII a 7 bit { 0000000
, 0000001
, ..., 1111111
}. { 0
, 1
} 32 è l'insieme di valori interi a 32 bit.
Sia Σ un alfabeto e L ⊆ Σ * . L è chiamata lingua sopra Σ .
Per un alfabeto Σ , l'insieme vuoto e Σ * sono lingue banali oltre Σ . Il primo viene spesso definito linguaggio vuoto . La lingua vuota {} e la lingua contenente solo la parola vuota { ε } sono diverse.
Il sottoinsieme di { 0
, 1
} 32 che corrisponde a valori in virgola mobile IEEE 754 non NaN è un linguaggio finito.
Le lingue possono avere un numero infinito di parole ma ogni lingua è numerabile. L'insieme di stringhe { 1
, 2
...} che denota i numeri interi in notazione decimale è un linguaggio infinita sopra l'alfabeto { 0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
}. L'insieme infinito di stringhe { 2
, 3
, 5
, 7
, 11
, 13
, ...} che denota i numeri primi in notazione decimale è un sottoinsieme corretto della stessa. Il linguaggio che contiene tutte le parole che corrispondono all'espressione regolare [+-]?\d+\.\d*([eE][+-]?\d+)?
è un linguaggio sul set di caratteri ASCII (che indica un sottoinsieme delle espressioni in virgola mobile valide definite dal linguaggio di programmazione C).
Non esiste un linguaggio che contenga tutti i numeri reali (in nessuna notazione) perché l'insieme dei numeri reali non è numerabile.
Sia Σ un alfabeto e L ⊆ Σ * . Una macchina D decide L se per ogni input w ∈ Σ * calcola la funzione caratteristica χ L ( w ) in tempo finito. La funzione caratteristica è definita come
χ L : Σ * → {0, 1}
w ↦ 1, w ∈ L
0, altrimenti.
Tale macchina è chiamata decisiva per L . Scriviamo “ D ( w ) = x ” per “dato w , uscite D x ”.
Esistono molti modelli di macchine. Quello più generale oggi in uso pratico è il modello di una macchina Turing . Una macchina di Turing ha memoria lineare illimitata raggruppata in celle. Ogni cella può contenere esattamente un simbolo di un alfabeto in qualsiasi momento. La macchina di Turing esegue il suo calcolo come una sequenza di fasi di calcolo. In ogni passaggio, può leggere una cella, eventualmente sovrascriverne il valore e spostare la testina di lettura / scrittura di una posizione sulla cella sinistra o destra. Quale azione eseguirà la macchina è controllata da un automa a stati finiti.
Una macchina ad accesso casuale con un set limitato di istruzioni e spazio di archiviazione illimitato è un altro modello di macchina potente quanto il modello di macchina di Turing.
Per motivi di questa discussione, non ci disturberemo con il modello di macchina preciso che usiamo, ma piuttosto basti dire che la macchina ha un'unità di controllo deterministica finita, memoria illimitata ed esegue un calcolo come una sequenza di passi che possono essere contati.
Dato che l'hai usato nella tua domanda, presumo che tu abbia già familiarità con la notazione "big-O", quindi ecco solo un rapido aggiornamento.
Sia f : N → una funzione. L'insieme O ( f ) contiene tutte le funzioni g : N → N per le quali esistono costanti n 0 ∈ N e c ∈ N tali che per ogni n ∈ N con n > n 0 è vero che g ( n ) ≤ c f ( n ).
Ora siamo pronti ad affrontare la vera domanda.
La classe P contiene tutte le lingue L per le quali esiste una macchina di Turing D che decide L e una costante k ∈ N tale che per ogni input w , D si ferma dopo al massimo T (| w |) passi per una funzione T ∈ O ( n ↦ n k ).
Poiché O ( n ↦ n k ), sebbene matematicamente corretto, è scomodo da scrivere e leggere, la maggior parte delle persone - a dire il vero, tutti tranne me stesso - di solito scrive semplicemente O ( n k ).
Si noti che il limite dipende dalla lunghezza di w . Pertanto, l'argomento che fai per il linguaggio dei numeri primi è corretto solo per i numeri nelle codifiche unaray , dove per la codifica w di un numero n , la lunghezza della codifica | w | è proporzionale a n . Nessuno userebbe mai tale codifica in pratica. Utilizzando un algoritmo più avanzato del semplice tentativo di provare tutti i possibili fattori, si può dimostrare, tuttavia, che il linguaggio dei numeri primi rimane in P se gli input sono codificati in binario (o su qualsiasi altra base). (Nonostante il grande interesse, questo potrebbe essere dimostrato solo da Manindra Agrawal, Neeraj Kayal e Nitin Saxena in un documento pluripremiato del 2004, quindi puoi immaginare che l'algoritmo non sia molto semplice.)
I linguaggi banali {} e Σ * e il linguaggio non banale { ε } sono ovviamente in P (per qualsiasi alfabeto Σ ). Riesci a scrivere funzioni nel tuo linguaggio di programmazione preferito che prendono una stringa come input e restituiscono un valore booleano che dice se la stringa è una parola dalla lingua per ognuna di queste e dimostra che la tua funzione ha una complessità runtime polinomiale?
Ogni regolare linguaggio (un linguaggio descritto da un'espressione regolare) è in P .
Sia Σ un alfabeto e L ⊆ Σ * . Una macchina V che accetta una tupla codificata di due parole w , c ∈ Σ * e genera 0 o 1 dopo un numero finito di passi è un verificatore per L se ha le seguenti proprietà.
- Data ( w , c ), V uscite 1 solo se w ∈ L .
- Per ogni w ∈ L , esiste una c ∈ Σ * tale che V ( w , c ) = 1.
La c nella definizione sopra è chiamata testimone (o certificato ).
Un verificatore è autorizzato a dare falsi negativi per la testimonianza sbagliato anche se w in realtà è in L . Non è tuttavia consentito dare falsi positivi. È inoltre richiesto che per ogni parola nella lingua esista almeno un testimone.
Per il linguaggio COMPOSITO, che contiene le codifiche decimali di tutti i numeri interi che non sono primi, un testimone potrebbe essere una fattorizzazione. Ad esempio, (659, 709)
è testimone di 467231
∈ COMPOSITO. Puoi facilmente verificare che su un foglio di carta senza il testimone dato, dimostrando che 467231 non è primo sarebbe difficile senza usare un computer.
Non abbiamo detto nulla su come trovare un testimone appropriato. Questa è la parte non deterministica.
La classe NP contiene tutte le lingue L per le quali esiste una macchina di Turing V che verifica L e una costante k ∈ N tale che per ogni input ( w , c ), V si ferma dopo al massimo T (| w |) passi per una funzione T ∈ O ( n ↦ n k ).
Si noti che la definizione di cui sopra implica che per ogni w ∈ L esiste un testimone c con | c | ≤ T (| w |). (La macchina di Turing non può assolutamente guardare più simboli del testimone.)
NP è un superset di P (perché?). Non è noto se esistono lingue che sono NP , ma non in P .
La fattorizzazione in numeri interi non è una lingua in sé. Tuttavia, possiamo costruire un linguaggio che rappresenti il problema decisionale ad esso associato. Cioè, una lingua che contiene tutte le tuple ( n , m ) tale che n ha un fattore d con d ≤ m . Chiamiamo questa lingua FATTORE. Se si dispone di un algoritmo per decidere FACTOR, può essere utilizzato per calcolare una fattorizzazione completa con un solo sovraccarico polinomiale eseguendo una ricerca binaria ricorsiva per ciascun fattore primo.
È facile dimostrare che FACTOR è in NP . Un testimone appropriato sarebbe semplicemente il fattore d stesso e tutto ciò che il verificatore dovrebbe fare è verificare che d ≤ m e n mod d = 0. Tutto ciò può essere fatto in tempo polinomiale. (Ricorda, ancora una volta, che è la lunghezza della codifica che conta e che è logaritmica in n .)
Se puoi mostrare che FACTOR è anche in P , puoi essere sicuro di ottenere molti premi interessanti. (E hai rotto una parte significativa della crittografia di oggi.)
Per ogni linguaggio in NP , esiste un algoritmo a forza bruta che lo decide in modo deterministico. Esegue semplicemente una ricerca esaustiva su tutti i testimoni. (Nota che la lunghezza massima di un testimone è limitata da un polinomio.) Quindi, il tuo algoritmo per decidere PRIMES era in realtà un algoritmo a forza bruta per decidere COMPOSITO.
Per rispondere alla tua domanda finale, dobbiamo introdurre una riduzione . Le riduzioni sono un concetto molto potente di informatica teorica. Ridurre un problema ad un altro significa fondamentalmente risolvere un problema mediante la risoluzione di un altro problema.
Sia Σ un alfabeto e A e B siano le lingue sopra Σ . A è più volte polinomiale riducibile a B se esiste una funzione f : Σ * → Σ * con le seguenti proprietà.
- w ∈ A ⇔ f ( w ) ∈ B per tutto w ∈ Σ * .
- La funzione f può essere calcolata da una macchina di Turing per ogni input w in una serie di passaggi delimitati da un polinomio in | w |.
In questo caso, scriviamo A ≤ P B .
Ad esempio, sia A il linguaggio che contiene tutti i grafici (codificati come matrice di adiacenza) che contengono un triangolo. (Un triangolo è un ciclo di lunghezza 3.) Sia B ulteriormente la lingua che contiene tutte le matrici con traccia diversa da zero. (La traccia di una matrice è la somma dei suoi principali elementi diagonali.) Quindi A è polinomiale molti-one riducibile a B . Per dimostrarlo, dobbiamo trovare una funzione di trasformazione appropriata f . In questo caso, possiamo impostare f per calcolare la terza potenza della matrice di adiacenza. Ciò richiede due prodotti matrice-matrice, ognuno dei quali presenta una complessità polinomiale.
È banalmente vero che L ≤ P L . (Puoi provarlo formalmente?)
Lo applicheremo ora a NP .
Una lingua L è NP -hard se e solo se L '≤ p L per ogni lingua L ' ∈ NP .
Una lingua di NP -hard può o meno essere nella stessa NP .
Un linguaggio L è NP -Complete se e solo se
Il linguaggio completo NP più famoso è SAT. Contiene tutte le formule booleane che possono essere soddisfatte. Ad esempio, ( a ∨ b ) ∧ (¬ a ∨ ¬ b ) ∈ SAT. Un testimone valido è { a = 1, b = 0}. La formula ( a ∨ b ) ∧ (¬ a ∨ b ) ∧ ¬ b ∉ SAT. (Come lo dimostreresti?)
Non è difficile dimostrare che SAT ∈ NP . Mostrare il NP -hardness di SAT è un po 'di lavoro, ma è stato fatto nel 1971 da Stephen Cook .
Una volta conosciuta una lingua completa NP , era relativamente semplice mostrare la completezza NP di altre lingue tramite riduzione. Se la lingua A è nota per essere NP -hard, allora mostrando che A ≤ p B mostra che anche B è NP -hard (tramite la transitività di "≤ p "). Nel 1972 Richard Karp pubblicò un elenco di 21 lingue che poteva mostrare erano NP-completo tramite riduzione (transitiva) di SAT. (Questo è l'unico documento in questa risposta che consiglio vivamente di leggere. A differenza degli altri, non è difficile da capire e dà un'ottima idea di come funziona la dimostrazione della completezza NP attraverso la riduzione.)
Finalmente un breve riassunto. Useremo i simboli NPH e NPC per indicare rispettivamente le classi delle lingue complete NP -hard e NP - complete .
- P ⊆ NP
- NPC ⊂ NP e NPC ⊂ NPH , in realtà NPC = NP ∩ NPH per definizione
- ( A ∈ NP ) ∧ ( B ∈ NPH ) ⇒ A ≤ p B
Si noti che l'inclusione NPC ⊂ NP è corretta anche nel caso in cui P = NP . Per vedere questo, renditi chiaro che nessun linguaggio non banale può essere ridotto a un linguaggio banale e ci sono linguaggi banali in P e linguaggi non banali in NP . Questo è un caso angolare (non molto interessante), però.
appendice
La tua principale fonte di confusione sembra essere che stavi pensando alla " n " in " O ( n ↦ f ( n ))" come l' interpretazione dell'input di un algoritmo quando si riferisce effettivamente alla lunghezza dell'input. Questa è una distinzione importante perché significa che la complessità asintotica di un algoritmo dipende dalla codifica utilizzata per l'input.
Questa settimana è stato raggiunto un nuovo record per il più grande numero primo noto di Mersenne . Il numero primo più grande attualmente conosciuto è 2 74 207 281 - 1. Questo numero è così grande che mi dà mal di testa, quindi ne userò uno più piccolo nel seguente esempio: 2 31 - 1 = 2 147 483 647. Può essere codificato in diversi modi.
- dal suo esponente Mersenne come numero decimale:
31
(2 byte)
- come numero decimale:
2147483647
(10 byte)
- come numero unario:
11111…11
dove …
deve essere sostituito da 2 147 483 640 più 1
s (quasi 2 GiB)
Tutte queste stringhe codificano lo stesso numero e dato uno di questi, possiamo facilmente costruire qualsiasi altra codifica dello stesso numero. (Se lo si desidera, è possibile sostituire la codifica decimale con binaria, ottale o esadecimale. Cambia la lunghezza solo con un fattore costante.)
L'algoritmo ingenuo per testare la primalità è solo polinomiale per codifiche unarie. Il test di primalità AKS è polinomiale per decimale (o qualsiasi altra base b ≥ 2). Il test di primalità di Lucas-Lehmer è l'algoritmo più noto per i numeri primi di Mersenne M p con p un numero dispari, ma è ancora esponenziale nella lunghezza della codifica binaria dell'esponente Mersenne p (polinomio in p ).
Se vogliamo parlare della complessità di un algoritmo, è molto importante che siamo molto chiari su quale rappresentazione utilizziamo. In generale, si può presumere che venga utilizzata la codifica più efficiente. Cioè, binario per numeri interi. (Nota che non tutti i numeri primi sono numeri primi di Mersenne, quindi l'uso dell'esponente Mersenne non è uno schema di codifica generale.)
Nella crittografia teorica, molti algoritmi hanno formalmente passato una stringa completamente inutile di k 1
come primo parametro. L'algoritmo non esamina mai questo parametro ma gli consente di essere formalmente polinomiale in k , che è il parametro di sicurezza utilizzato per ottimizzare la sicurezza della procedura.
Per alcuni problemi per i quali il linguaggio decisionale nella codifica binaria è NP- completo, il linguaggio decisionale non è più NP- completo se la codifica dei numeri incorporati viene commutata su unaria. Le lingue delle decisioni per altri problemi rimangono NP-complete anche allora. Questi ultimi sono chiamati fortemente NP- completi . L'esempio più noto è l' imballaggio del contenitore .
È anche (e forse più) interessante vedere come cambia la complessità di un algoritmo se l'input è compresso . Nell'esempio dei numeri primi di Mersenne, abbiamo visto tre codifiche, ognuna delle quali è logaritmicamente più compressa rispetto al suo predecessore.
Nel 1983, Hana Galperin e Avi Wigderson hanno scritto un articolo interessante sulla complessità degli algoritmi grafici comuni quando la codifica di input del grafico è compressa logaritmicamente. Per questi input, il linguaggio dei grafici contenenti un triangolo dall'alto (dove era chiaramente in P ) diventa improvvisamente completo di NP .
E questo perché le classi linguistiche come P e NP sono definite per le lingue , non per i problemi .