Pour essayer de vérifier si un algorithme est correct pour un problème donné, le point de départ habituel est d'essayer de l'exécuter à la main sur un certain nombre de cas de test simples. Essayez-le sur quelques exemples de problèmes, y compris quelques "cas simples". ". C'est une excellente heuristique: c'est un excellent moyen d'éliminer rapidement de nombreuses tentatives incorrectes d'algorithme et de comprendre pourquoi cet algorithme ne fonctionne pas.
Cependant, lors de l’apprentissage des algorithmes, certains étudiants sont tentés de s’arrêter là: si leur algorithme fonctionne correctement avec quelques exemples, y compris tous les cas difficiles qu’ils peuvent penser, ils concluent que l’algorithme doit être correct. Il y a toujours un étudiant qui demande: "Pourquoi dois-je prouver que mon algorithme est correct, si je peux simplement l'essayer sur quelques cas de test?"
Alors, comment vous tromper l'heuristique «essayez un tas de cas de test»? Je cherche de bons exemples pour montrer que cette heuristique ne suffit pas. En d’autres termes, je cherche un ou plusieurs exemples d’algorithmes qui, superficiellement, semblent pouvoir être corrects et qui fournissent la bonne réponse pour toutes les petites entrées susceptibles d’être fournies par quiconque, mais dont ne fonctionne pas. Peut-être que l'algorithme fonctionne correctement sur toutes les petites entrées et échoue uniquement pour les grandes entrées, ou uniquement pour les entrées avec un motif inhabituel.
Plus précisément, je recherche:
Un algorithme. La faille doit être au niveau algorithmique. Je ne cherche pas de bogues de mise en œuvre. (Par exemple, au minimum, l'exemple devrait être indépendant de la langue et la faille devrait concerner des problèmes algorithmiques plutôt que des problèmes d'ingénierie logicielle ou de mise en œuvre.)
Un algorithme que quelqu'un pourrait plausiblement proposer. Le pseudo-code doit avoir au moins une apparence plausible (par exemple, un code obscurci ou douteux n’est pas un bon exemple). Des points bonus s’il s’agit d’un algorithme créé par un étudiant lorsqu’il tente de résoudre un problème de devoirs ou d’examen.
Un algorithme qui passerait avec une stratégie de test manuel raisonnable avec une probabilité élevée. Il est peu probable que quelqu'un qui teste quelques petits cas de test à la main découvre la faille. Par exemple, "simuler QuickCheck manuellement à la main sur une douzaine de petits cas de test" ne devrait probablement pas révéler que l'algorithme est incorrect.
De préférence, un algorithme déterministe. J'ai vu beaucoup d'étudiants penser qu'essayer quelques cas de test à la main est un moyen raisonnable de vérifier si un algorithme déterministe est correct, mais je suppose que la plupart des étudiants ne présumeraient pas qu'essayer quelques cas de test est un bon moyen de vérifier des méthodes probabilistes. algorithmes. Pour les algorithmes probabilistes, il est souvent impossible de déterminer si une sortie donnée est correcte. et vous ne pouvez pas donner assez d'exemples pour effectuer un test statistique utile sur la distribution de la sortie. Je préférerais donc me concentrer sur les algorithmes déterministes, car ils vont plus clairement au cœur des idées fausses des étudiants.
J'aimerais enseigner l'importance de prouver que votre algorithme est correct, et j'espère utiliser quelques exemples comme celui-ci pour aider à motiver les preuves de l'exactitude. Je préférerais des exemples relativement simples et accessibles aux étudiants de premier cycle; Les exemples qui nécessitent des machines lourdes ou une tonne de connaissances mathématiques / algorithmiques sont moins utiles. En outre, je ne veux pas d’algorithmes «non naturels»; S'il est peut-être facile de construire un algorithme artificiel étrange pour tromper l'heuristique, s'il semble extrêmement artificiel ou si sa porte dérobée est construite simplement pour tromper cette heuristique, il ne sera probablement pas convaincant pour les étudiants. Des bons exemples?
Réponses:
Je pense qu’une erreur courante consiste à utiliser des algorithmes gloutons, ce qui n’est pas toujours la bonne approche, mais peut fonctionner dans la plupart des cas de test.
Exemple: dénominations de monnaie, et un nombre , expriment comme une somme de : s avec le moins de pièces que possible. n n d iré1, … , Dk n n réje
Une approche naïve consiste à utiliser d'abord la plus grosse pièce de monnaie possible et à produire goulûment une telle somme.
Par exemple, les pièces de valeur , et donneront des réponses correctes avec gourmandise pour tous les nombres compris entre et sauf pour le nombre .5 1 1 14 10 = 6 + 1 + 1 + 1 + 1 = 5 + 56 5 1 1 14 10 = 6 + 1 + 1 + 1 + 1 = 5 + 5
la source
Je me suis immédiatement rappelé un exemple de R. Backhouse (cela aurait pu être dans l'un de ses livres). Apparemment, il avait assigné une tâche de programmation où les étudiants devaient écrire un programme Pascal pour tester l’égalité de deux chaînes. L'un des programmes proposés par un étudiant était le suivant:
Nous pouvons maintenant tester le programme avec les entrées suivantes:
"université" "université" True; D'accord⇒
"cours" "cours" True; D'accord⇒
"" "" True; D'accord⇒
"université" "cours" False; D'accord⇒
"lecture" "course" False; D'accord⇒
"précision" "exactitude" Faux, OK⇒
Tout cela semble très prometteur: peut-être que le programme fonctionne effectivement. Mais des tests plus minutieux avec "pure" et "true" révèlent une sortie défectueuse. En fait, le programme dit "True" si les chaînes ont la même longueur et le même dernier caractère!
Cependant, les tests avaient été assez approfondis: nous avions des chaînes de longueur différente, des chaînes de même longueur mais de contenu différent, et même des chaînes égales. De plus, l'étudiant avait même testé et exécuté chaque branche. Vous ne pouvez pas vraiment dire que les tests avaient été négligents ici - étant donné que le programme est en effet très simple, il peut être difficile de trouver la motivation et l'énergie pour le tester de manière suffisamment approfondie.
Un autre exemple intéressant est la recherche binaire. Dans le TAOCP, Knuth a déclaré que "bien que l'idée de base de la recherche binaire soit relativement simple, les détails peuvent être étonnamment complexes". Apparemment, un bogue dans l'implémentation de la recherche binaire de Java est passé inaperçu pendant une décennie. C'était un bogue de débordement d'entier, qui ne se manifestait qu'avec une entrée assez grande. Les détails délicats des implémentations de recherche binaire sont également couverts par Bentley dans le livre Programming Pearls .
En bout de ligne: il peut être étonnamment difficile d’être certain qu’un algorithme de recherche binaire est correct en le testant simplement.
la source
Le meilleur exemple que j'ai jamais rencontré est le test de primalité:
Cela fonctionne pour (presque) tous les nombres, à l'exception de très peu d'exemples de compteurs, et il faut en fait une machine pour trouver un contre-exemple dans une période de temps réaliste. Le premier contre-exemple est 341, et la densité de contre-exemples diminue en fait avec p, bien que logarithmiquement.
Au lieu d'utiliser simplement 2 comme base de la puissance, on peut améliorer l'algorithme en utilisant également des nombres premiers petits et croissants supplémentaires au cas où le nombre premier précédent renvoyait 1. Et il existe encore un contre-exemple à ce schéma, à savoir les nombres de Carmichael, assez rare cependant
la source
En voici un qui m'a été lancé par les représentants de Google lors d'une convention à laquelle je suis allé. C'était codé en C, mais cela fonctionne dans d'autres langages qui utilisent des références. Désolé de devoir coder sur [cs.se], mais c'est le seul moyen de l'illustrer.
Cet algorithme fonctionnera pour toutes les valeurs données à x et y, même si elles ont la même valeur. Cela ne fonctionnera pas si on l'appelle comme swap (x, x). Dans cette situation, x finit par 0. Cela ne vous satisfera peut-être pas, car vous pouvez prouver que cette opération est mathématiquement correcte, mais vous oubliez tout de même ce cas.
la source
Il existe toute une classe d’algorithmes difficiles à tester: les générateurs de nombres pseudo-aléatoires . Vous ne pouvez pas tester une seule sortie, mais vous devez examiner de nombreuses séries de sorties avec des statistiques. En fonction de ce que vous testez et de la manière dont vous le testez, vous risquez de manquer des caractéristiques non aléatoires.
RANDU est un cas célèbre dans lequel les choses ont mal tourné . Il a réussi l'examen détaillé disponible à l'époque - qui n'a pas tenu compte du comportement des n - uplets des produits ultérieurs. Déjà les triples montrent beaucoup de structure:
Fondamentalement, les tests ne couvraient pas tous les cas d'utilisation: alors que l'utilisation unidimensionnelle de RANDU était (probablement en grande partie) satisfaisante, elle ne permettait pas de l'utiliser pour échantillonner des points tridimensionnels (de cette manière).
Un échantillonnage pseudo-aléatoire correct est une affaire délicate. Heureusement, il existe des suites de tests puissantes sur le marché, par exemple des machines à imprimer spécialisées dans la projection de toutes les statistiques dont nous disposons sur un générateur proposé. Est-ce suffisant?
Pour être juste, je n'ai aucune idée de ce que vous pouvez prouver de manière réaliste pour les GNRP.
la source
2D local maximum
entrée: 2 dimensions arrayAn×n A
sortie: un maximum local - une paire telle que n'a pas de cellule voisine dans le tableau qui contient une valeur strictement supérieure. A [ i , j ](i,j) A[i,j]
(Les cellules voisines sont celles de présentes dans le tableau.) Donc, par exemple, si estA[i,j+1],A[i,j−1],A[i−1,j],A[i+1,j] A
alors chaque cellule en gras est un maximum local. Chaque tableau non vide a au moins un maximum local.
Algorithme. Il existe un algorithme de temps : il suffit de vérifier chaque cellule. Voici une idée pour un algorithme plus rapide et récursif.O(n2)
Étant donné , définissez la croix comme étant composée des cellules de la colonne du milieu et des cellules de la rangée du milieu. Tout d' abord vérifier chaque cellule pour voir si la cellule est un maximum local dans . Si tel est le cas, retournez une telle cellule. Sinon, soit une cellule dans avec une valeur maximale. Puisque n'est pas un maximum local, il doit avoir une cellule voisine avec une valeur plus grande.A X X A (i,j) X (i,j) (i′,j′)
Partitionnez (le tableau , moins les cellules de ) en quatre quadrants - les quadrants supérieur gauche, supérieur droit, inférieur gauche et inférieur droit - de manière naturelle. La cellule voisine avec la plus grande valeur doit être dans l'un de ces quadrants. Appelez ce quadrant .A∖X A X (i′,j′) A′
Lemme. Quadrant contient un maximum local de .A′ A
Preuve. Pensez à partir de la cellule . Si ce n'est pas un maximum local, déplacez-vous vers un voisin avec une valeur plus grande. Cela peut être répété jusqu'à arriver à une cellule qui est un maximum local. Cette dernière cellule doit être en , car est limité de tous les côtés par des cellules dont les valeurs sont inférieures à la valeur de la cellule . Cela prouve le lemma.(i′,j′) A′ A′ (i′,j′) ⋄
L'algorithme se nomme récursivement sur le sous-tableau pour y trouver un maximum local , puis renvoie cette cellule.n2×n2 A′ (i,j)
Le temps d'exécution pour une matrice satisfait , donc . n × n T ( n ) = T ( n / 2 ) + O ( n ) T ( n ) = O ( n )T(n) n×n T(n)=T(n/2)+O(n) T(n)=O(n)
Ainsi, nous avons prouvé le théorème suivant:
Ou avons-nous?
la source
Ce sont des exemples de primalité, car ils sont courants.
(1) Primalité dans SymPy. Numéro 1789 . Il y avait un test incorrect mis sur un site Web bien connu qui n'a échoué qu'après 10 ^ 14. Bien que le correctif soit correct, il s'agissait simplement de corriger les trous plutôt que de repenser le problème.
(2) Primalité en Perl 6. Perl6 a ajouté is-prime, qui utilise un certain nombre de tests MR avec des bases fixes. Il existe des contre-exemples connus, mais ils sont assez volumineux car le nombre de tests par défaut est énorme (masquant le vrai problème en dégradant les performances). Cela sera bientôt résolu.
(3) Primalité dans FLINT. n_isprime () retourne vrai pour les composites , depuis corrigé. Fondamentalement, le même problème que SymPy. En utilisant la base de données Feitsma / Galway des pseudoprimes SPRP-2 à 2 ^ 64, nous pouvons maintenant les tester.
(4) Maths de Perl :: Primality. is_aks_prime cassé . Cette séquence semble similaire à beaucoup d'implémentations AKS - beaucoup de code qui a fonctionné par accident (par exemple, s'est perdu à l'étape 1 et a fini par tout faire par division d'essai) ou n'a pas fonctionné pour des exemples plus grands. Malheureusement, AKS est si lent qu'il est difficile à tester.
(5) is_prime de la version antérieure à 2.2 de Pari. Math :: Billet Pari . Il a utilisé 10 bases aléatoires pour les tests MR (avec une valeur de départ fixe au démarrage, plutôt que la valeur de départ établie de GMP à chaque appel). Il vous dira que 9 correspond à environ 1 appel sur 1 million. Si vous choisissez le bon numéro, vous pouvez le faire échouer assez souvent, mais les chiffres deviennent plus clairsemés, de sorte qu'il n'apparaît pas beaucoup dans la pratique. Ils ont depuis changé l'algorithme et l'API.
Ce n’est pas faux, mais c’est un classique des tests probabilistes: combien de tours donnez-vous, disons, mpz_probab_prime_p? Si nous lui donnons 5 tours, il semble bien que cela fonctionne bien - les nombres doivent réussir un test de Fermat en base 210, puis 5 tests présélectionnés Miller-Rabin. Vous ne trouverez pas de contre-exemple avant 3892757297131 (avec GMP 5.0.1 ou 6.0.0a), vous devrez donc effectuer de nombreux tests pour le trouver. Mais il existe des milliers de contre-exemples de moins de 2 ^ 64. Donc, vous continuez à augmenter le nombre. À quelle distance? Y a-t-il un adversaire? Quelle est l’importance d’une réponse correcte? Confondez-vous des bases aléatoires avec des bases fixes? Savez-vous quelle taille d'entrée vous sera donnée?
Ce sont assez difficiles à tester correctement. Ma stratégie inclut des tests unitaires évidents, ainsi que des exemples de défaillances observées auparavant ou dans d'autres packages, des tests par rapport à des bases de données connues lorsque cela est possible (par exemple, si vous effectuez un seul test MR en base 2, vous réduirez ainsi le calcul infaisable. tâche de tester 2 ^ 64 numéros pour tester environ 32 millions de chiffres), et enfin, de nombreux tests randomisés utilisant un autre package en standard. Le dernier point fonctionne pour des fonctions telles que la primalité où il existe une entrée assez simple et une sortie connue, mais quelques tâches sont comme cela. J'ai utilisé cela pour trouver des défauts dans mon propre code de développement ainsi que des problèmes occasionnels dans les packages de comparaison. Mais compte tenu de l'espace d'entrée infini, nous ne pouvons pas tout tester.
Pour ce qui est de prouver l’exactitude, voici un autre exemple de primalité. Les méthodes BLS75 et ECPP ont le concept d'un certificat de primalité. Essentiellement, une fois qu'ils ont terminé leurs recherches pour trouver des valeurs qui fonctionnent pour leurs preuves, ils peuvent les générer dans un format connu. On peut alors écrire un vérificateur ou faire écrire par quelqu'un d'autre. Celles-ci fonctionnent très vite comparé à la création, et maintenant soit (1) les deux morceaux de code sont incorrects (d'où la raison pour laquelle vous préférez utiliser d'autres programmeurs pour les vérificateurs), ou (2) le calcul derrière l'idée de preuve est faux. Le numéro 2 est toujours possible, mais ils ont généralement été publiés et examinés par plusieurs personnes (et sont parfois assez faciles pour que vous puissiez vous y retrouver).
En comparaison, des méthodes comme AKS, APR-CL, la division d’essai ou le test déterministe de Rabin ne produisent toutes que des résultats autres que "premier" ou "composite". Dans le dernier cas, nous pouvons avoir un facteur qui peut donc être vérifié, mais dans le premier cas, il ne nous reste rien d'autre que ce bit de sortie. Le programme a-t-il fonctionné correctement? Dunno.
Il est important de tester le logiciel sur plus que quelques exemples de jouets, et de passer en revue quelques exemples à chaque étape de l'algorithme et de dire "compte tenu de cette entrée, est-il logique que je sois ici avec cet état?"
la source
L'algorithme de brassage de Fisher-Yates-Knuth est un exemple (pratique) sur lequel l' un des auteurs de ce site a fait des commentaires .
L'algorithme génère une permutation aléatoire d'un tableau donné sous la forme:
Un algorithme "naïf" pourrait être:
Où dans la boucle, l'élément à échanger est choisi parmi tous les éléments disponibles. Cependant, cela produit un échantillonnage biaisé des permutations (certaines sont surreprésentées, etc.)
En fait, on peut imaginer le brassage pêcher-mêlée en utilisant une analyse de comptage simple (ou naïf) .
Le principal problème pour vérifier si l’algorithme de réorganisation est correct ou non ( biaisé ou non ) est qu’en raison des statistiques, un grand nombre d’échantillons est nécessaire. L' article de codinghorror que je renvoie ci-dessus explique exactement cela (et les tests réels).
la source
Le meilleur exemple (lire: ce qui me fait le plus mal au ventre ) que j’ai jamais vu concerne la conjecture de collatz. J'étais dans un concours de programmation (avec un prix de 500 dollars sur la ligne pour la première place) dans lequel l'un des problèmes était de trouver le nombre minimum d'étapes nécessaires pour que deux numéros atteignent le même nombre. La solution consiste bien sûr à alterner chaque étape jusqu'à ce que les deux obtiennent quelque chose que l'on a déjà vu. On nous a donné une plage de nombres (je crois que c'était entre 1 et 1000000) et on nous a dit que la conjecture de collatz avait été vérifiée jusqu'à 2 ^ 64, de sorte que tous les nombres qui nous ont été donnés finiraient par converger à 1. J'ai utilisé 32 bits entiers pour faire les étapes avec cependant. Il se trouve qu’il existe un nombre obscur compris entre 1 et 1000000 (170 000 environ) qui entraînera un dépassement d’un nombre entier 32 bits en temps voulu. En fait, ces chiffres sont extrêmement rares ci-dessous 2 ^ 31. Nous avons testé notre système à la recherche de nombres ÉNORMES bien supérieurs à 1 000 000 pour "garantir" que le débordement ne se produisait pas. Il s'avère qu'un nombre beaucoup plus petit que nous n'avons pas testé a provoqué un débordement. Parce que j'ai utilisé "int" au lieu de "long", je n'ai reçu qu'un prix de 300 dollars au lieu d'un prix de 500 $.
la source
Le problème de Knapsack 0/1 en est un que presque tous les étudiants pensent pouvoir être résolu par un algorithme glouton. Cela se produit plus souvent si vous présentiez auparavant des solutions gloutonnes en tant que version du problème de Knapsack où un algorithme glouton fonctionne .
Pour ces problèmes, en classe , je devrais montrer la preuve de Knapsack 0/1 ( programmation dynamique ) pour éliminer tout doute et pour la version du problème glouton aussi. En réalité, les deux preuves ne sont pas anodines et les étudiants les trouvent probablement très utiles. De plus, il y a un commentaire à ce sujet dans CLRS 3ed , Chapitre 16, Page 425-427 .
Problème: le voleur dévalise un magasin et peut transporter un poids maximal de W dans son sac à dos. Il y a n articles et chaque article pèse wi et vaut vi dollars. Quels articles le voleur doit-il prendre? maximiser son gain ?
Problème avec le sac à dos 0/1 : la configuration est la même, mais les objets ne peuvent pas être cassés en morceaux plus petits , ainsi le voleur peut décider de prendre un objet ou de le laisser (choix binaire), mais ne peut prendre une fraction d'un objet. .
Et vous pouvez obtenir des étudiants des idées ou des algorithmes qui suivent la même idée que le problème de version gourmande, à savoir:
Est-ce utile pour vous? en fait, nous savons que le problème de la pièce de monnaie est une version du problème du sac à dos. Mais il y a plus d'exemples dans la forêt de problèmes de sacs à dos, par exemple, qu'en est-il de Knapsack 2D (c'est vraiment utile lorsque vous voulez couper du bois pour fabriquer des meubles , j'ai vu dans un local de ma ville), il est très courant de penser que le gourmand fonctionne ici aussi, mais pas.
la source
Une erreur courante consiste à implémenter des algorithmes de mélange aléatoires. Voir la discussion sur wikipedia .
la source
Pythons PEP450 ayant introduit des fonctions de statistiques dans la bibliothèque standard pourrait présenter un intérêt. Dans le cadre de la justification d'une fonction qui calcule la variance dans la bibliothèque standard de python, l'auteur Steven D'Aprano écrit:
Le problème concerne les valeurs numériques et la perte de précision. Si vous voulez une précision maximale, vous devez ordonner vos opérations d’une certaine manière. Une implémentation naïve conduit à des résultats incorrects car l'imprécision est trop grande. C’était l’une des questions sur lesquelles mon cours de mathématiques à l’université avait trait.
la source
Bien que ce ne soit probablement pas tout à fait ce que vous cherchez, il est certainement facile à comprendre et tester quelques petits cas sans aucune réflexion aboutira à un algorithme incorrect.
Solution proposée :
Cette approche "essayer quelques petits cas et déduire un algorithme du résultat" apparaît souvent (mais pas aussi fort qu'ici) dans les compétitions de programmation où la pression consiste à créer un algorithme que (a) est rapide à mettre en œuvre et (b ) a un temps d'exécution rapide.
la source