Cómo engañar a la heurística de "probar algunos casos de prueba": Algoritmos que parecen correctos, pero en realidad son incorrectos


105

Para intentar probar si un algoritmo para algún problema es correcto, el punto de partida habitual es intentar ejecutar el algoritmo a mano en una serie de casos de prueba simples; pruébelo en algunos ejemplos de problemas, incluidos algunos "casos de esquina" simples ". Esta es una gran heurística: es una excelente manera de eliminar rápidamente muchos intentos incorrectos de un algoritmo y de comprender por qué el algoritmo no funciona.

Sin embargo, cuando aprenden algoritmos, algunos estudiantes se sienten tentados a detenerse allí: si su algoritmo funciona correctamente en un puñado de ejemplos, incluidos todos los casos de esquina que pueden pensar intentar, entonces concluyen que el algoritmo debe ser correcto. Siempre hay un estudiante que pregunta: "¿Por qué necesito probar que mi algoritmo es correcto, si puedo probarlo en algunos casos de prueba?"

Entonces, ¿cómo engañas a la heurística "prueba un montón de casos de prueba"? Estoy buscando algunos buenos ejemplos para demostrar que esta heurística no es suficiente. En otras palabras, estoy buscando uno o más ejemplos de un algoritmo que superficialmente parece ser correcto, y que genera la respuesta correcta en todas las entradas pequeñas que cualquiera pueda encontrar, pero donde el algoritmo realmente no funciona Tal vez el algoritmo funciona correctamente en todas las entradas pequeñas y solo falla para las entradas grandes, o solo falla para las entradas con un patrón inusual.

Específicamente, estoy buscando:

  1. Un algoritmo La falla tiene que estar en el nivel algorítmico. No estoy buscando errores de implementación. (Por ejemplo, como mínimo, el ejemplo debe ser independiente del lenguaje y la falla debe relacionarse con preocupaciones algorítmicas en lugar de problemas de implementación o ingeniería de software).

  2. Un algoritmo que alguien podría inventar. El pseudocódigo debería verse al menos plausiblemente correcto (por ejemplo, el código que está ofuscado u obviamente dudoso no es un buen ejemplo). Puntos de bonificación si se trata de un algoritmo que a algún alumno se le ocurrió al tratar de resolver un problema de tarea o examen.

  3. Un algoritmo que pasaría una estrategia de prueba manual razonable con alta probabilidad. Es improbable que alguien que prueba algunos casos de prueba pequeños a mano descubra la falla. Por ejemplo, "simular QuickCheck a mano en una docena de casos de prueba pequeños" no debería revelar que el algoritmo es incorrecto.

  4. Preferiblemente, un algoritmo determinista. He visto a muchos estudiantes pensar que "probar algunos casos de prueba a mano" es una forma razonable de verificar si un algoritmo determinista es correcto, pero sospecho que la mayoría de los estudiantes no supondría que probar algunos casos de prueba es una buena manera de verificar probabilidades algoritmos Para los algoritmos probabilísticos, a menudo no hay forma de saber si alguna salida en particular es correcta; y no puede hacer suficientes ejemplos a mano para hacer una prueba estadística útil sobre la distribución de salida. Por lo tanto, preferiría centrarme en los algoritmos deterministas, ya que llegan de manera más clara al corazón de los conceptos erróneos de los estudiantes.

Me gustaría enseñar la importancia de probar que su algoritmo es correcto, y espero utilizar algunos ejemplos como este para ayudar a motivar las pruebas de corrección. Preferiría ejemplos que sean relativamente simples y accesibles para estudiantes universitarios; Los ejemplos que requieren maquinaria pesada o un montón de antecedentes matemáticos / algorítmicos son menos útiles. Además, no quiero algoritmos que sean "antinaturales"; Si bien puede ser fácil construir algún algoritmo artificial extraño para engañar a la heurística, si se ve muy poco natural o tiene una puerta trasera obvia construida solo para engañar a esta heurística, probablemente no sea convincente para los estudiantes. ¿Algún buen ejemplo?


2
Me encanta tu pregunta, también está relacionada con una pregunta muy interesante que vi en Matemáticas el otro día relacionada con refutar conjeturas con constantes grandes. Puede encontrarlo aquí
ZeroUltimax

1
Un poco más de excavación y encontré esos dos algoritmos geométricos.
ZeroUltimax

@ZeroUltimax Tienes razón, no se garantiza que el punto central de 3 puntos no colineales esté dentro. El remedio rápido es obtener un punto en la línea entre el extremo izquierdo y el extremo derecho. ¿Hay algún otro problema donde?
InformadoA

