Come ingannare l'euristica "provare alcuni casi di prova": algoritmi che sembrano corretti, ma in realtà non corretti


105

Per provare a verificare se un algoritmo per qualche problema è corretto, il solito punto di partenza è provare a eseguire l'algoritmo manualmente su una serie di semplici casi di test - provalo su alcuni casi esemplificativi, inclusi alcuni semplici "casi angolari" ". Questa è una grande euristica: è un ottimo modo per eliminare rapidamente molti tentativi errati di un algoritmo e per capire perché l'algoritmo non funziona.

Tuttavia, quando apprendono gli algoritmi, alcuni studenti sono tentati di fermarsi qui: se il loro algoritmo funziona correttamente su una manciata di esempi, compresi tutti i casi angolari che possono pensare di provare, allora concludono che l'algoritmo deve essere corretto. C'è sempre uno studente che chiede: "Perché devo dimostrare il mio algoritmo corretto, se posso provarlo solo su alcuni casi di test?"

Quindi, come si fa a ingannare l'euristica "provare un mucchio di casi di prova"? Sto cercando alcuni buoni esempi per dimostrare che questa euristica non è abbastanza. In altre parole, sto cercando uno o più esempi di un algoritmo che apparentemente superficialmente potrebbe essere corretto e che fornisca la risposta giusta su tutti i piccoli input che chiunque probabilmente troverà, ma in cui l'algoritmo effettivamente non funziona Forse l'algoritmo funziona correttamente su tutti i piccoli input e fallisce solo per input di grandi dimensioni o fallisce solo per input con un modello insolito.

