2010-05-07 37 views
11

Actualmente estoy trabajando en un problema de proyecto de Euler (www.projecteuler.net) por diversión, pero he topado con un obstáculo. Uno de los problemas proporciona una cuadrícula de números de 20x20 y solicita el mayor producto de 4 números en una línea recta. Esta línea puede ser horizontal, vertical o diagonal.Multiplicando números en líneas horizontales, verticales y diagonales

Utilizando un lenguaje de procedimiento no tendría problemas para resolver esto, pero parte de mi motivación para hacer estos problemas en primer lugar es ganar más experiencia y aprender más Haskell.
En este momento estoy leyendo en la cuadrícula y convirtiéndola en una lista de entradas, por ej. - [[Int]]. Esto hace que la multiplicación horizontal sea trivial, y al transponer esta cuadrícula, la vertical también se vuelve trivial.

La diagonal es lo que me está causando problemas. He pensado en algunas formas en las que podría usar el corte o indexación explícitos de matrices, para obtener una solución, pero parece demasiado complicado y hackey. Creo que probablemente haya una solución elegante y funcional aquí, y me encantaría escuchar lo que otros pueden proponer.

+0

por favor especifique el número de problema de Euler también. algunas personas ya lo han resuelto y pueden desear mirar su propia solución, y tal vez darle una respuesta útil basada en él – yairchu

+0

Es problema # 11 – MtnViewMark

Respuesta

10

No estoy de acuerdo con el estimable Don Stewart. Dada la naturaleza combinatoria del problema y el hecho de que el tamaño del problema es solo de 20x20, las listas de listas serán lo suficientemente rápidas. Y la última lo que desea es utilizar la indexación de matrices. En su lugar, le sugiero que amplíe las técnicas desarrolladas por Richard Bird en su justamente famoso sudoku solver. Para ser más específicos, sugeriría lo siguiente:

  • escribir una función que da una secuencia, devuelve todas las subsecuencias contiguos de longitud 4.

  • escribir una función que da una cuadrícula, se recuperan todos filas

  • Escriba una función que, dada una cuadrícula, devuelve todas las columnas.

  • Escribe una función que, dada una cuadrícula, devuelve todas las diagonales.

Con estas funciones en la mano, su solución será fácil. Pero como mencionas, la diagonal no es tan obvia. ¿Qué es una diagonal de todos modos? Veamos un ejemplo:

X . . . . . 
. X . . . . 
. . X . . . 
. . . X . . 
. . . . X . 
. . . . . X 

Supongamos por un momento que se utiliza la función drop y se le cae 0 elementos de la fila 0, 1 elemento de la fila 1, y así sucesivamente. Esto es lo que terminan con:

X . . . . . 
X . . . . 
X . . . 
X . . 
X . 
X 

Los elementos de la diagonal ahora forman la primera columna de la cosa triangular que le queda.Aún mejor, cada columna de lo que le queda es una diagonal de la matriz original. Agregue algunas transformaciones de simetría y podrá enumerar fácilmente todas las diagonales de una matriz cuadrada de cualquier tamaño. ¡Golpea a cada uno con tu función de "subsecuencias contiguas de longitud 4" y Bob es tu tío!


Un poco más de detalle para los que puede ser atrapado:

La clave de este problema es composición. Las diagonales vienen en cuatro grupos. Mi ejemplo da un grupo. Para obtener los otros tres, aplique la misma función a la imagen reflejada, a la transposición y a la imagen reflejada de la transposición.

  • Transpose es una función de una sola línea, y la necesita de todos modos para recuperar columnas limpiamente.

    Imagen espejo
  • es aún más simple que transponer — pensar en qué funciones puede utilizar desde el preludio.

El método de simetría dará a cada diagonal principal dos veces; Afortunadamente, para el problema indicado, está bien repetir una diagonal.

+0

Estaba pensando en resolver esto durante el fin de semana y tu ejemplo muestra (¿valida?) El enfoque que estaba planeando tomar :) –

+0

"lo último que quieres es cambiar la indexación de matriz" - las bibliotecas de matriz como vector son basado en combinadores, por lo que no es peor que la lista API en facilidad de uso. Pero tomo su punto en 20x20. –

+0

@ Don: Oh, bien. Seguí tu enlace vectorial y me sentí abrumado por la cantidad de deliciosas funciones vectoriales disponibles. –

1

Puede usar la función !! para recuperar elementos en una lista por índice. Que con un paso fijo, ya sea incrementando o disminuyendo el índice obtiene una diagonal.

+0