La premisa de esta pregunta me parece extraña en una forma en que tengo dificultades para entenderlo, pero creo que todo se reduce a que el proceso para el diseño del algoritmo como se describe es fundamentalmente roto. Incluso para los estudiantes que no 'se detienen allí' está condenado. 1> algoritmo de escritura, 2> pensar / ejecutar casos de prueba, 3a> detener o 3b> probar correcto. El primer paso más o menos ha sido identificar las clases de entrada para el dominio del problema. Los casos de esquina y el algoritmo en sí surgen de ellos. (cont)
Mr.Mindor

1
¿Cómo distingue formalmente un error de implementación de un algoritmo defectuoso? Su pregunta me interesó, pero al mismo tiempo me molestó el hecho de que la situación que describe parece ser más la regla que la excepción. Muchas personas prueban lo que implementan, pero generalmente todavía tienen errores. El segundo ejemplo de la respuesta más votada es precisamente ese error.
babou

Respuestas:


70

Creo que un error común es utilizar algoritmos codiciosos, que no siempre es el enfoque correcto, pero que podría funcionar en la mayoría de los casos de prueba.

Ejemplo: las denominaciones de monedas, y un número , expresan como una suma de : s con la menor cantidad de monedas posible. n n d ire1,...,reknortenortereyo

Un enfoque ingenuo es usar la moneda más grande posible primero y producir con avidez tal suma.

Por ejemplo, las monedas con valor , y darán respuestas correctas con codicia para todos los números entre y excepto el número .5 1 1 14 10 = 6 + 1 + 1 + 1 + 1 = 5 + 56 65 5111410=6 6+1+1+1+1=5 5+5 5


10
Este es de hecho un buen ejemplo, en particular uno en el que los estudiantes se equivocan habitualmente. No solo necesita elegir conjuntos de monedas en particular, sino también valores particulares para ver que el algoritmo falla.
Raphael

2
Además, permítanme decir que los estudiantes también suelen tener pruebas erróneas en este ejemplo (con algunos argumentos ingenuos que fallan en un examen más detallado), por lo que aquí se puede aprender más de una lección.
Raphael

2
El antiguo sistema de monedas británico (antes de la decimalización de 1971) tenía un ejemplo real de esto. Un algoritmo codicioso para contar cuatro chelines usaría una media corona (2½ chelines), una moneda de un chelín y seis peniques (½ chelín). Pero la solución óptima utiliza dos florines (2 chelines cada uno).
Mark Dominus

1
De hecho, en muchos casos los algoritmos codiciosos parecen razonables, pero no funcionan; otro ejemplo es la coincidencia bipartita máxima. Por otro lado, también hay ejemplos en los que parece que un algoritmo codicioso no debería funcionar, pero lo hace: árbol de expansión máxima.
jkff

62

Inmediatamente recordé un ejemplo de R. Backhouse (esto podría haber estado en uno de sus libros). Aparentemente, había asignado una tarea de programación donde los estudiantes tenían que escribir un programa Pascal para probar la igualdad de dos cadenas. Uno de los programas entregados por un estudiante fue el siguiente:

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

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

write(issame);

Ahora podemos probar el programa con las siguientes entradas:

"universidad" "universidad" Verdadero; Okay

"curso" "curso" Verdadero; Okay

"" "" Verdadero; Okay

"curso" "universitario" falso; Okay

"conferencia" "curso" Falso; Okay

"precisión" "exactitud" Falso, OK

Todo esto parece muy prometedor: tal vez el programa sí funciona. Pero una prueba más cuidadosa con decir "puro" y "verdadero" revela una salida defectuosa. De hecho, el programa dice "Verdadero" si las cadenas tienen la misma longitud y el mismo último carácter.

Sin embargo, las pruebas habían sido bastante exhaustivas: teníamos cadenas con diferente longitud, cadenas con igual longitud pero diferente contenido, e incluso cadenas iguales. Además, el estudiante incluso había probado y ejecutado cada rama. Realmente no se puede argumentar que las pruebas han sido descuidadas aquí, dado que el programa es muy simple, puede ser difícil encontrar la motivación y la energía para probarlo lo suficientemente a fondo.