In particolare, sto cercando:

  1. Un algoritmo Il difetto deve essere a livello algoritmico. Non sto cercando bug di implementazione. (Ad esempio, come minimo, l'esempio dovrebbe essere indipendente dal linguaggio e il difetto dovrebbe riguardare problemi algoritmici piuttosto che problemi di ingegneria del software o di implementazione.)

  2. Un algoritmo che qualcuno potrebbe plausibilmente inventare. Lo pseudocodice dovrebbe apparire almeno plausibilmente corretto (ad esempio, il codice che è offuscato o ovviamente dubbioso non è un buon esempio). Punti bonus se si tratta di un algoritmo che alcuni studenti hanno escogitato durante il tentativo di risolvere un problema di compiti a casa o di esame.

  3. Un algoritmo che avrebbe superato una ragionevole strategia di test manuale con alta probabilità. Qualcuno che prova a mano alcuni piccoli casi di test dovrebbe essere improbabile che scopra il difetto. Ad esempio, "simulare QuickCheck a mano su una dozzina di piccoli casi di test" dovrebbe essere improbabile per rivelare che l'algoritmo non è corretto.

  4. Preferibilmente, un algoritmo deterministico. Ho visto molti studenti pensare che "provare alcuni casi di test a mano" è un modo ragionevole per verificare se un algoritmo deterministico è corretto, ma sospetto che la maggior parte degli studenti non ritenga che provare alcuni casi di test sia un buon modo per verificare probabilistici algoritmi. Per gli algoritmi probabilistici, spesso non c'è modo di dire se un determinato output è corretto; e non è possibile manovrare a sufficienza esempi per eseguire test statistici utili sulla distribuzione dell'output. Quindi, preferirei concentrarmi sugli algoritmi deterministici, poiché arrivano più chiaramente al cuore delle idee sbagliate degli studenti.

Mi piacerebbe insegnare l'importanza di dimostrare il tuo algoritmo corretto e spero di usare alcuni esempi come questo per motivare le prove di correttezza. Preferirei esempi relativamente semplici e accessibili agli studenti; esempi che richiedono macchinari pesanti o tonnellate di background matematico / algoritmico sono meno utili. Inoltre, non voglio algoritmi "innaturali"; mentre potrebbe essere facile costruire uno strano algoritmo artificiale per ingannare l'euristico, se sembra altamente innaturale o ha un'ovvia backdoor costruita solo per ingannare questo euristico, probabilmente non sarà convincente per gli studenti. Qualche buon esempio?


2
Adoro la tua domanda, è anche collegata a una domanda molto interessante che ho visto su Mathematics l'altro giorno relativa alla confutazione di congetture con grandi costanti. Puoi trovarlo qui
ZeroUltimax,

1
Qualche altro scavo e ho trovato quei due algoritmi geometrici.
ZeroUltimax,

@ZeroUltimax Hai ragione, non è garantito che il pt centrale di 3 punti non lineari si trovi all'interno. Il rimedio rapido è quello di ottenere un punto sulla linea tra l'estrema sinistra e l'estrema destra. C'è altro problema dove?
Informato il

La premessa di questa domanda mi sembra strana in un modo in cui ho difficoltà a farmi girare la testa, ma penso che si tratti di un processo per la progettazione di algoritmi come descritto è fondamentalmente rotto. Anche per gli studenti che non si "fermano qui" è condannato. 1> scrivere algoritmo, 2> pensare / eseguire test case, 3a> stop o 3b> dimostrarsi corretti. Il primo passo è stato quello di identificare le classi di input per il dominio problematico. I casi angolari e l'algoritmo stesso derivano da quelli. (cont)
Mr.Mindor,

1
Come si fa a distinguere formalmente un bug di implementazione da un algoritmo difettoso? Ero interessato alla tua domanda, ma allo stesso tempo ero infastidito dal fatto che la situazione che descrivi sembra essere più la regola che l'eccezione. Molte persone verificano ciò che implementano, ma di solito hanno ancora dei bug. Il secondo esempio della risposta più votata è proprio un tale bug.
babou,

Risposte:


70

Un errore comune penso sia l'uso di algoritmi avidi, che non è sempre l'approccio corretto, ma potrebbe funzionare nella maggior parte dei casi di test.

Esempio: denominazioni di monete, e un numero , esprimono come somma di : s con il minor numero di monete possibile. n n d id1,,dknndi

Un approccio ingenuo consiste nell'utilizzare prima la moneta più grande possibile e produrre avidamente una tale somma.

Ad esempio, le monete con valore , 5 e 1 daranno risposte corrette con avidità per tutti i numeri tra 1 e 14 tranne il numero 10 = 6 + 1 + 1 + 1 + 1 = 5 + 5 .65111410=6+1+1+1+1=5+5


10
Questo è davvero un buon esempio, in particolare uno che gli studenti sbagliano abitualmente. Non solo devi scegliere particolari set di monete, ma anche valori particolari per far fallire l'algoritmo.
Raffaello

2
Inoltre, lasciami dire che anche gli studenti avranno spesso prove errate in questo esempio (sfoggiando alcuni argomenti ingenui che non riescono a un esame più attento), quindi qui è possibile apprendere più di una lezione.
Raffaello

2
Il vecchio sistema di monete britannico (prima della decimalizzazione del 1971) ne aveva un vero esempio. Un algoritmo avido per contare quattro scellini userebbe una mezza corona (2½ scellini), una moneta da uno scellino e un sei penny (½ scellino). Ma la soluzione ottimale utilizza due fiorini (2 scellini ciascuno).
Mark Dominus,

1
Infatti in molti casi gli algoritmi avidi sembrano ragionevoli, ma non funzionano: un altro esempio è la corrispondenza massima bipartita. D'altra parte, ci sono anche esempi in cui sembra che un algoritmo avido non dovrebbe funzionare, ma lo fa: albero di spanning massimo.
jkff,

62

Ho immediatamente ricordato un esempio di R. Backhouse (questo potrebbe essere stato in uno dei suoi libri). Apparentemente, aveva assegnato un incarico di programmazione in cui gli studenti dovevano scrivere un programma Pascal per testare l'uguaglianza di due stringhe. Uno dei programmi offerti da uno studente era il seguente:

issame := (string1.length = string2.length);

if issame then
  for i := 1 to string1.length do
    issame := string1.char[i] = string2.char[i];

write(issame);

Ora possiamo testare il programma con i seguenti input:

"università" "università" Vero; ok

"corso" "corso" Vero; ok

"" "" Vero; ok

"corso" "universitario" Falso; ok

"lezione" "corso" Falso; ok

"precisione" "esattezza" Falso, OK

Tutto ciò sembra molto promettente: forse il programma funziona davvero. Ma un test più attento con dire "puro" e "vero" rivela un output difettoso. In effetti, il programma dice "True" se le stringhe hanno la stessa lunghezza e lo stesso ultimo carattere!

Tuttavia, i test sono stati abbastanza approfonditi: avevamo stringhe con lunghezza diversa, stringhe con lunghezza uguale ma contenuto diverso e persino stringhe uguali. Inoltre, lo studente aveva persino testato ed eseguito ogni ramo. Non si può davvero sostenere che i test siano stati trascurati qui - dato che il programma è davvero molto semplice, potrebbe essere difficile trovare la motivazione e l'energia per testarlo abbastanza a fondo.


Un altro esempio carino è la ricerca binaria. In TAOCP, Knuth afferma che "sebbene l'idea di base della ricerca binaria sia relativamente semplice, i dettagli possono essere sorprendentemente complicati". Apparentemente, un bug nell'implementazione della ricerca binaria di Java è passato inosservato per un decennio. Era un bug di overflow di numero intero e si manifestava solo con input abbastanza grande. I dettagli complicati delle implementazioni della ricerca binaria sono anche trattati da Bentley nel libro Programming Pearls .

In conclusione: può essere sorprendentemente difficile essere certi che un algoritmo di ricerca binaria sia corretto semplicemente testandolo.


9
Certo, il difetto è abbastanza evidente dalla fonte (se hai già scritto una cosa simile prima).
Raffaello

3
Anche se il semplice difetto nel programma di esempio viene corretto, le stringhe danno molti problemi interessanti! L'inversione delle stringhe è un classico: il modo "base" per farlo è semplicemente invertire i byte. Quindi entra in gioco la codifica. Quindi surrogati (di solito due volte). Il problema è, ovviamente, che non esiste un modo semplice per dimostrare formalmente che il metodo è corretto.
Ordous,

6
Forse sto completamente fraintendendo la domanda, ma questo sembra essere un difetto nell'implementazione piuttosto che un difetto nell'algoritmo stesso.
Mr.Mindor,

8
@ Mr.Mindor: come puoi sapere se il programmatore ha scritto un algoritmo corretto e poi lo ha implementato in modo errato, oppure ha scritto un algoritmo errato e poi lo ha implementato fedelmente (esito a dire "correttamente"!)
Steve Jessop

1
@wabbit È discutibile. Ciò che è ovvio per te potrebbe non essere ovvio per uno studente del primo anno.
Juho,

30

Il miglior esempio che abbia mai incontrato è il test di primalità:

input: numero naturale p, p! = 2
output: è pa prime o no?
algoritmo: calcolare 2 ** (p-1) mod p. Se risultato = 1, allora p è primo, altrimenti p non lo è.

Questo funziona per (quasi) tutti i numeri, ad eccezione di pochissimi esempi di contatori, e uno ha effettivamente bisogno di una macchina per trovare un controesempio in un periodo di tempo realistico. Il primo controesempio è 341, e la densità dei controesempi diminuisce effettivamente all'aumentare di p, sebbene quasi logaritmicamente.

Invece di usare solo 2 come base del potere, si può migliorare l'algoritmo usando anche ulteriori primi crescenti come base nel caso in cui il primo precedente sia tornato 1. E tuttavia, ci sono controesempio a questo schema, vale a dire i numeri di Carmichael, piuttosto raro però


Il test di primalità di Fermat è un test probabilistico, quindi la tua post-condizione non è corretta.
Femaref,

5
spesso è un test probabilistico ma la risposta mostra bene (più in generale) come gli algoritmi probabilistici scambiati per quelli esatti possano essere fonte di errore. di più sui numeri di Carmichael
vzn,

2
Questo è un bell'esempio, con una limitazione: per l'uso pratico del test di primalità con cui ho familiarità, ovvero la generazione di chiavi crittografiche asimmetriche, utilizziamo algoritmi probabilistici! I numeri sono troppo grandi per i test esatti (se non lo fossero, non sarebbero adatti alla crittografia perché i tasti potrebbero essere trovati con la forza bruta in tempo realistico).
Gilles,

1
la limitazione a cui si fa riferimento è pratica, non teorica, e i test primi nei sistemi crittografici, ad esempio RSA, sono soggetti a guasti rari / altamente improbabili proprio per questi motivi, sottolineando nuovamente il significato dell'esempio. cioè in pratica a volte questa limitazione è accettata come inevitabile. ci sono algoritmi di tempo P per i test di primalità, ad esempio AKS, ma impiegano troppo tempo per numeri "più piccoli" utilizzati nella pratica.
vzn,

Se esegui il test non solo con 2 p, ma con una p per 50 diversi valori casuali 2 ≤ a <p, allora la maggior parte delle persone saprà che è probabilistica, ma con guasti così improbabili che è più probabile che si verifichi un malfunzionamento nel tuo computer la risposta sbagliata. Con 2 p, 3 p, 5 p e 7 p, i guasti sono già molto rari.
gnasher729,

21

Eccone uno che mi è stato lanciato dai rappresentanti di Google a una convention a cui sono andato. È stato codificato in C, ma funziona in altre lingue che usano riferimenti. Ci scusiamo per aver dovuto scrivere il codice su [cs.se], ma è l'unico a illustrarlo.

swap(int& X, int& Y){
    X := X ^ Y
    Y := X ^ Y
    X := X ^ Y
}

Questo algoritmo funzionerà per qualsiasi valore dato a xey, anche se hanno lo stesso valore. Non funzionerà comunque se viene chiamato swap (x, x). In quella situazione, x finisce come 0. Ora, questo potrebbe non soddisfarti, dal momento che puoi in qualche modo dimostrare che questa operazione è corretta matematicamente, ma dimentica ancora questo caso limite.


1
Quel trucco è stato usato nel subdolo concorso C per produrre un'implementazione RC4 imperfetta . Rileggendo l'articolo, ho appena notato che questo hack è stato probabilmente inviato da @DW
CodesInChaos il

7
Questo difetto è davvero sottile, ma il difetto è specifico del linguaggio, quindi non è proprio un difetto dell'algoritmo; è un difetto nell'implementazione. Si potrebbero trovare altri esempi di stranezze del linguaggio che rendono facile nascondere impercettibili imperfezioni, ma non era proprio quello che stavo cercando (stavo cercando qualcosa a livello di astrazione degli algoritmi). In ogni caso, questo difetto non è una dimostrazione ideale del valore della prova; a meno che tu non stia già pensando all'aliasing, potresti finire per trascurare lo stesso problema quando scrivi la tua "prova" di correttezza.
DW

Ecco perché sono sorpreso che sia stato votato così in alto.
ZeroUltimax,

2
@DW È una questione di come in quale modello si definisce l'algoritmo. Se si scende a un livello in cui i riferimenti di memoria sono espliciti (piuttosto che il modello comune che presuppone l'assenza di condivisione), questo è un difetto dell'algoritmo. Il difetto non è specifico della lingua, si presenta in qualsiasi lingua che supporti la condivisione dei riferimenti di memoria.
Gilles,

