Um zu testen, ob ein Algorithmus für ein Problem korrekt ist, versuchen Sie in der Regel, den Algorithmus für eine Reihe einfacher Testfälle von Hand auszuführen. Versuchen Sie es an einigen Beispiel-Problemfällen, einschließlich einiger einfacher Eckfälle ". Dies ist eine großartige Heuristik: Es ist eine großartige Möglichkeit, viele fehlerhafte Versuche an einem Algorithmus schnell auszumerzen und zu verstehen, warum der Algorithmus nicht funktioniert.
Beim Erlernen von Algorithmen sind einige Schüler jedoch versucht, damit aufzuhören: Wenn ihr Algorithmus an einer Handvoll Beispiele korrekt funktioniert, einschließlich aller Eckfälle, die sie ausprobieren können, kommen sie zu dem Schluss, dass der Algorithmus korrekt sein muss. Es gibt immer einen Studenten, der fragt: "Warum muss ich meinen Algorithmus nachweisen, wenn ich ihn nur an ein paar Testfällen ausprobieren kann?"
Also, wie können Sie die Heuristik "Testfälle ausprobieren" zum Narren halten? Ich suche einige gute Beispiele, um zu zeigen, dass diese Heuristik nicht ausreicht. Mit anderen Worten, ich suche nach einem oder mehreren Beispielen für einen Algorithmus, der auf den ersten Blick korrekt aussieht und die richtige Antwort auf alle kleinen Eingaben ausgibt, die wahrscheinlich von irgendjemandem kommen, aber wo der Algorithmus tatsächlich ist funktioniert nicht Möglicherweise funktioniert der Algorithmus nur bei allen kleinen Eingaben ordnungsgemäß und schlägt nur bei großen Eingaben oder nur bei Eingaben mit einem ungewöhnlichen Muster fehl.
Konkret suche ich:
Ein Algorithmus. Der Fehler muss auf der algorithmischen Ebene liegen. Ich suche keine Implementierungsfehler. (Zum Beispiel sollte das Beispiel zumindest sprachunabhängig sein, und der Fehler sollte sich eher auf algorithmische Belange als auf Software-Engineering- oder Implementierungsprobleme beziehen.)
Ein Algorithmus, den sich jemand einfallen lassen könnte. Der Pseudocode sollte zumindest plausibel korrekt aussehen (z. B. ist Code, der verschleiert oder offensichtlich zweifelhaft ist, kein gutes Beispiel). Bonuspunkte, wenn es sich um einen Algorithmus handelt, den ein Schüler bei der Lösung einer Hausaufgabe oder eines Prüfungsproblems entwickelt hat.
Ein Algorithmus, der mit hoher Wahrscheinlichkeit eine vernünftige manuelle Teststrategie besteht. Es ist unwahrscheinlich, dass jemand, der ein paar kleine Testfälle von Hand ausprobiert, den Fehler entdeckt. Zum Beispiel sollte es unwahrscheinlich sein, dass "QuickCheck von Hand in einem Dutzend kleiner Testfälle simuliert", dass der Algorithmus falsch ist.
Vorzugsweise ein deterministischer Algorithmus. Ich habe viele Studenten gesehen, die dachten, dass "einige Testfälle von Hand versuchen" eine vernünftige Methode ist, um zu überprüfen, ob ein deterministischer Algorithmus korrekt ist, aber ich vermute, die meisten Studenten würden nicht davon ausgehen, dass das Probieren einiger Testfälle eine gute Methode ist, um die Wahrscheinlichkeit zu überprüfen Algorithmen. Bei probabilistischen Algorithmen kann häufig nicht festgestellt werden, ob eine bestimmte Ausgabe korrekt ist. Und Sie können nicht genügend Beispiele manuell ankurbeln, um einen nützlichen statistischen Test der Ausgabeverteilung durchzuführen. Daher würde ich mich lieber auf deterministische Algorithmen konzentrieren, da sie die falschen Vorstellungen der Schüler klarer herausarbeiten.
Ich möchte lehren, wie wichtig es ist, Ihren Algorithmus als korrekt zu beweisen, und ich hoffe, dass ich einige Beispiele wie dieses verwenden kann, um Beweise für die Richtigkeit zu motivieren. Ich würde Beispiele bevorzugen, die relativ einfach und für Studierende zugänglich sind. Beispiele, die schwere Maschinen oder eine Tonne mathematischen / algorithmischen Hintergrunds erfordern, sind weniger nützlich. Außerdem möchte ich keine "unnatürlichen" Algorithmen. Es mag zwar einfach sein, einen seltsamen künstlichen Algorithmus zu konstruieren, um die Heuristik zu täuschen, aber wenn er höchst unnatürlich aussieht oder eine offensichtliche Hintertür hat, die nur dazu dient, diese Heuristik zu täuschen, wird er die Schüler wahrscheinlich nicht überzeugen. Irgendwelche guten Beispiele?
Antworten:
Ein häufiger Fehler ist meines Erachtens die Verwendung gieriger Algorithmen, was nicht immer der richtige Ansatz ist, aber in den meisten Testfällen funktionieren könnte.
Beispiel: , und eine Zahl , drücken als Summe von : s mit so wenig Münzen wie möglich aus. n n d id1, … , Dk n n dich
Ein naiver Ansatz ist es, zuerst die größtmögliche Münze zu verwenden und gierig eine solche Summe zu produzieren.
Zum Beispiel geben die Münzen mit den Werten , und für alle Zahlen zwischen und korrekte Antworten mit Gier, mit Ausnahme der Zahl .5 1 1 14 10 = 6 + 1 + 1 + 1 + 1 = 5 + 56 5 1 1 14 10 = 6 + 1 + 1 + 1 + 1 = 5 + 5
quelle
Ich erinnerte mich sofort an ein Beispiel von R. Backhouse (dies könnte in einem seiner Bücher gewesen sein). Anscheinend hatte er einen Programmierauftrag vergeben, bei dem die Schüler ein Pascal-Programm schreiben mussten, um die Gleichheit von zwei Zeichenfolgen zu testen. Eines der von einem Studenten eingereichten Programme war das folgende:
Wir können das Programm nun mit folgenden Eingaben testen:
"Universität" "Universität" True; okay⇒
"course" "course" True; okay⇒
"" " True; okay⇒
"Universität" "Kurs" False; okay⇒
"Vorlesung" "Kurs" False; okay⇒
"Genauigkeit" "Genauigkeit" False, OK⇒
All dies scheint sehr vielversprechend: Vielleicht funktioniert das Programm tatsächlich. Aber ein sorgfältigeres Testen mit "rein" und "wahr" zeigt fehlerhafte Ausgabe. Tatsächlich sagt das Programm "True", wenn die Zeichenfolgen dieselbe Länge und dasselbe letzte Zeichen haben!
Die Tests waren jedoch ziemlich gründlich: Wir hatten Zeichenfolgen mit unterschiedlicher Länge, Zeichenfolgen mit gleicher Länge, aber unterschiedlichem Inhalt und sogar gleichen Zeichenfolgen. Darüber hinaus hatte der Student sogar jede Branche getestet und ausgeführt. Man kann nicht wirklich behaupten, dass das Testen hier nachlässig war - da das Programm in der Tat sehr einfach ist, kann es schwierig sein, die Motivation und Energie zu finden, es gründlich genug zu testen.
Ein weiteres nettes Beispiel ist die binäre Suche. In TAOCP sagt Knuth, dass "obwohl die Grundidee der binären Suche vergleichsweise einfach ist, die Details überraschend schwierig sein können". Anscheinend ist ein Fehler in der Implementierung der binären Suche von Java ein Jahrzehnt lang unbemerkt geblieben. Es war ein Integer-Überlauffehler, der sich nur bei ausreichend großer Eingabe manifestierte. Knifflige Details zu Implementierungen der binären Suche werden auch von Bentley in dem Buch Programming Pearls behandelt .
Fazit: Es kann überraschend schwierig sein, sich zu vergewissern, dass ein binärer Suchalgorithmus korrekt ist, wenn man ihn nur testet.
quelle
Das beste Beispiel, das mir jemals begegnet ist, ist das Testen der Ursprünglichkeit:
Dies funktioniert für (fast) jede Zahl, außer für einige wenige Zählerbeispiele, und man benötigt tatsächlich eine Maschine, um in einem realistischen Zeitraum ein Gegenbeispiel zu finden. Das erste Gegenbeispiel ist 341, und die Dichte der Gegenbeispiele nimmt tatsächlich mit zunehmendem p ab, wenn auch fast logarithmisch.
Anstatt nur 2 als Grundlage für die Potenz zu verwenden, kann man den Algorithmus verbessern, indem man auch zusätzliche, zunehmende kleine Primzahlen als Grundlage verwendet, falls die vorherige Primzahl 1 zurückgibt. Dennoch gibt es ein Gegenbeispiel zu diesem Schema, nämlich die Carmichael-Zahlen ziemlich selten
quelle
Hier ist eine, die mir von Google-Vertretern auf einer Tagung, an der ich teilgenommen habe, gezeigt wurde. Es wurde in C codiert, funktioniert aber auch in anderen Sprachen, die Verweise verwenden. Es tut mir leid, dass Sie auf [cs.se] codieren müssen, aber es ist das einzige Beispiel dafür.
Dieser Algorithmus funktioniert für alle Werte für x und y, auch wenn sie denselben Wert haben. Es wird jedoch nicht funktionieren, wenn es als Swap (x, x) aufgerufen wird. In dieser Situation hat x den Wert 0. Nun, dies könnte Sie nicht zufriedenstellen, da Sie diese Operation mathematisch als korrekt beweisen können, diesen Kantenfall jedoch immer noch vergessen.
quelle
Es gibt eine ganze Klasse von Algorithmen, die von Natur aus schwer zu testen sind: Pseudozufallszahlengeneratoren . Sie können nicht einen einzelnen Ausgang testen, sondern müssen (viele) Reihen von Ausgängen mit statistischen Mitteln untersuchen. Abhängig davon, was und wie Sie testen, können Sie möglicherweise nicht zufällige Merkmale übersehen.
Ein berühmter Fall, in dem die Dinge schrecklich schief gelaufen sind, ist RANDU . Es bestand die zu diesem Zeitpunkt verfügbare Prüfung, bei der das Verhalten von Tupeln nachfolgender Ausgaben nicht berücksichtigt wurde . Bereits Dreifache zeigen viel Struktur:
Grundsätzlich deckten die Tests nicht alle Anwendungsfälle ab: Während die eindimensionale Verwendung von RANDU (wahrscheinlich größtenteils) in Ordnung war, konnte sie (auf diese Weise) nicht zum Abtasten dreidimensionaler Punkte verwendet werden.
Richtige Pseudo-Zufallsauswahl ist eine knifflige Angelegenheit. Zum Glück gibt es an manchen Tagen leistungsfähige Testsuiten , z. B. dieharder , die darauf spezialisiert sind, alle uns bekannten Statistiken auf einen vorgeschlagenen Generator zu werfen. Reicht das?
Um fair zu sein, ich habe keine Ahnung, was Sie für PRNGs durchführbar beweisen können.
quelle
2D lokales Maximum
dann ist jede fettgedruckte Zelle ein lokales Maximum. Jedes nicht leere Array hat mindestens ein lokales Maximum.
Wir haben also folgenden Satz bewiesen:
Oder haben wir?
quelle
Dies sind Beispiele für die Ursprünglichkeit, da sie häufig vorkommen.
(1) Primalität in SymPy. Ausgabe 1789 . Auf einer bekannten Website wurde ein falscher Test durchgeführt, der erst nach 10 ^ 14 fehlschlug. Während die Korrektur korrekt war, wurden lediglich Löcher ausgebessert, anstatt das Problem zu überdenken.
(2) Primalität in Perl 6. Perl6 hat is-prime hinzugefügt, das eine Reihe von MR-Tests mit festen Basen verwendet. Es gibt bekannte Gegenbeispiele, aber sie sind ziemlich umfangreich, da die Standardanzahl der Tests sehr hoch ist (im Grunde genommen wird das eigentliche Problem durch Leistungseinbußen ausgeblendet). Dies wird in Kürze behoben.
(3) Primalität in FLINT. n_isprime () gibt true für Composites zurück , da behoben. Grundsätzlich das gleiche Problem wie bei SymPy. Mit der Feitsma / Galway-Datenbank von SPRP-2-Pseudoprimes auf 2 ^ 64 können wir diese nun testen.
(4) Perls Mathematik :: Primalität. is_aks_prime ist kaputt . Diese Sequenz scheint vielen AKS-Implementierungen ähnlich zu sein - viel Code, der entweder versehentlich funktioniert hat (z. B. in Schritt 1 verloren gegangen ist und das Ganze durch die Testabteilung erledigt hat) oder bei größeren Beispielen nicht funktioniert hat. Leider ist AKS so langsam, dass es schwer zu testen ist.
(5) Paris Version vor 2.2 ist_prime. Math :: Pari Ticket . Es wurden 10 zufällige Basen für MR-Tests verwendet (mit festem Startwert anstelle von GMPs festem Startwert bei jedem Aufruf). Es zeigt an, dass 9 ungefähr 1 von 1 Millionen Anrufen ist. Wenn Sie die richtige Zahl auswählen, kann dies relativ häufig zum Scheitern führen, die Zahlen werden jedoch spärlicher, sodass in der Praxis nicht viel davon zu sehen ist. Sie haben seitdem den Algorithmus und die API geändert.
Das ist nicht falsch, aber es ist ein Klassiker der Wahrscheinlichkeitstests: Wie viele Runden geben Sie beispielsweise mpz_probab_prime_p? Wenn wir es 5 Runden geben, sieht es sicher so aus, als ob es gut funktioniert - Zahlen müssen einen Fermat-Test zur Basis 210 und dann 5 vorgewählte Miller-Rabin-Tests zur Basis bestehen. Sie werden kein Gegenbeispiel finden, bis Sie 3892757297131 (mit GMP 5.0.1 oder 6.0.0a) gefunden haben. Sie müssten also viele Tests durchführen, um es zu finden. Aber es gibt Tausende von Gegenbeispielen unter 2 ^ 64. Also erhöhen Sie die Zahl weiter. Wie weit? Gibt es einen Gegner? Wie wichtig ist eine richtige Antwort? Verwechseln Sie zufällige Basen mit festen Basen? Wissen Sie, welche Eingabegrößen Sie erhalten?
Diese sind recht schwer richtig zu testen. Meine Strategie umfasst offensichtliche Komponententests sowie Randfälle und Beispiele für Fehler, die vor oder in anderen Paketen aufgetreten sind, Tests im Vergleich zu bekannten Datenbanken, sofern dies möglich ist (z. B. wenn Sie einen einzelnen Base-2-MR-Test durchführen, haben Sie das rechnerisch Unmögliche reduziert Aufgabe des Testens von 2 ^ 64 Zahlen bis zum Testen von etwa 32 Millionen Zahlen) und schließlich viele randomisierte Tests unter Verwendung eines anderen Pakets als Standard. Der letzte Punkt funktioniert für Funktionen wie Primalität, bei denen es eine ziemlich einfache Eingabe und eine bekannte Ausgabe gibt, aber so einige Aufgaben sind. Ich habe dies verwendet, um sowohl Fehler in meinem eigenen Entwicklungscode als auch gelegentliche Probleme in den Vergleichspaketen zu finden. Aber angesichts des unendlichen Eingaberaums können wir nicht alles testen.
Hier ist ein weiteres Beispiel für den Beweis der Korrektheit. Die BLS75-Methoden und ECPP haben das Konzept eines Primalitätszertifikats. Grundsätzlich können sie, nachdem sie die Suche nach Werten, die für ihre Proofs geeignet sind, abgebrochen haben, diese in einem bekannten Format ausgeben. Man kann dann einen Prüfer schreiben oder ihn von jemand anderem schreiben lassen. Diese laufen im Vergleich zur Erstellung sehr schnell, und jetzt sind entweder (1) beide Codeteile falsch (daher würden Sie andere Programmierer für die Verifizierer bevorzugen) oder (2) die Mathematik hinter der Beweisidee ist falsch. # 2 ist immer möglich, aber diese wurden in der Regel von mehreren Personen veröffentlicht und überprüft (und in einigen Fällen sind sie für Sie einfach genug, um durch sich selbst zu gehen).
Im Vergleich dazu liefern Methoden wie AKS, APR-CL, Trial Division oder der deterministische Rabin-Test keine anderen Ergebnisse als "Prime" oder "Composite". Im letzteren Fall haben wir vielleicht einen Faktor, den wir verifizieren können, aber im ersten Fall haben wir nichts anderes als dieses eine Ausgabebit. Hat das Programm richtig funktioniert? Keine Ahnung.
Es ist wichtig, die Software an mehr als nur ein paar Spielzeugbeispielen zu testen und bei jedem Schritt des Algorithmus einige Beispiele durchzugehen und zu sagen: "Ist es angesichts dieser Eingabe sinnvoll, dass ich in diesem Zustand hier bin?"
quelle
Der Fisher-Yates-Knuth-Shuffling-Algorithmus ist ein (praktisches) Beispiel, zu dem einer der Autoren dieser Website Stellung genommen hat .
Der Algorithmus erzeugt eine zufällige Permutation eines gegebenen Arrays als:
Ein "naiver" Algorithmus könnte sein:
Wo in der Schleife wird das auszutauschende Element aus allen verfügbaren Elementen ausgewählt. Dies führt jedoch zu einer verzerrten Abtastung der Permutationen (einige sind überrepräsentiert usw.).
Tatsächlich kann man mit einer einfachen (oder naiven) Zählanalyse das Fisher-Yates-Knuth-Mischen herausfinden .
Das Hauptproblem bei der Überprüfung, ob der Shuffling-Algorithmus korrekt ist oder nicht ( verzerrt oder nicht ), besteht darin, dass aufgrund der Statistik eine große Anzahl von Samples benötigt wird. Der obige Coding-Horror-Artikel erklärt genau das (und mit aktuellen Tests).
quelle
Das beste Beispiel, das ich je gesehen habe, hat mit Collatz-Vermutungen zu tun. Ich war in einem Programmierwettbewerb (mit einem Preisgeld von 500 Dollar auf dem ersten Platz), bei dem eines der Probleme darin bestand, die Mindestanzahl von Schritten zu finden, die erforderlich sind, damit zwei Zahlen dieselbe Zahl erreichen. Die Lösung besteht natürlich darin, abwechselnd in Schritten vorzugehen, bis beide zu etwas gelangen, das zuvor gesehen wurde. Wir erhielten eine Reihe von Zahlen (ich glaube, sie lagen zwischen 1 und 1000000) und sagten, dass die Kollatz-Vermutung auf 2 ^ 64 überprüft worden war, so dass alle Zahlen, die wir erhielten, irgendwann bei 1 konvergieren würden. Ich habe 32-Bit verwendet Ganzzahlen, mit denen Sie die Schritte ausführen können. Es stellt sich heraus, dass es eine undurchsichtige Zahl zwischen 1 und 1000000 (170.000) gibt, die dazu führt, dass eine 32-Bit-Ganzzahl zu gegebener Zeit überläuft. Tatsächlich sind diese Zahlen unter 2 ^ 31 äußerst selten. Wir haben unser System auf RIESIGE Zahlen getestet, die weit über 1000000 liegen, um sicherzustellen, dass kein Überlauf auftritt. Es stellt sich heraus, dass eine viel kleinere Zahl, die wir gerade nicht getestet haben, einen Überlauf verursacht hat. Da ich "int" anstelle von "long" verwendet habe, habe ich nur einen 300-Dollar-Preis statt eines 500-Dollar-Preises erhalten.
quelle
Das Knapsack-0/1- Problem ist eines, von dem fast alle Studenten glauben, dass es mit einem gierigen Algorithmus lösbar ist. Das kommt häufiger vor, wenn Sie zuvor einige gierige Lösungen als Problemversion des Knapsacks anzeigen, in der ein gieriger Algorithmus funktioniert .
Für diese Probleme sollte ich in der Klasse den Proof für Knapsack 0/1 ( dynamische Programmierung ) zeigen, um alle Zweifel zu beseitigen, und auch für die gierige Problemversion. Tatsächlich sind beide Beweise nicht trivial und die Studenten finden sie wahrscheinlich sehr hilfreich. Darüber hinaus enthält CLRS 3ed , Kapitel 16, Seite 425-427 einen Kommentar .
Problem: Dieb rauben ein Geschäft aus und können ein maximales Gewicht von W in ihren Rucksack tragen. Es gibt n Artikel und i-te Artikel wiegen wi und ist vi Dollar wert. Welche Gegenstände sollte der Dieb mitnehmen? seinen Gewinn zu maximieren ?
Rucksack-0/1-Problem : Der Aufbau ist derselbe, aber die Gegenstände dürfen nicht in kleinere Teile zerbrochen werden , sodass der Dieb entweder beschließt , einen Gegenstand zu nehmen oder ihn zu verlassen (binäre Wahl), aber möglicherweise keinen Bruchteil eines Gegenstands nimmt .
Und Sie können von den Schülern einige Ideen oder Algorithmen erhalten, die der gleichen Idee wie das Problem der gierigen Version folgen.
Ist es hilfreich für dich? Tatsächlich wissen wir, dass es sich bei dem Münzenproblem um eine Version mit Rucksackproblem handelt. Aber es gibt weitere Beispiele im Wald von Ranzen Problemen, mit gutem Beispiel, was Knapsack 2D (das ist wirklich nützlich , wenn Sie Holz für Make Möbel schneiden wollen , sah ich in einem lokalen aus meiner Stadt), ist es sehr häufig denkt , dass die gierig funktioniert auch hier, aber nicht.
quelle
Ein häufiger Fehler ist die Implementierung falscher Mischalgorithmen. Siehe Diskussion auf Wikipedia .
quelle
Pythons PEP450 , mit denen Statistikfunktionen in die Standardbibliothek eingeführt wurden, könnten von Interesse sein. Zur Rechtfertigung einer Funktion, die die Varianz in der Standardbibliothek von Python berechnet, schreibt der Autor Steven D'Aprano:
Es geht um Zahlen und darum, wie Präzision verloren geht. Wenn Sie maximale Präzision wünschen, müssen Sie Ihre Operationen auf eine bestimmte Weise anordnen. Eine naive Implementierung führt zu falschen Ergebnissen, da die Ungenauigkeit zu groß ist. Das war eines der Themen, um die es in meinem Numerikkurs an der Universität ging.
quelle
Dies ist wahrscheinlich nicht ganz das, wonach Sie suchen, aber es ist mit Sicherheit leicht zu verstehen, und das Testen einiger kleiner Fälle, ohne dass andere Überlegungen angestellt werden, führt zu einem falschen Algorithmus.
Vorgeschlagene Lösung :
Dieser Ansatz "Versuche einige kleine Fälle und leite einen Algorithmus aus dem Ergebnis ab" taucht häufig (wenn auch nicht so extrem wie hier) bei Programmierwettbewerben auf, bei denen der Druck besteht, einen Algorithmus zu finden, der (a) schnell zu implementieren ist und (b) ) hat eine schnelle Laufzeit.
quelle