Otro lindo ejemplo es la búsqueda binaria. En TAOCP, Knuth dice que "aunque la idea básica de búsqueda binaria es relativamente sencilla, los detalles pueden ser sorprendentemente difíciles". Aparentemente, un error en la implementación de búsqueda binaria de Java pasó desapercibido durante una década. Fue un error de desbordamiento de enteros, y solo se manifestó con una entrada lo suficientemente grande. Bentley también cubre detalles engañosos de implementaciones de búsqueda binaria en el libro Programming Pearls .

En pocas palabras: puede ser sorprendentemente difícil estar seguro de que un algoritmo de búsqueda binaria es correcto simplemente probándolo.


99
Por supuesto, la falla es bastante evidente desde la fuente (si usted mismo ha escrito algo similar antes).
Raphael

3
Incluso si se corrige la falla simple en el programa de ejemplo, las cadenas dan muchos problemas interesantes. La inversión de cadenas es un clásico: la forma "básica" de hacerlo es simplemente invirtiendo los bytes. Entonces la codificación entra en juego. Luego sustitutos (generalmente dos veces). El problema es, por supuesto, que no hay una manera fácil de demostrar formalmente que su método es correcto.
Ordous

66
Tal vez estoy malinterpretando completamente la pregunta, pero esto parece ser una falla en la implementación en lugar de una falla en el algoritmo en sí.
Mr.Mindor

8
@ Mr.Mindor: ¿cómo puede saber si el programador ha escrito un algoritmo correcto y luego lo implementó incorrectamente, o si escribió un algoritmo incorrecto y luego lo implementó fielmente (dudo en decir "correctamente")
Steve Jessop

1
@wabbit Eso es discutible. Lo que es obvio para usted podría no serlo para un estudiante de primer año.
Juho

30

El mejor ejemplo que he encontrado es la prueba de primalidad:

entrada: número natural p, p! = 2
salida: es pa prime o no?
algoritmo: calcular 2 ** (p-1) mod p. Si resultado = 1, entonces p es primo, de lo contrario p no lo es.

Esto funciona para (casi) todos los números, excepto para unos pocos ejemplos de contadores, y uno realmente necesita una máquina para encontrar un contraejemplo en un período de tiempo realista. El primer contraejemplo es 341, y la densidad de los contraejemplos en realidad disminuye con el aumento de p, aunque casi logarítmicamente.

En lugar de usar solo 2 como la base de la potencia, uno puede mejorar el algoritmo usando también primos pequeños adicionales que aumenten como base en caso de que el primo anterior devuelva 1. Y aún así, hay un contraejemplo de este esquema, a saber, los números de Carmichael, aunque bastante raro


La prueba de primalidad de Fermat es una prueba probabilística, por lo que su condición posterior no es correcta.
Femaref

55
ofc es una prueba probabilística, pero la respuesta muestra muy bien (de manera más general) cómo los algoritmos probabilísticos confundidos con los exactos pueden ser una fuente de error. más sobre los números de Carmichael
vzn

2
Es un buen ejemplo, con una limitación: para el uso práctico de las pruebas de primalidad con las que estoy familiarizado, es decir, la generación de claves criptográficas asimétricas, ¡utilizamos algoritmos probabilísticos! Los números son demasiado grandes para las pruebas exactas (si no lo fueran, entonces no serían adecuados para la criptografía porque las claves podrían encontrarse por la fuerza bruta en tiempo realista).
Gilles

1
la limitación a la que se refiere es práctica, no teórica, y las pruebas principales en sistemas criptográficos, por ejemplo, RSA, están sujetas a fallas raras / altamente improbables por exactamente estas razones, lo que subraya nuevamente la importancia del ejemplo. es decir, en la práctica, a veces esta limitación se acepta como inevitable. existen algoritmos de tiempo P para la prueba de primalidad, por ejemplo, AKS, pero tardan demasiado en utilizar números "más pequeños" en la práctica.
vzn

Si prueba no solo con 2 p, sino con una p para 50 valores aleatorios diferentes 2 ≤ a <p, entonces la mayoría de la gente sabrá que es probabilístico, pero con fallas tan improbables que es más probable que se produzca un mal funcionamiento en su computadora La respuesta equivocada. Con 2 p, 3 p, 5 p y 7 p, las fallas ya son muy raras.
gnasher729

21

Aquí hay uno que me arrojaron los representantes de Google en una convención a la que fui. Fue codificado en C, pero funciona en otros lenguajes que usan referencias. Perdón por tener que codificar en [cs.se], pero es el único que lo ilustra.

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

Este algoritmo funcionará para cualquier valor dado a x e y, incluso si tienen el mismo valor. Sin embargo, no funcionará si se llama swap (x, x). En esa situación, x termina como 0. Ahora, esto podría no satisfacerte, ya que de alguna manera puedes probar que esta operación es matemáticamente correcta, pero aún así olvidarte de este caso límite.