16

Esiste un'intera classe di algoritmi che è intrinsecamente difficile da testare: generatori di numeri pseudo-casuali . Non è possibile testare un singolo output ma è necessario esaminare (molte) serie di output con mezzi statistici. A seconda di cosa e come testare, potreste perdere caratteristiche non casuali.

Un caso famoso in cui le cose sono andate terribilmente storto è RANDU . Ha superato il controllo disponibile al momento - che non è riuscito a considerare il comportamento delle tuple delle uscite successive. Le triple già mostrano molta struttura:

Fondamentalmente, i test non coprivano tutti i casi d'uso: mentre l'uso monodimensionale di RANDU andava (probabilmente per lo più) bene, non supportava il suo utilizzo per campionare punti tridimensionali (in questo modo).

Il campionamento pseudo-casuale corretto è un affare complicato. Fortunatamente, ci sono potenti test suite al giorno d'oggi, ad esempio dieharder specializzati nel lanciare tutte le statistiche che conosciamo in un generatore proposto. È abbastanza?

Ad essere sinceri, non ho idea di cosa si possa provare in modo fattibile per i PRNG.


2
bell'esempio, tuttavia in realtà in generale non c'è modo di dimostrare che un PRNG non ha alcun difetto, c'è solo una gerarchia infinita di test più deboli vs più forti. in realtà dimostrare che uno è "casuale" in ogni senso stretto è presumibilmente indecidibile (non lo si è visto però).
vzn,

