Comment tromper l'heuristique «essayez des scénarios de test»: des algorithmes qui semblent corrects, mais qui sont en réalité incorrects


106

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:

  1. 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.)

  2. 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.

  3. 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.

  4. 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?


2
J'adore votre question, il est également lié à une question très intéressante , j'ai vu sur les mathématiques , l'autre jour en rapport avec de grandes conjectures constantes réfutant. Vous pouvez le trouver ici
ZeroUltimax Le

1
Encore plus à creuser et j'ai trouvé ces deux algorithmes géométriques.
ZeroUltimax

@ ZeroUltimax Vous avez raison, il n'est pas garanti que le point central de 3 points non colinéaires soit à l'intérieur. La solution rapide consiste à placer un point sur la ligne située entre l'extrême gauche et l'extrême droite. Y a-t-il autre problème où?
InformedA

La prémisse de cette question me semble étrange et j’ai du mal à me faire comprendre, mais j’estime que le processus de conception d’algorithmes décrit ci-dessus est fondamentalement rompu. Même pour les étudiants qui ne s'arrêtent pas là, c'est condamné. 1> algorithme d'écriture, 2> penser à / exécuter des cas de test, 3a> arrêter ou 3b> prouver que c'est correct. La première étape à peu près a être l' identification des classes d'entrée pour le domaine du problème. Les cas d'angle et l'algorithme lui-même découlent de ceux-ci. (suite)
Mr.Mindor

1
Comment distingue-t-on formellement un bogue d'implémentation d'un algorithme défectueux? Votre question m'intéressait, mais en même temps j'étais préoccupé par le fait que la situation que vous décrivez semble être plus la règle que l'exception. Beaucoup de gens testent ce qu'ils implémentent, mais ils ont toujours des bogues. Le deuxième exemple de la réponse la plus votée est précisément un tel bug.
babou

Réponses:


70

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 id1,,dknndi

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 + 565111410=6+1+1+1+1=5+5


10
C’est effectivement un bon exemple, en particulier celui pour lequel les étudiants se trompent régulièrement. Vous devez non seulement choisir des jeux de pièces particuliers, mais également des valeurs particulières pour que l'algorithme échoue.
Raphaël

2
En outre, permettez-moi de dire que les étudiants auront souvent de mauvaises preuves dans cet exemple (arborant des arguments naïfs qui échoueront à l'examen approfondi), de sorte que plus d'une leçon peut être apprise ici.
Raphaël

2
Le système de monnaie britannique à l’ancienne (avant la décimalisation de 1971) en était un exemple concret. Un algorithme gourmand pour compter quatre shillings utiliserait une demi-couronne (2½ shillings), une pièce d'un shilling et un six pence (½ shilling). Mais la solution optimale utilise deux florins (2 shillings chacun).
Mark Dominus

1
En effet, dans de nombreux cas, les algorithmes gloutons semblent raisonnables, mais ne fonctionnent pas - un autre exemple est la correspondance bipartite maximale. D'autre part, il y a aussi des exemples où il semble qu'un algorithme glouton ne devrait pas fonctionner, mais c'est le cas: arbre de recouvrement maximal.
JKFF

62

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:

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

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

write(issame);

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.


9
Bien sûr, la faille est assez évidente à la source (si vous avez déjà écrit une chose similaire auparavant).
Raphaël

3
Même si la faille simple dans l'exemple de programme est corrigée, les chaînes de caractères posent un certain nombre de problèmes intéressants! L'inversion de chaîne est un classique - la méthode "de base" consiste à inverser simplement les octets. Ensuite, l'encodage entre en jeu. Ensuite, les mères porteuses (généralement deux fois). Le problème est, bien sûr, qu’il n’ya pas de moyen facile de prouver formellement que votre méthode est correcte.
Ordous

6
J'interprète peut-être complètement la question, mais cela semble être une faille dans la mise en œuvre plutôt que dans l' algorithme lui-même.
Mr.Mindor

8
@ Mr.Mindor: comment savoir si le programmeur a écrit un algorithme correct et l'a ensuite mal implémenté, ou s'il a écrit un algorithme incorrect et l'a ensuite appliqué fidèlement (j'hésite à dire "correctement"!)
Steve Jessop

1
@ wabbit C'est discutable. Ce qui est évident pour vous pourrait ne pas l'être pour un étudiant de première année.
Juho

30

Le meilleur exemple que j'ai jamais rencontré est le test de primalité:

entrée: nombre naturel p, p! = 2
sortie: est-ce que pa prime ou pas?
algorithme: calcul 2 ** (p-1) mod p. Si result = 1 alors p est premier sinon p n'est pas.

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