1
Ese truco se usó en el concurso de C descuidado para producir una implementación RC4 defectuosa . Al leer ese artículo nuevamente, me di cuenta de que este truco probablemente fue enviado por @DW
CodesInChaos

77
Esta falla es de hecho sutil, pero la falla es específica del idioma, por lo que no es realmente una falla en el algoritmo; Es una falla en la implementación. Se podrían encontrar otros ejemplos de rarezas del lenguaje que faciliten ocultar defectos sutiles, pero eso no era realmente lo que estaba buscando (estaba buscando algo al nivel de abstracción de algoritmos). En cualquier caso, este defecto no es una demostración ideal del valor de la prueba; a menos que ya esté pensando en aliasing, podría terminar pasando por alto el mismo problema cuando escriba su "prueba" de corrección.
DW

Por eso me sorprende que se haya votado tan alto.
ZeroUltimax

2
@DW Eso es una cuestión de en qué modelo define el algoritmo. Si baja a un nivel donde las referencias de memoria son explícitas (en lugar del modelo común que supone la ausencia de compartir), este es un defecto del algoritmo. La falla no es realmente específica del idioma, aparece en cualquier idioma que permita compartir referencias de memoria.
Gilles

16

Hay toda una clase de algoritmos que es inherentemente difícil de probar: generadores de números pseudoaleatorios . No puede probar una sola salida, pero tiene que investigar (muchas) series de salidas con medios estadísticos. Dependiendo de qué y cómo realice la prueba, puede pasar por alto características no aleatorias.

Un caso famoso donde las cosas salieron terriblemente mal es RANDU . Pasó el escrutinio disponible en ese momento, que no tuvo en cuenta el comportamiento de las tuplas de los resultados posteriores. Los triples ya muestran mucha estructura:

Básicamente, las pruebas no cubrieron todos los casos de uso: si bien el uso unidimensional de RANDU fue (probablemente en su mayoría) correcto, no admitió su uso para muestrear puntos tridimensionales (de esta manera).

El muestreo pseudoaleatorio adecuado es un asunto complicado. Afortunadamente, hay poderosos conjuntos de pruebas en estos días, por ejemplo, dieharder que se especializan en lanzar todas las estadísticas que conocemos en un generador propuesto. ¿Es suficiente?

Para ser justos, no tengo idea de lo que puede probar para los PRNG.


2
Un buen ejemplo, sin embargo, en realidad, en general, no hay forma de demostrar que un PRNG no tiene fallas, solo hay una jerarquía infinita de pruebas más débiles frente a más fuertes. en realidad, probar que uno es "aleatorio" en cualquier sentido estricto es presumiblemente indecidible (aunque todavía no lo he comprobado).
vzn

1
Esa es una buena idea de algo que es difícil de probar, pero los RNG también son difíciles de probar. Los PRNG no son tan propensos a errores de implementación como a ser mal especificados. Las pruebas como diehard son buenas para algunos usos, pero para crypto, puedes pasar diehard y aún así reírte de la sala. No existe un CSPRNG “probado seguro”, lo mejor que puede esperar es demostrar que si su CSPRNG está roto, también lo está AES.
Gilles

@Gilles No estaba tratando de entrar en criptografía, solo aleatoriedad estadística (creo que los dos tienen requisitos bastante ortogonales). ¿Debo aclarar eso en la respuesta?
Raphael

1
La aleatoriedad criptográfica implica aleatoriedad estadística. Sin embargo, ninguno de los dos tiene una definición matemáticamente formal, que yo sepa, aparte de la noción ideal (y contradictoriamente con el concepto de un PRNG implementado en una máquina determinista de Turing) de aleatoriedad teórica de la información. ¿La aleatoriedad estadística tiene una definición formal más allá de "debe ser independiente de las distribuciones con las que la probaremos"?
Gilles

1
@vzn: lo que significa ser una secuencia aleatoria de números se puede definir de muchas maneras posibles, pero una simple es "gran complejidad de Komolgorov". En ese caso, es fácil demostrar que determinar la aleatoriedad es indecidible.
cody

9

Máximo local 2D

entrada: 2 dimensiones matriz Anorte×norteUNA

salida: un máximo local: un par tal que A [ i , j ] no tiene una celda vecina en la matriz que contenga un valor estrictamente mayor. (yo,j)UNA[yo,j]