1
È una buona idea di qualcosa che è difficile da testare, ma anche l'RNG è difficile da provare. I PRNG non sono così inclini ai bug di implementazione che a essere specificati male. Test come irriducibili sono buoni per alcuni usi, ma per criptovaluta puoi passare irriducibile ed essere comunque deriso dalla stanza. Non esiste un CSPRNG "sicuro provato", il meglio che si possa sperare è di dimostrare che se il CSPRNG è rotto, lo è anche AES.
Gilles,

@Gilles Non stavo cercando di entrare in criptovaluta, solo casualità statistica (penso che i due abbiano praticamente requisiti ortogonali). Devo chiarirlo nella risposta?
Raffaello

1
La casualità crittografica implica casualità statistica. Nessuno dei due ha una definizione matematicamente formale, per quanto ne so, a parte l'idea ideale (e contraddittoria con il concetto di un PRNG implementato su una macchina di Turing deterministica) della casualità teorica dell'informazione. La casualità statistica ha una definizione formale oltre "deve essere indipendente dalle distribuzioni con cui lo testeremo"?
Gilles,

1
@vzn: cosa significa essere una sequenza casuale di numeri può essere definito in molti modi possibili, ma uno semplice è "grande complessità di Komolgorov". In tal caso, è facile dimostrare che determinare la casualità è indecidibile.
cody

9

Massimo locale 2D

n×nA

(i,j)A[i,j]

A[i,j+1],A[i,j1],A[i1,j],A[i+1,j]A

0134323125014013

quindi ogni cella in grassetto è un massimo locale. Ogni array non vuoto ha almeno un massimo locale.

O(n2)