El acceso aleatorio en las listas es O (n) sin embargo. Por supuesto, eso probablemente no sea un problema para una simple cuadrícula de 20x20. – sepp2k

+0

Esto era a lo que me refería cuando mencioné poder resolverlo usando indexación y división. Tal vez esta es la mejor solución, solo tenía la intuición de que había algo más elegante que podía probar. – untwisted

2

Las listas son la estructura de datos incorrecta para este problema, ya que no proporcionan una indexación aleatoria en tiempo constante: se desvían hacia los cruces lineales. Entonces tus diagonales siempre serán más molestas/lentas con las listas.

¿Qué le parece usar matrices? P.ej. parallel vectors o regular vectors.

+0

Todavía no he mirado demasiado a las estructuras de datos alternativas, aunque ¿me proporcionarían algo en términos de facilidad de implementación o solo eficiencia y velocidad? Por cierto, gracias por el gran libro, es una gran parte de la razón por la que estoy tan avanzado en mi aprendizaje de Haskell como lo soy :) – untwisted

+1

@untwisted: debería ser más fácil implementar su solución usando una matriz escriba, porque puede indexar su matriz como una coordenada tuple (x, y), por lo que si está utilizando la biblioteca vanilla 'Data.Array' de haskell, tendría un tipo de' Array (Int, Int) Int'. Esto también será mucho más rápido a menos que tengas un algoritmo realmente inteligente usando [[Int]]. – jberryman

+0

Gracias por la aclaración, sería mucho más fácil que [[Int]] trabajar con él. – untwisted

2

Bueno, para este problema en particular, ¡una sola lista o matriz lineal es realmente la estructura más fácil! La clave es pensar en estas carreras como omitir la lista con un paso determinado. Si la red es w × h en tamaño, a continuación,

  • una carrera horizontal tiene una zancada de
  • una carrera vertical tiene un paso de w
  • una carrera diagonal tiene un paso de w-1
  • una carrera diagonal tiene un paso de w + 1

Ahora, para cada uno de los cuatro tipos de ejecuciones, solo necesita calcular los posibles puntos de inicio. Algo como esto:

allRuns :: Int -> Int -> Int -> [a] -> [[a]] 
allRuns n w h es = horiz ++ vert ++ acute ++ grave 
    where horiz = runs [0..w-n] [0..h-1] 1 
      vert = runs [0..w-1] [0..h-n] w 
      acute = runs [n-1..w-1] [0..h-n] (w-1) 
      grave = runs [0..w-n] [0..h-n] (w+1) 

      runs xs ys s = [run (x+y*w) s | x <- xs, y <- ys] 
      run i s = map (es!!) [i,i+s..i+(n-1)*s] 

Por supuesto, en una implementación eficiente, que le sustituya el [a] con algo como Data.Array Int a y es!! con es!

+0

La aritmética de índice es FORTRAN, no Haskell. –

+1

De hecho lo es, pero en este caso, el problema * es * esencialmente uno de indización. Todavía tengo que ver una solución que genere todas las ejecuciones, que es tan breve y clara. Creo que, al final, esto expresa cuál es el problema después de forma bastante directa. ¡Y pienso * que * es la esencia de Haskell! – MtnViewMark

1

Así que hay una rejilla de NxN y quiere extraer todo horizontal , líneas verticales y diagonales de longitud M, luego para encontrar el producto máximo. Vamos a ilustrar algunas de las técnicas de Haskell en la parrilla ejemplo 4x4, siendo la longitud de la línea 2:

[[ 1, 2, 3, 4], 
[ 5, 6, 7, 8], 
[ 9,10,11,12], 
[13,14,15,16]] 

horizontal y vertical es fácil, todo lo que necesita es una función que extraer trozos de longitud M de una lista:

chunks 2 [1,2,3,4] == [[1,2],[2,3],[3,4]] 

El tipo de dicha función es [a] -> [[a]].Esta es una función relacionada con la lista, así que antes de reinventar la rueda, veamos si hay algo similar en Data.List. Aha, tails es similar, devuelve las listas con más y más elementos desde el principio de la lista eliminado:

tails [1,2,3,4] == [[1,2,3,4],[2,3,4],[3,4],[4],[]] 

Si tan sólo pudiéramos acortar las listas secundarias para que sean de longitud 2. Pero podemos, mediante el uso de map función, que aplica una función a cada elemento de la lista y devuelve una nueva lista:

map (take n) (tails xs) -- [[1,2],[2,3],[3,4],[4],[]] 