UNA[yo,j+1],UNA[yo,j-1],UNA[yo-1,j],UNA[yo+1,j]UNA

0 0134 4323125 50 014 40 013

entonces cada celda en negrita es un máximo local. Cada matriz no vacía tiene al menos un máximo local.

O(norte2)

UNAXXUNA(yo,j)X(yo,j)(yo,j)

UNAXUNAX(yo,j)UNA

UNAUNA

(yo,j)UNAUNA(yo,j)

norte2×norte2UNA(yo,j)

T(norte)norte×norteT(norte)=T(norte/ /2)+O(norte)T(norte)=O(norte)

Por lo tanto, hemos demostrado el siguiente teorema:

O(norte)norte×norte

O tenemos?


T(norte)=O(norteIniciar sesiónnorte)T(norte)=T(norte/ /2)+O(norte)

2
Este es un hermoso ejemplo! Me encanta. Gracias. (Finalmente descubrí la falla en este algoritmo. De las marcas de tiempo puede obtener un límite inferior de cuánto tiempo me llevó. Estoy demasiado avergonzado para revelar el tiempo real. :-)
DW

1
O(norte)

8

Estos son ejemplos de primalidad, porque son comunes.

(1) Primalidad en SymPy. Edición 1789 . Hubo una prueba incorrecta en un sitio web conocido que no falló hasta después de 10 ^ 14. Si bien la solución fue correcta, solo estaba reparando agujeros en lugar de repensar el problema.

(2) Primalidad en Perl 6. Perl6 ha añadido is-prime que utiliza una serie de pruebas de RM con bases fijas. Hay contraejemplos conocidos, pero son bastante grandes ya que el número predeterminado de pruebas es enorme (básicamente oculta el problema real al degradar el rendimiento). Esto se abordará pronto.

(3) Primalidad en FLINT. n_isprime () devuelve verdadero para los compuestos , ya que corregido Básicamente el mismo problema que SymPy. Usando la base de datos Feitsma / Galway de pseudoprimos SPRP-2 a 2 ^ 64 ahora podemos probar estos.

(4) Matemáticas de Perl :: Primalidad. is_aks_prime roto . Esta secuencia parece similar a muchas implementaciones de AKS: mucho código que funcionó por accidente (por ejemplo, se perdió en el paso 1 y terminó haciendo todo por división de prueba) o no funcionó para ejemplos más grandes. Lamentablemente, AKS es tan lento que es difícil probarlo.

(5) Pari pre-2.2 es_prime. Matemáticas :: Boleto Pari . Se utilizaron 10 bases aleatorias para las pruebas de MR (con semilla fija en el inicio, en lugar de la semilla fija de GMP en cada llamada). Le dirá que 9 es primo sobre 1 de cada 1 millón de llamadas. Si elige el número correcto, puede hacer que falle con relativa frecuencia, pero los números se vuelven más dispersos, por lo que no aparece mucho en la práctica. Desde entonces han cambiado el algoritmo y la API.

Esto no está mal, pero es un clásico de las pruebas probabilísticas: ¿cuántas rondas das, por ejemplo, mpz_probab_prime_p? Si le damos 5 rondas, seguramente parece que funciona bien: los números tienen que pasar una prueba de Fermat de base 210 y luego 5 pruebas de Miller-Rabin de bases preseleccionadas. No encontrará un contraejemplo hasta 3892757297131 (con GMP 5.0.1 o 6.0.0a), por lo que tendrá que hacer muchas pruebas para encontrarlo. Pero hay miles de contraejemplos bajo 2 ^ 64. Entonces sigues aumentando el número. ¿Cuán lejos? ¿Hay un adversario? ¿Qué tan importante es una respuesta correcta? ¿Estás confundiendo bases aleatorias con bases fijas? ¿Sabes qué tamaños de entrada recibirás?

10dieciséis

Estos son bastante difíciles de probar correctamente. Mi estrategia incluye pruebas unitarias obvias, más casos extremos, más ejemplos de fallas vistas antes o en otros paquetes, prueba frente a bases de datos conocidas donde sea posible (por ejemplo, si realiza una sola prueba de MR de base-2, entonces ha reducido la inviabilidad computacional tarea de probar 2 ^ 64 números para probar unos 32 millones de números), y finalmente, muchas pruebas aleatorias utilizando otro paquete como estándar. El último punto funciona para funciones como primalidad donde hay una entrada bastante simple y una salida conocida, pero algunas tareas son así. He usado esto para encontrar defectos tanto en mi propio código de desarrollo como problemas ocasionales en los paquetes de comparación. Pero dado el espacio de entrada infinito, no podemos probar todo.