Le test de primalité de Fermat est un test probabiliste, votre post-condition n'est donc pas correcte.
Femaref

5
c’est un test probabiliste, mais la réponse montre bien (plus généralement) à quel point les algorithmes probabilistes pris pour des algorithmes exacts peuvent être une source d’erreur. plus sur les numéros Carmichael
vzn

2
C'est un bel exemple, avec une limitation: pour l'utilisation pratique des tests de primalité que je connais bien, à savoir la génération de clés cryptographiques asymétriques, nous utilisons des algorithmes probabilistes! Les nombres sont trop grands pour des tests exacts (s'ils ne l'étaient pas, ils ne conviendraient pas à la cryptographie car les clés pourraient être trouvées par la force brute en temps réel).
Gilles

1
la limitation à laquelle vous faites référence est pratique, et non théorique, et les tests principaux dans les systèmes de cryptographie, par exemple RSA, sont sujets à des échecs rares / très improbables pour ces raisons, soulignant à nouveau la signification de l'exemple. C'est-à-dire que dans la pratique, cette limitation est parfois considérée comme inévitable. il existe P algorithmes de temps pour tester la primalité, par exemple AKS, mais ils prennent trop de temps pour les nombres "plus petits" utilisés dans la pratique.
vzn

Si vous testez non seulement avec 2 p, mais avec un p pour 50 valeurs aléatoires différentes 2 ≤ a <p, la plupart des gens le sauront probabiliste, mais avec des échecs si improbables qu'il est plus probable qu'un dysfonctionnement de votre ordinateur produise la mauvaise réponse. Avec 2 p, 3 p, 5 p et 7 p, les échecs sont déjà très rares.
gnasher729

21

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.

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

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.


1
Cette astuce a été utilisée dans le concours C sournois pour produire une implémentation défectueuse de RC4 . En relisant cet article, je viens de remarquer que ce hack a probablement été soumis par @DW
CodesInChaos

7
Cette faille est en effet subtile - mais la faille est spécifique à la langue, ce n'est donc pas vraiment une faille dans l'algorithme; c'est une faille dans la mise en œuvre. On pourrait trouver d’autres exemples de bizarreries linguistiques qui permettent de dissimuler facilement des imperfections subtiles, mais ce n’était pas vraiment ce que je cherchais (je cherchais quelque chose au niveau de l’abstraction des algorithmes). En tout état de cause, cette faille n’est pas une démonstration idéale de la valeur de la preuve; à moins que vous ne songiez déjà à créer un alias, vous risquez de ne pas tenir compte du même problème lorsque vous écrivez votre "preuve" de correction.
DW

C'est pourquoi je suis surpris que cela ait été voté si haut.
ZeroUltimax

2
@DW Cela dépend du modèle dans lequel vous définissez l'algorithme. Si vous descendez à un niveau où les références de mémoire sont explicites (plutôt que le modèle commun qui suppose l'absence de partage), il s'agit d'un défaut d'algorithme. La faille n'est pas vraiment spécifique à une langue, elle apparaît dans toutes les langues prenant en charge le partage de références de mémoire.
Gilles

16

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.


2
bel exemple, mais en réalité, il n’existe aucun moyen de prouver qu’un PRNG n’a pas de défaut, il n’ya qu’une hiérarchie infinie de tests plus faibles et de tests plus forts. prouver que l'on est "aléatoire" dans un sens strict est vraisemblablement indécidable (je ne l'ai pas encore prouvé).
vzn

1
C'est une bonne idée de quelque chose qui est difficile à tester, mais les GNA sont également difficiles à prouver. Les PRNG ne sont pas tellement sujets aux bogues d’implémentation qu’ils soient mal spécifiés. Des tests tels que "hardhard" conviennent à certains usages, mais pour la cryptographie, vous pouvez passer à la vitesse supérieure et continuer à vous faire rire de la salle. Il n'y a pas de CSPRNG «éprouvé et sécurisé», le mieux que vous puissiez espérer est de prouver que si votre CSPRNG est cassé, alors AES l'est aussi.
Gilles

@Gilles Je n'essayais pas d'entrer en crypto, seulement du statistique statistique (je pense que les deux ont à peu près des exigences orthogonales). Dois-je le préciser dans la réponse?
Raphaël