UNXXUN(io,j)X(io,j)(io',j')

UNXUNX(io',j')UN'

UN'UN

(io',j')UN'UN'(io',j')

n2×n2UN'(io,j)

T(n)n×nT(n)=T(n/2)+O(n)T(n)=O(n)

Pertanto, abbiamo dimostrato il seguente teorema:

O(n)n×n

O abbiamo?


T(n)=O(nceppon)T(n)=T(n/2)+O(n)

2
Questo è un bellissimo esempio! Lo adoro. Grazie. (Alla fine ho scoperto il difetto di questo algoritmo. Dai timestamp puoi ottenere un limite inferiore su quanto tempo mi ci è voluto. Sono troppo imbarazzato per rivelare il tempo reale. :-)
DW

1
O(n)

8

Questi sono esempi di primalità, perché sono comuni.

(1) Primalità in SymPy. Numero 1789 . È stato effettuato un test errato su un noto sito Web che non ha avuto esito negativo fino a dopo 10 ^ 14. Sebbene la correzione fosse corretta, si trattava solo di correggere i buchi anziché ripensare il problema.

(2) Primalità in Perl 6. Perl6 ha aggiunto is-prime che utilizza una serie di test MR con basi fisse. Esistono controesempi noti, ma sono piuttosto grandi poiché il numero predefinito di test è enorme (sostanzialmente nasconde il vero problema degradando le prestazioni). Questo sarà affrontato presto.

(3) Primalità in FLINT. n_isprime () restituisce true per i compositi , poiché risolto. Fondamentalmente lo stesso problema di SymPy. Usando il database Feitsma / Galway degli pseudoprimi SPRP-2 su 2 ^ 64 ora possiamo testarli.

(4) Perl's Math :: Primality. is_aks_prime rotto . Questa sequenza sembra simile a molte implementazioni di AKS - un sacco di codice che ha funzionato per caso (ad esempio, si è perso nel passaggio 1 e ha finito per fare tutto per divisione di prova) o non ha funzionato per esempi più grandi. Purtroppo AKS è così lento che è difficile testarlo.