En cuanto a probar la corrección, aquí hay otro ejemplo de primalidad. Los métodos BLS75 y ECPP tienen el concepto de un certificado de primalidad. Básicamente, después de realizar búsquedas para encontrar valores que funcionen para sus pruebas, pueden generarlos en un formato conocido. Entonces se puede escribir un verificador o que alguien más lo escriba. Estos se ejecutan muy rápido en comparación con la creación, y ahora (1) ambos fragmentos de código son incorrectos (de ahí que prefiera otros programadores para los verificadores), o (2) la matemática detrás de la idea de la prueba es incorrecta. El n. ° 2 siempre es posible, pero generalmente han sido publicados y revisados ​​por varias personas (y en algunos casos son lo suficientemente fáciles como para que usted lo revise).

En comparación, los métodos como AKS, APR-CL, la división de prueba o la prueba determinista de Rabin, no producen otro resultado que no sea "primo" o "compuesto". En el último caso, podemos tener un factor que puede verificar, pero en el primer caso nos queda nada más que este bit de salida. ¿Funcionó el programa correctamente? No sé.

Es importante probar el software en más que unos pocos ejemplos de juguetes, y también revisar algunos ejemplos en cada paso del algoritmo y decir "dada esta entrada, ¿tiene sentido que esté aquí con este estado?"


1
Muchos de estos se ven como (1) errores de implementación (el algoritmo subyacente es correcto pero no se implementó correctamente), que son interesantes pero no el punto de esta pregunta, o (2) una elección consciente y deliberada para seleccionar algo que es rápido y funciona principalmente, pero puede fallar con una probabilidad muy pequeña (para el código que se está probando con una base aleatoria o algunas bases fijas / aleatorias, espero que quien elija hacer eso sepa que está haciendo una compensación de rendimiento).
DW

Tiene razón en el primer punto: el algoritmo correcto + error no es el punto, aunque la discusión y otros ejemplos también los combinan. El campo está lleno de conjeturas que funcionan para números pequeños pero son incorrectas. Para el punto (2) eso es cierto para algunos, pero mis ejemplos # 1 y # 3 no fueron este caso: se creía que el algoritmo era correcto (estas 5 bases dan resultados probados para números menores de 10 ^ 16), y luego descubrió que no era así.
DanaJ

¿No es este un problema fundamental con las pruebas de pseudoprimidad?
asmeurer

Asmeurer, sí en mi # 2 y la discusión posterior de ellos. Pero el n. ° 1 y el n. ° 3 fueron casos de uso de Miller-Rabin con bases conocidas para dar resultados deterministas correctos por debajo de un umbral. Entonces, en este caso, el "algoritmo" (usando el término libremente para que coincida con el OP) era incorrecto. # 4 no es una prueba principal probable, pero como DW señaló, el algoritmo funciona bien, es solo la implementación lo que es difícil. Lo incluí porque conduce a una situación similar: se necesitan pruebas y, ¿hasta dónde puede llegar más allá de los ejemplos simples antes de decir que funciona?
DanaJ

Algunas de sus publicaciones parecen encajar en la pregunta mientras que otras no (cf. comentario de @ DW) Elimine los ejemplos (y otro contenido) que no respondan la pregunta.
Raphael

7

El algoritmo de barajado de Fisher-Yates-Knuth es un ejemplo (práctico) y sobre el cual uno de los autores de este sitio ha comentado .

El algoritmo genera una permutación aleatoria de una matriz dada como:

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

yoj0 0jyo

Un algoritmo "ingenuo" podría ser:

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

En qué parte del bucle se elige el elemento a intercambiar entre todos los elementos disponibles. Sin embargo, esto produce un muestreo sesgado de las permutaciones (algunas están sobrerrepresentadas, etc.)

En realidad, uno puede inventar la mezcla de pescador-yates-knuth usando un análisis de conteo simple (o ingenuo) .

nortenorte!=norte×norte-1×norte-2..nortenorte-1

El principal problema para verificar si el algoritmo de barajado es correcto o no ( sesgado o no ) es que debido a las estadísticas, se necesita una gran cantidad de muestras. El artículo de codinghorror que enlace arriba explica exactamente eso (y con pruebas reales).


1
Vea aquí un ejemplo de prueba de corrección para un algoritmo aleatorio.
Raphael

5