1
La crypto-aléatoire implique une statistique aléatoire. Pour autant que je sache, aucune définition mathématique formelle ne figure cependant à côté de la notion idéale (et contradictoire avec le concept de PRNG implémenté sur une machine de Turing déterministe) de l’aléatoire de la théorie de l’information. Le hasard statistique a-t-il une définition formelle au-delà de «doit être indépendant des distributions sur lesquelles nous allons le tester»?
Gilles

1
@vzn: ce que signifie être une séquence aléatoire de nombres peut être défini de nombreuses façons, mais une simple est "grande complexité de Komolgorov". Dans ce cas, il est facile de montrer que déterminer le caractère aléatoire est indécidable.
Cody le

9

2D local maximum

entrée: 2 dimensions arrayAn×nA

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,j1],A[i1,j],A[i+1,j]A

0134323125014013

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.AXXA(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 . AXAX(i,j)A

Lemme. Quadrant contient un maximum local de .AA

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)AA(i,j)

L'algorithme se nomme récursivement sur le sous-tableau pour y trouver un maximum local , puis renvoie cette cellule.n2×n2A(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×nT(n)=T(n/2)+O(n)T(n)=O(n)

Ainsi, nous avons prouvé le théorème suivant:

O(n)n×n

Ou avons-nous?


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

2
C'est un bel exemple! J'aime cela. Je vous remercie. (J'ai enfin compris la faille dans cet algorithme. D'après l'horodatage, vous pouvez obtenir une limite inférieure sur le temps qu'il m'a fallu. Je suis trop gêné pour révéler l'heure réelle. :-)
DW

1
O(n)

8

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?

1016

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?"


1
Beaucoup d’entre elles ressemblent soit à (1) des erreurs d’implémentation (l’algorithme sous-jacent est correct mais il n’a pas été implémenté correctement), ce qui est intéressant mais n’est pas le but de cette question, ou bien (2) à un choix délibéré et conscient de choisir est rapide et fonctionne généralement, mais peut échouer avec une probabilité très faible (pour le code qui teste avec une base aléatoire ou quelques bases fixes / aléatoires, j'espère que celui qui choisira de le faire sait qu'il fait un compromis sur ses performances).
DW

Vous avez raison sur le premier point - algorithme correct + bogue n'est pas le problème, bien que la discussion et d'autres exemples les confondent également. Le champ est mûr avec des conjectures qui fonctionnent pour de petits nombres mais sont incorrectes. Pour le point (2), cela est vrai pour certains, mais mes exemples n ° 1 et n ° 3 ne sont pas ce cas - on croyait que l’algorithme était correct (ces 5 bases donnent des résultats prouvés pour des nombres inférieurs à 10 ^ 16), puis plus tard découvert que ce n'était pas.
DanaJ

N'est-ce pas un problème fondamental avec les tests de pseudo-primalité?
Asmeurer

asmeurer, oui dans mon # 2 et la discussion ultérieure d’eux. Mais les n ° 1 et n ° 3 étaient les deux cas d'utilisation de Miller-Rabin avec des bases connues pour donner des résultats déterministes corrects en dessous d'un seuil. Donc, dans ce cas, l '"algorithme" (en utilisant le terme librement pour faire correspondre le PO) était incorrect. N ° 4 n'est pas un test primordial, mais comme DW l'a souligné, l'algorithme fonctionne bien, c'est simplement la mise en œuvre qui est difficile. Je l'ai inclus car cela conduit à une situation similaire: des tests sont nécessaires et jusqu'à quel point allez-vous au-delà de simples exemples avant de dire que cela fonctionne?
DanaJ

Certains de vos messages semblent correspondre à la question, d'autres non (commentaire de @ DW). Supprimez les exemples (et autres contenus) qui ne répondent pas à la question.
Raphaël

7

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:

 // 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]

ij0ji

Un algorithme "naïf" pourrait être:

 // 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]

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) .

nn!=n×n1×n2..nn1

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).


1
Voir ici pour un exemple de preuve d'exactitude pour un algorithme de lecture aléatoire.
Raphaël

5

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 $.


5

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:

  • Prenez la capacité totale du sac, et mettez autant que possible l'objet le plus intéressant, et répétez cette méthode jusqu'à ce que vous ne puissiez plus en mettre car le sac est plein ou qu'il n'y a pas d'objet de poids égal à mettre dans le sac.
  • Une autre mauvaise façon de penser est de penser: mettre des articles plus légers et mettre ceux-ci suivants du plus haut au plus bas prix
  • ...

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.


Greedy était déjà couvert dans la réponse acceptée , mais le problème de Knapsack, en particulier, convient bien à la définition de certains pièges.
Raphaël

3

Une erreur courante consiste à implémenter des algorithmes de mélange aléatoires. Voir la discussion sur wikipedia .