yo no me preocuparía por líneas más pequeñas, como la tarea original es para encontrar el producto más grande, y el producto de [15, N] ≥ producto de [15], N ≥ 1. Pero si desea deshacerse de ellos, parece th en una lista de longitud N contiene N-M + 1 trozos de longitud M, por lo que podría aplicar take (4-2+1) a la lista resultante. Alternativamente, usted podría simplemente filter la lista:

chunks n xs = filter ((==n) . length) $ map (take n) (tails xs) 
-- [[1,2],[2,3],[3,4]] 

Ok, podemos extraer una lista de trozos de una lista, pero tenemos una en 2D de la red, no una lista plana! map nos rescata de nuevo:

map (chunks 2) grid -- [[[1,2],[2,3],[3,4]],[[5,6],[6,7],[7,8]],...] 

Pero aquí es la cosa, el código resultante pone trozos en listas separadas, y que complica las cosas, ya que en realidad no importa, a partir del cual se origina la línea del trozo. Así que nos gustaría para aplanar un nivel la lista resultante por concat . map o equivalente concatMap:

concatMap (chunks 2) grid -- [[1,2],[2,3],[3,4],[5,6],[6,7],[7,8],...] 

Ahora, ¿cómo consigo trozos verticales de una red? Suena de miedo al principio, hasta que se da cuenta de que puede transpose toda la red, es decir, a su vez filas en columnas y columnas en filas, y luego aplicar el mismo código:

concatMap (chunks 2) (transpose grid) -- [[1,5],[5,9],[9,13],[2,6],[6,10],...] 

Ahora la parte difícil: las líneas diagonales. Norman Ramsey da una idea: ¿y si pudiera eliminar 0 elementos de la línea 0, 1 elementos de la línea 1, etc.? La línea diagonal se convertiría en una línea vertical, que es fácil de extraer. Recuerde que para aplicar una función a cada elemento de una lista utiliza map, pero aquí debe aplicar diferentes funciones a cada elemento, a saber, drop 0, drop 1, drop 2, etc. map no sería adecuado. Pero mire, el primer argumento para drop forma un patrón de números sucesivos, que pueden representarse como una lista infinita [0..]. Ahora, ¿qué pasaría si pudiéramos tomar un elemento de [0..] Lo que necesitamos es una función que toma un número de una lista infinita [0..] y una fila de la cuadrícula, y aplica drop con este número a la fila. zipWith es lo que necesita:

zipWith drop [0..] grid -- [[1,2,3,4],[6,7,8],[11,12],[16]] 
map head $ zipWith drop [0..] grid -- [1,6,11,16] 

Pero quiero que todos diagonales de longitud 2, no sólo la mayor diagonal. Así que mira la cuadrícula y piensa, ¿qué líneas diagonales ves con elementos en la fila 0? [1,6],[2,7],[3,8]. Así que está claro que es necesario tomar sólo 2 primeras filas y la transposición elementos:

transpose $ zipWith drop [0,1] grid -- [[1,6],[2,7],[3,8],[4]] 

ahora cómo consigo diagonales a partir de otras filas, así? ¿Recuerdas nuestro truco tails?Podemos obtener todas las diagonales proporcionando nuestra nueva función a un concatMap y aplicándolo a tails grid:

concatMap (transpose . zipWith drop [0,1]) (tails g) 
-- [[1,6],[2,7],[3,8],[5,10],[6,11],...] 

Pero estos son sólo diagonales que van desde la parte superior izquierda a la inferior derecha. ¿Y los que van de arriba a la derecha a abajo a la izquierda? Es más fácil sólo para revertir las filas de la cuadrícula:

concatMap (transpose . zipWith drop [0,1]) (tails $ reverse g) 
-- [[13,10],[14,11],[15,12],[9,6],[10,7],...] 

Por último, es necesario encontrar productos de todas las líneas y elegir el más grande. El código final se ve así:

grid = [[1..4],[5..8],[9..12],[13..16]] 
chunks n xs = map (take n) (tails xs) 
horizontal = concatMap (chunks 2) grid 
vertical = concatMap (chunks 2) (transpose grid) 
grave = concatMap (transpose . zipWith drop [0,1]) (tails grid) 
acute = concatMap (transpose . zipWith drop [0,1]) (tails $ reverse grid) 
maxProduct = maximum $ map product $ horizontal ++ vertical ++ grave ++ acute 
-- answer: 240 

¿Este código es al máximo elegante y eficiente? Diablos, no, pero funciona e ilustra ciertos patrones de pensamiento de la programación funcional. Al principio, necesita escribir un código que simplemente funcione, luego refactorizarlo iterativamente, hasta que llegue a una solución que sea fácil de leer y general.

Cuestiones relacionadas