El mejor ejemplo (léase: algo que me duele más) que he visto tiene que ver con la conjetura de collatz. Estuve en una competencia de programación (con un premio de 500 dólares en la línea por el primer lugar) en el que uno de los problemas era encontrar el número mínimo de pasos necesarios para que dos números alcancen el mismo número. La solución, por supuesto, es dar un paso alternativo a cada uno hasta que ambos alcancen algo que se haya visto antes. Nos dieron un rango de números (creo que estaba entre 1 y 1000000) y nos dijeron que la conjetura de collatz había sido verificada hasta 2 ^ 64, por lo que todos los números que nos dieron eventualmente convergerían en 1. Usé 32 bits enteros para hacer los pasos con sin embargo. Resulta que hay un número oscuro entre 1 y 1000000 (170 mil algo) que hará que un número entero de 32 bits se desborde a su debido tiempo. De hecho, estos números son extremadamente raros debajo de 2 ^ 31. Probamos nuestro sistema en busca de NÚMEROS ENORMES muy superiores a 1000000 para "garantizar" que no se produjera un desbordamiento. Resulta que un número mucho más pequeño que simplemente no probamos causó un desbordamiento. Debido a que usé "int" en lugar de "largo", solo obtuve un premio de 300 dólares en lugar de un premio de $ 500.


5

El problema de Mochila 0/1 es uno que casi todos los estudiantes piensan que es solucionable mediante un algoritmo codicioso. Eso sucede más a menudo si previamente muestra algunas soluciones codiciosas como la versión problemática de la mochila donde funciona un algoritmo codicioso .

Para esos problemas, en clase , debo mostrar la prueba de Knapsack 0/1 ( programación dinámica ) para eliminar cualquier duda y también para la versión del problema codicioso. En realidad, ambas pruebas no son triviales y los estudiantes probablemente las encuentren muy útiles. Además, hay un comentario sobre esto en CLRS 3ed , Capítulo 16, Página 425-427 .

Problema: el ladrón roba una tienda y puede llevar un peso máximo de W en su mochila. Hay n artículos y un artículo pesa wi y vale vi dólares. ¿Qué artículos debe llevar el ladrón? para maximizar su ganancia ?

Problema de mochila 0/1 : la configuración es la misma, pero los elementos no se pueden dividir en pedazos más pequeños , por lo que el ladrón puede decidir tomar un elemento o dejarlo (opción binaria), pero no puede tomar una fracción de un elemento .

Y puede obtener de los estudiantes algunas ideas o algoritmos que siguen la misma idea que el problema de la versión codiciosa, eso es:

  • Tome la capacidad total de la bolsa y coloque tanto como sea posible el objeto de mayor valor, e itere este método hasta que no pueda poner más objeto porque la bolsa está llena o no hay un objeto con menos o igual peso para poner dentro de la bolsa.
  • Otra forma incorrecta es pensar: coloque artículos más livianos y coloque estos siguientes de mayor a menor precio.
  • ...

¿Es útil para ti? En realidad, sabemos que el problema de la moneda es una versión con problemas de mochila. Pero, hay más ejemplos en el bosque de problemas de la mochila, por ejemplo, ¿qué pasa con la mochila 2D (que es realmente útil cuando quieres cortar madera para hacer muebles , vi en un local de mi ciudad), es muy común pensar que el codicioso trabaja aquí también, pero no.


Greedy ya estaba cubierto en la respuesta aceptada , pero el problema de la mochila en particular es muy adecuado para colocar algunas trampas.
Raphael

3

Un error común es implementar mal los algoritmos de barajado. Ver discusión en wikipedia .

norte!nortenorte(norte-1)norte


1
Es un buen error, pero no es una buena ilustración de engañar a los casos de prueba heurísticos, ya que las pruebas realmente no se aplican a algoritmos aleatorios (es aleatorio, entonces, ¿cómo lo probarías? ¿Qué significaría fallar un caso de prueba, y ¿cómo detectarías eso mirando la salida?)
DW

Lo pruebas estadísticamente, por supuesto. La aleatoriedad uniforme está lejos de "cualquier cosa puede suceder en la salida". ¿No sospecharías si un programa que dice emular un dado te diera 100 3 seguidos?
Según Alexandersson el

Nuevamente, estoy hablando de la heurística del estudiante de "probar algunos casos de prueba a mano". He visto a muchos estudiantes pensar que esta es una forma razonable de verificar si un algoritmo determinista es correcto, pero sospecho que no asumirían que es una buena forma de probar si un algoritmo de barajado es correcto (dado que un algoritmo de barajado es aleatorio, hay no hay forma de saber si alguna salida en particular es correcta; en cualquier caso, no puede hacer suficientes ejemplos a mano para hacer una prueba estadística útil). Por lo tanto, no espero que los algoritmos aleatorios ayuden mucho a aclarar el error común.
DW