(5) is_prime pre-2.2 di Pari. Matematica :: biglietto Pari . Ha usato 10 basi casuali per i test MR (con seed fisso all'avvio, piuttosto che seed fisso di GMP ogni chiamata). Ti dirà che 9 è primo circa 1 su ogni 1 milione di chiamate. Se scegli il numero giusto puoi farlo fallire relativamente spesso, ma i numeri diventano più scarsi, quindi in pratica non appare molto. Da allora hanno modificato l'algoritmo e l'API.

Questo non è sbagliato ma è un classico dei test probabilistici: quanti round dai, diciamo, mpz_probab_prime_p? Se gli diamo 5 round, sembra che funzioni bene: i numeri devono superare un test Fermat di base 210 e quindi 5 test Miller-Rabin di basi preselezionate. Non troverai un controesempio fino al 3892757297131 (con GMP 5.0.1 o 6.0.0a), quindi dovresti fare molti test per trovarlo. Ma ci sono migliaia di controesempi sotto 2 ^ 64. Quindi continui ad aumentare il numero. Quanto lontano? C'è un avversario? Quanto è importante una risposta corretta? Stai confondendo basi casuali con basi fisse? Sai quali dimensioni di input ti verranno fornite?

1016

Questi sono abbastanza difficili da testare correttamente. La mia strategia include test unitari ovvi, oltre a casi limite, oltre ad esempi di guasti visti prima o in altri pacchetti, ove possibile test contro database noti (ad es. Se si esegue un singolo test MR di base-2, quindi è stato ridotto il calcolo non fattibile compito di testare 2 ^ 64 numeri per testare circa 32 milioni di numeri) e, infine, molti test randomizzati usando un altro pacchetto come standard. L'ultimo punto funziona per funzioni come la primalità in cui vi è un input abbastanza semplice e un output noto, ma alcune attività sono come questa. Ho usato questo per trovare difetti sia nel mio codice di sviluppo sia problemi occasionali nei pacchetti di confronto. Ma dato lo spazio di input infinito, non possiamo testare tutto.

Per quanto riguarda la dimostrazione della correttezza, ecco un altro esempio di primalità. I metodi BLS75 e ECPP hanno il concetto di un certificato di primalità. Fondamentalmente dopo che si allontanano facendo ricerche per trovare valori che funzionano per le loro prove, possono produrli in un formato noto. Si può quindi scrivere un verificatore o farlo scrivere a qualcun altro. Questi funzionano molto velocemente rispetto alla creazione, e ora o (1) entrambi i pezzi di codice sono errati (quindi perché preferiresti altri programmatori per i verificatori), o (2) la matematica dietro l'idea della prova è sbagliata. # 2 è sempre possibile, ma questi sono stati in genere pubblicati e recensiti da più persone (e in alcuni casi sono abbastanza facili da attraversare te stesso).

In confronto, metodi come AKS, APR-CL, divisione di prova o test deterministico di Rabin, non producono alcun output diverso da "prime" o "composito". In quest'ultimo caso potremmo avere un fattore che quindi possiamo verificare, ma nel primo caso non ci resta altro che questo bit di output. Il programma ha funzionato correttamente? Boh.

È importante testare il software su più di alcuni esempi di giocattoli, e anche passare attraverso alcuni esempi in ogni fase dell'algoritmo e dire "dato questo input, ha senso che io sia qui con questo stato?"


1
Molti di questi sembrano errori di implementazione (1) (l'algoritmo sottostante è corretto ma non è stato implementato correttamente), che sono interessanti ma non il punto di questa domanda, o (2) una scelta consapevole e consapevole di selezionare qualcosa che è veloce e per lo più funziona ma potrebbe non riuscire con una probabilità molto piccola (per il codice che sta testando con una base casuale o poche basi fisse / casuali, spero che chiunque scelga di farlo sappia che sta facendo un compromesso sulle prestazioni).
DW

Hai ragione sul primo punto: l'algoritmo corretto + bug non è il punto, anche se la discussione e altri esempi li stanno anche combinando. Il campo è maturo con congetture che funzionano per piccoli numeri ma non sono corrette. Per il punto (2) questo è vero per alcuni, ma i miei esempi n. 1 e n. 3 non erano in questo caso: si riteneva che l'algoritmo fosse corretto (queste 5 basi forniscono risultati comprovati per numeri inferiori a 10 ^ 16), quindi in seguito scoperto che non lo era.
DanaJ,

Non è questo un problema fondamentale con i test di pseudo-primalità?
asmeurer,

asmeurer, sì nel mio n. 2 e nella successiva discussione su di loro. Ma n. 1 e n. 3 erano entrambi casi di utilizzo di Miller-Rabin con basi note per fornire risultati corretti deterministici al di sotto di una soglia. Quindi, in questo caso, "l'algoritmo" (usando il termine liberamente per abbinare l'OP) era errato. # 4 non è un probabile test primo, ma come ha sottolineato DW, l'algoritmo funziona bene, è solo l'implementazione che è difficile. L'ho incluso perché porta a una situazione simile: sono necessari dei test e fino a che punto si va oltre i semplici esempi prima di dire che funziona?
DanaJ,

Alcuni dei tuoi post sembrano adattarsi alla domanda, mentre altri no (vedi il commento di @ DW). Rimuovi gli esempi (e altri contenuti) che non rispondono alla domanda.
Raffaello

7

L'algoritmo shuffling di Fisher-Yates-Knuth è un esempio (pratico) e uno su cui uno degli autori di questo sito ha commentato .

L'algoritmo genera una permutazione casuale di un determinato array come:

 // To shuffle an array a of n elements (indices 0..n-1):
  for i from n − 1 downto 1 do
       j ← random integer with 0 ≤ j ≤ i
       exchange a[j] and a[i]

ioj0jio

Un algoritmo "ingenuo" potrebbe essere:

 // To shuffle an array a of n elements (indices 0..n-1):
  for i from n − 1 downto 1 do
       j ← random integer with 0 ≤ j ≤ n-1
       exchange a[j] and a[i]

Dove nel ciclo l'elemento da scambiare viene scelto tra tutti gli elementi disponibili. Tuttavia, ciò produce un campionamento parziale delle permutazioni (alcune sono sovrarappresentate, ecc.)

In realtà si può inventare la mescolanza di pesca-yates-knuth usando un'analisi di conteggio semplice (o ingenua) .

nn!=n×n-1×n-2..nn-1

Il problema principale con la verifica se l'algoritmo di shuffle è corretto o meno ( distorto o meno ) è che a causa delle statistiche è necessario un gran numero di campioni. L' articolo di codinghorror che linko sopra lo spiega esattamente (e con test reali).


1
Vedi qui per un esempio di prova di correttezza per un algoritmo shuffle.
Raffaello

5

L'esempio migliore (leggi: cosa di cui mi fa più male il culo) che abbia mai visto riguarda le congetture di collatz. Ero in una competizione di programmazione (con un premio di 500 dollari in prima fila) in cui uno dei problemi era trovare il numero minimo di passaggi necessari affinché due numeri raggiungessero lo stesso numero. La soluzione ovviamente è di alternare ciascuno fino a quando entrambi raggiungono qualcosa che è stato visto prima. Ci è stato dato un intervallo di numeri (penso che fosse tra 1 e 1000000) e ci è stato detto che la congettura di collatz era stata verificata fino a 2 ^ 64, quindi tutti i numeri che ci erano stati dati alla fine sarebbero convertibili a 1. Ho usato 32-bit numeri interi con cui eseguire i passaggi. Si scopre che esiste un numero oscuro compreso tra 1 e 1000000 (170 mila qualcosa) che causerà il trabocco di un numero intero a 32 bit a tempo debito. In realtà questi numeri sono estremamente rari sotto 2 ^ 31. Abbiamo testato il nostro sistema per numeri ENORMI di gran lunga superiori a 1000000 per "garantire" che non si verificasse un overflow. Risulta un numero molto più piccolo che non abbiamo appena testato ha causato trabocco. Poiché ho usato "int" anziché "long", ho ricevuto solo un premio di 300 dollari anziché un premio di $ 500.


5

Il problema di Knacksack 0/1 è uno che quasi tutti gli studenti ritengono risolvibile con un algoritmo avido. Ciò accade più spesso se in precedenza hai mostrato alcune soluzioni avide come la versione problematica dello zaino in cui funziona un algoritmo avido .

Per questi problemi, in classe , dovrei mostrare la prova per Knapsack 0/1 ( programmazione dinamica ) per rimuovere ogni dubbio e anche per la versione avida del problema. In realtà, entrambe le prove non sono banali e probabilmente gli studenti le trovano molto utili. Inoltre, c'è un commento al riguardo in CLRS 3ed , capitolo 16, pagina 425-427 .

Problema: il ladro ruba un negozio e può trasportare un peso massimo di W nello zaino. Ci sono n articoli e ogni articolo pesa wi e vale vi dollari. Quali oggetti dovrebbe prendere il ladro? massimizzare il suo guadagno ?

Zaino problema 0/1 : la configurazione è la stessa, ma gli oggetti non possono essere suddivisi in pezzi più piccoli , quindi il ladro può decidere di prendere un oggetto o di lasciarlo (scelta binaria), ma potrebbe non prendere una frazione di un oggetto .

E puoi ottenere dagli studenti alcune idee o algoritmi che seguono la stessa idea del problema della versione golosa, ovvero:

  • Prendi la capacità totale della borsa e metti il ​​più possibile l'oggetto di maggior valore e itera questo metodo fino a quando non puoi mettere più oggetti perché la borsa è piena o non c'è un oggetto con meno o uguale peso da mettere all'interno della borsa.
  • Un altro modo sbagliato è pensare: metti gli articoli più leggeri e mettili seguendo il prezzo più alto o più basso.
  • ...

È utile per te? in realtà, sappiamo che il problema della moneta è una versione del problema dello zaino. Ma ci sono altri esempi nella foresta dei problemi degli zaini, ad esempio cosa ne pensi di Zaino 2D (è molto utile quando vuoi tagliare il legno per fare mobili , ho visto in un locale della mia città), è molto comune pensare che il anche qui gli avidi lavorano, ma no.


Greedy era già stato trattato nella risposta accettata , ma il problema di Zaino, in particolare, è adatto per impostare alcune trappole.
Raffaello

3

Un errore comune è quello di implementare errati algoritmi di shuffle. Vedi la discussione su Wikipedia .

n!nn(n-1)n


1
È un buon bug, ma non una buona illustrazione dell'inganno euristico dei casi di test, poiché i test non si applicano realmente a un algoritmo di shuffle (è randomizzato, quindi come lo testeresti? Cosa significherebbe fallire un caso di test, e come lo rileveresti guardando l'output?)
DW

Lo provi statisticamente ovviamente. La casualità uniforme è tutt'altro che "tutto può succedere nell'output". Non saresti sospettoso se un programma che dice di emulare un dado ti ha dato 100 3 di fila?
Per Alexandersson,

Ancora una volta, sto parlando dell'euristico studente di "provare alcuni casi di test a mano". Ho visto molti studenti pensare che questo sia un modo ragionevole per verificare se un algoritmo deterministico è corretto, ma sospetto che non presumano che sia un buon modo per testare se un algoritmo di mescolamento è corretto (poiché un algoritmo di mescolamento è randomizzato, c'è non è possibile stabilire se un determinato output sia corretto; in ogni caso, non è possibile eseguire manualmente a mano un numero sufficiente di esempi per eseguire un utile test statistico). Quindi non mi aspetto che gli algoritmi di mescolamento aiuteranno molto a chiarire il malinteso comune.
DW

1
@PerAlexandersson: Anche se generi solo un shuffle non può essere veramente casuale usando MT con n> 2080. Ora la deviazione dal previsto sarà molto piccola, quindi probabilmente non ti importerà ... ma questo vale anche se generi molto meno del periodo (come sottolinea asmeurer sopra).
Charles,

2
Questa risposta sembra essere stata resa obsoleta da quella più elaborata di Nikos M. ?
Raffaello

2

Pythons PEP450 che ha introdotto funzioni statistiche nella libreria standard potrebbe essere interessante. Come parte della giustificazione per avere una funzione che calcola la varianza nella libreria standard di Python, l'autore Steven D'Aprano scrive:

def variance(data):
        # Use the Computational Formula for Variance.
        n = len(data)
        ss = sum(x**2 for x in data) - (sum(data)**2)/n
        return ss/(n-1)

Quanto sopra sembra essere corretto con un test casuale:

>>> data = [1, 2, 4, 5, 8]
>>> variance(data)
  7.5

Ma l'aggiunta di una costante per ogni punto dati non dovrebbe modificare la varianza:

>>> data = [x+1e12 for x in data]
>>> variance(data)
  0.0

E la varianza non dovrebbe mai essere negativa:

>>> variance(data*100)
  -1239429440.1282566

Il problema riguarda i numeri e come si perde la precisione. Se si desidera la massima precisione, è necessario ordinare le operazioni in un determinato modo. Un'implementazione ingenua porta a risultati errati perché l'imprecisione è troppo grande. Questo era uno dei problemi su cui il mio corso di numerazione all'università riguardava.


1
n-1

2
@Raphael: Sebbene sia giusto, l'algoritmo scelto è ben noto per essere una cattiva scelta per i dati in virgola mobile.

2
Non si tratta semplicemente dell'implementazione dell'operazione sulla numerica e su come si perde la precisione. Se si desidera la massima precisione, è necessario ordinare le operazioni in un determinato modo. Questo era uno dei problemi su cui il mio corso di numerazione all'università riguardava.
Christian,

Oltre al preciso commento di Raffaello, un difetto di questo esempio è che non credo che una prova di correttezza possa aiutare a evitare questo difetto. Se non sei a conoscenza delle sottigliezze dell'aritmetica in virgola mobile, potresti pensare di averlo dimostrato correttamente (dimostrando che la formula è valida). Quindi non è un esempio ideale per insegnare agli studenti perché è importante dimostrare che i loro algoritmi sono corretti. Se gli studenti vedessero questo esempio, il mio sospetto è che trarrebbero invece la lezione "Il calcolo in virgola mobile / calcolo numerico è complicato".
DW

1

Anche se questo probabilmente non è proprio quello che stai cercando, è certamente facile da capire e testare alcuni piccoli casi senza alcun altro pensiero porterà a un algoritmo errato.

nn2+n+410<dd divide n2+n+41d<n2+n+41

Soluzione proposta :

int f(int n) {
   return 1;
}

n=0,1,2,...,39n=40

Questo approccio "prova alcuni piccoli casi e deduce un algoritmo dal risultato" affiora frequentemente (anche se non come in questo caso) nelle competizioni di programmazione in cui la pressione è quella di creare un algoritmo che (a) è veloce da implementare e (b ) ha un tempo di esecuzione rapido.


5
Non penso che questo sia un ottimo esempio, perché poche persone tenterebbero di trovare i divisori di un polinomio restituendo 1.
Brian S

1
nn3-n

Ciò potrebbe essere rilevante, nel senso che restituire un valore costante per i divisori (o un'altra caclulazione), può essere il risultato di un approccio algoritmico errato a un problema (ad esempio un problema statistico o di non gestire casi limite dell'algoritmo). Tuttavia, la risposta deve essere riformulata
Nikos M.,

@NikosM. Eh. Mi sento come se stessi battendo un cavallo morto qui, ma il secondo paragrafo della domanda dice che "se il loro algoritmo funziona correttamente su una manciata di esempi, compresi tutti i casi angolari che possono pensare di provare, allora concludono che l'algoritmo deve avere ragione. C'è sempre uno studente che chiede: "Perché devo dimostrare il mio algoritmo corretto, se posso provarlo solo su alcuni casi di test?" In questo caso, per i primi 40 valori (molto più di uno studente è probabilmente provare), restituire 1 è corretto. Mi sembra sia quello che stava cercando l'OP.
Rick Decker,

Ok, sì, ma questo come espresso è banale (forse tipicamente corretto), ma non nello spirito della domanda. Avrebbe ancora bisogno di essere riformulato
Nikos M.,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.