n!nn(n1)n


1
C'est un bon bogue, mais ce n'est pas une bonne illustration de tromper l'heuristique des scénarios de test, car les tests ne s'appliquent pas vraiment à un algorithme de réorganisation (c'est aléatoire, comment le testeriez-vous? Que signifierait échouer un scénario de test?). comment le détecteriez-vous en regardant la sortie?)
DW

Vous le testez statistiquement bien sûr. Le caractère aléatoire uniforme est loin de "tout peut arriver dans la sortie". Ne seriez-vous pas méfiant si un programme censé imiter un dé vous donnait 100 3 de suite?
Per Alexandersson

Encore une fois, je parle de l'heuristique étudiante consistant à "essayer quelques cas de test à la main". J'ai vu de nombreux étudiants penser que c'était un moyen raisonnable de vérifier si un algorithme déterministe était correct, mais je suppose qu'ils ne présumeraient pas que c'est un bon moyen de vérifier si un algorithme de brassage est correct (étant donné qu'un algorithme de brassage est aléatoire, aucun moyen de savoir si un résultat particulier est correct, dans tous les cas, vous ne pouvez pas manipuler suffisamment d'exemples à la main pour effectuer un test statistique utile). Je ne pense donc pas que le fait de mélanger des algorithmes aidera beaucoup à dissiper l’idée fausse commune.
DW

1
@PerAlexandersson: Même si vous ne générez qu'un seul shuffle, l'utilisation de MT avec n> 2080 ne peut pas être vraiment aléatoire. Maintenant, l'écart par rapport à ce qui est attendu sera très petit, vous ne vous en soucierez donc probablement pas ... mais cela s'applique même si vous générez beaucoup moins que la période (comme le souligne asmeurer ci-dessus).
Charles le

2
Cette réponse semble avoir été rendue obsolète par celle plus élaborée de Nikos M. ?
Raphaël

2

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:

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)

Ce qui précède semble être correct avec un test occasionnel:

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

Mais l’ajout d’une constante à chaque point de données ne devrait pas changer la variance:

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

Et la variance ne devrait jamais être négative:

>>> variance(data*100)
  -1239429440.1282566

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.


1
n1

2
@Raphael: Bien que pour être juste, l'algorithme choisi est bien connu pour être un mauvais choix pour les données à virgule flottante.

2
Il ne s'agit pas simplement de la mise en œuvre de l'opération concernant 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. C’était l’une des questions sur lesquelles mon cours de mathématiques à l’université avait trait.
Christian

En plus du commentaire précis de Raphael, l'un des inconvénients de cet exemple est que je ne pense pas qu'une preuve d'exactitude aiderait à éviter ce problème. Si vous ne connaissez pas les subtilités de l'arithmétique en virgule flottante, vous pouvez penser que vous avez prouvé que c'est bien cela (en prouvant que la formule est valide). Ce n’est donc pas un exemple idéal pour expliquer aux élèves pourquoi il est important de prouver que leurs algorithmes sont corrects. Si les élèves ont vu cet exemple, je pense qu'ils tireraient plutôt la leçon suivante: "Le calcul en virgule flottante / numérique est délicat".
DW

1

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.

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

Solution proposée :

int f(int n) {
   return 1;
}

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

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.


5
Je ne pense pas que ce soit un très bon exemple, car peu de gens essaient de trouver les diviseurs d'un polynôme en renvoyant 1.
Brian S

1
nn3n

Cela peut être pertinent, en ce sens que le retour d'une valeur constante pour les diviseurs (ou une autre formulation) peut être le résultat d'une approche algorithmique erronée du problème (par exemple, un problème statistique ou la non gestion des cas extrêmes de l'algorithme). Cependant, la réponse doit être reformulée
Nikos M.

@ NikosM. Il h. Je me sens comme si je battais un cheval mort ici, mais le deuxième paragraphe de la question dit que "si leur algorithme fonctionne correctement sur une poignée d'exemples, y compris tous les cas difficiles qu'ils peuvent penser d'essayer, ils concluent que l'algorithme doit Il y a toujours un étudiant qui demande: "Pourquoi dois-je prouver que mon algorithme est correct, si je peux juste l'essayer sur quelques cas de test?" Dans cet exemple, pour les 40 premières valeurs (bien plus qu'un étudiant il est vrai que je pense que c’est ce que recherchait le PO.
Rick Decker

Ok, oui, mais cette formulation est triviale (peut-être généralement correcte), mais pas dans l’esprit de la question. Encore besoin de reformuler
Nikos M.
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.