1
@PerAlexandersson: Incluso si solo genera una mezcla aleatoria, no puede ser realmente aleatorio usando MT con n> 2080. Ahora la desviación de lo esperado será muy pequeña, por lo que probablemente no le importe ... pero esto se aplica incluso si genera mucho menos que el período (como señala un medidor arriba).
Charles

2
¿Esta respuesta parece haber quedado obsoleta por la más elaborada de Nikos M. ?
Raphael

2

Python PEP450 que introdujo funciones estadísticas en la biblioteca estándar podría ser de interés. Como parte de la justificación para tener una función que calcula la varianza en la biblioteca estándar de python, el autor Steven D'Aprano escribe:

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)

Lo anterior parece ser correcto con una prueba casual:

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

Pero agregar una constante a cada punto de datos no debería cambiar la varianza:

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

Y la varianza nunca debe ser negativa:

>>> variance(data*100)
  -1239429440.1282566

El problema es sobre los números y cómo se pierde la precisión. Si desea la máxima precisión, debe ordenar sus operaciones de cierta manera. Una implementación ingenua conduce a resultados incorrectos porque la imprecisión es demasiado grande. Ese fue uno de los temas de los que trataba mi curso de numérica en la universidad.


1
norte-1

2
@Raphael: Aunque para ser justos, el algoritmo elegido es una mala elección para los datos de coma flotante.

2
No se trata simplemente de la implementación de la operación, sino de los números y cómo se pierde la precisión. Si desea la máxima precisión, debe ordenar sus operaciones de cierta manera. Ese fue uno de los temas de los que trataba mi curso de numérica en la universidad.
Christian

Además del comentario exacto de Raphael, una deficiencia de este ejemplo es que no creo que una prueba de corrección ayude a evitar esta falla. Si no conoce las sutilezas de la aritmética de punto flotante, puede pensar que ha demostrado esto correcto (al demostrar que la fórmula es válida). Por lo tanto, no es un ejemplo ideal para enseñar a los estudiantes por qué es importante demostrar que sus algoritmos son correctos. Si los estudiantes vieran este ejemplo, mi sospecha es que en su lugar sacarían la lección "el material de coma flotante / cálculo numérico es complicado".
DW

1

Si bien es probable que esto no sea exactamente lo que buscas, sin duda es fácil de entender y probar algunos casos pequeños sin ningún otro pensamiento conducirá a un algoritmo incorrecto.

nortenorte2+norte+410 0<rere divide norte2+norte+41re<norte2+norte+41

Solución propuesta :

int f(int n) {
   return 1;
}

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

Este enfoque de "probar algunos casos pequeños e inferir un algoritmo a partir del resultado" aparece con frecuencia (aunque no tan extremadamente como aquí) en las competencias de programación donde la presión es crear un algoritmo que (a) se implemente rápidamente y (b ) tiene un tiempo de ejecución rápido.


55
No creo que este sea un muy buen ejemplo, porque pocas personas intentarían encontrar los divisores de un polinomio devolviendo 1.
Brian S

1
nortenorte3-norte

Esto podría ser relevante, en el sentido de que devolver un valor constante para los divisores (u otra acumulación), puede ser el resultado de un enfoque algorítmico incorrecto de un problema (por ejemplo, un problema estadístico, o no manejar casos extremos del algoritmo). Sin embargo, la respuesta debe reformularse
Nikos M.

@NikosM. Je Siento que estoy golpeando a un caballo muerto aquí, pero el segundo párrafo de la pregunta dice que "si su algoritmo funciona correctamente en un puñado de ejemplos, incluidos todos los casos de esquina que pueden pensar intentar, entonces concluyen que el algoritmo debe ser correcto. Siempre hay un estudiante que pregunta: "¿Por qué necesito probar que mi algoritmo es correcto, si puedo probarlo en algunos casos de prueba?" En este caso, para los primeros 40 valores (mucho más de lo que un estudiante es es probable que lo intente), devolver 1 es correcto. Me parece que eso es lo que estaba buscando el OP.
Rick Decker

Ok, sí, pero esto como está redactado es trivial (tal vez típicamente correcto), pero no en el espíritu de la pregunta. Todavía necesitaría una reformulación
Nikos M.
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.