Este desafío no se trata del juego Snake.
Imagine una serpiente 2D formada dibujando una línea horizontal de longitud n
. En los puntos enteros a lo largo de su cuerpo, esta serpiente puede girar su cuerpo en 90 grados. Si definimos que el frente de la serpiente esté en el extremo izquierdo para comenzar, la rotación moverá la parte posterior de la serpiente y la parte delantera se mantendrá en su lugar. Al hacer rotaciones repetidas, puede hacer muchas formas diferentes de cuerpo de serpiente.
Reglas
- Una parte del cuerpo de la serpiente no puede solaparse con otra.
- Tiene que ser posible alcanzar la orientación final sin que ninguna parte del cuerpo de la serpiente se superponga en el medio. Dos puntos que se tocan se cuentan como superpuestos en este problema.
- Considero que una serpiente y su reverso tienen la misma forma.
Tarea
Hasta la rotación, la traslación y la simetría de espejo, ¿cuál es el número total de diferentes formas de cuerpo de serpiente que se pueden hacer?
Ejemplo de rotación de parte del cuerpo de la serpiente. Imagínese n=10
y la serpiente está en su orientación inicial de una línea recta. Ahora gire en el punto 4
90 grados en sentido antihorario. Obtenemos la serpiente de 4
a 10
(la cola de la serpiente) que se encuentra en posición vertical y la serpiente de 0
a 4
posición horizontal. La serpiente ahora tiene un ángulo recto en su cuerpo.
Aquí hay algunos ejemplos gracias a Martin Büttner.
Comenzamos con la serpiente horizontal.
Ahora giramos desde la posición 4.
Terminamos después de la rotación en esta orientación.
Ahora consideremos esta orientación de una serpiente diferente.
Ahora podemos ver un movimiento ilegal donde habría una superposición causada durante la rotación.
Puntuación
Su puntaje es el mayor n
para el cual su código puede resolver el problema en menos de un minuto en mi computadora.
Cuando ocurre una rotación, moverá la mitad de la serpiente con ella. Tenemos que preocuparnos si alguna de esta parte que se rota podría superponerse a una parte de la serpiente durante la rotación. Por simplicidad, podemos suponer que la serpiente tiene un ancho cero. Solo puede girar en un punto particular de la serpiente hasta 90 grados en sentido horario o antihorario. Porque, nunca puedes doblar por completo la serpiente en dos, ya que eso habría implicado dos rotaciones en el mismo punto en la misma dirección.
Formas que no se pueden hacer
Un ejemplo simple de una forma que no se puede hacer es una capital T
. Una versión más sofisticada es
(Gracias a Harald Hanche-Olsen por este ejemplo)
En este ejemplo, todas las líneas horizontales adyacentes están separadas 1 como las verticales. Por lo tanto, no existe un movimiento legal desde esta posición y, dado que el problema es reversible, no hay forma de llegar desde la posición inicial.
Idiomas y bibliotecas
Puede usar cualquier idioma que tenga un compilador / intérprete / etc. para Linux y cualquier biblioteca que también esté disponible gratuitamente para Linux.
Mi máquina Los tiempos se ejecutarán en mi máquina. Esta es una instalación estándar de ubuntu en un procesador AMD FX-8350 de ocho núcleos. Esto también significa que necesito poder ejecutar su código. Como consecuencia, solo use software gratuito fácilmente disponible e incluya instrucciones completas sobre cómo compilar y ejecutar su código.
Respuestas:
Python 3 - puntaje provisional: n = 11 (n = 13 con PyPy *)
Como no hubo respuestas en la primera semana, aquí hay un ejemplo en Python para alentar la competencia. He intentado que sea razonablemente legible para que las ineficiencias puedan identificarse fácilmente y dar ideas para otras respuestas.
Acercarse
Código
(ahora con algunos doctests y afirma después de mi primer intento incorrecto)
Resultados
En mi máquina, la serpiente más larga que se puede calcular en menos de 1 minuto es la longitud 11 (o la longitud 13 con PyPy *). Obviamente, esto es solo un puntaje provisional hasta que descubramos cuál es el puntaje oficial de la máquina de Lembik.
A modo de comparación, aquí están los resultados para los primeros valores de n:
Avíseme si alguno de estos resulta ser incorrecto.
Si tiene un ejemplo de un arreglo que no debería poder desplegarse, puede usar la función
neighbours(snake)
para encontrar cualquier arreglo accesible en un solo paso, como prueba del código.snake
es una tupla de direcciones conjuntas (0 para sentido horario, 1 para derecho, 2 para sentido antihorario). Por ejemplo (1,1,1) es una serpiente recta de longitud 4 (con 3 articulaciones).Visualización
Para visualizar una serpiente que tiene en mente, o cualquiera de las serpientes devueltas
neighbours
, puede usar la funcióndisplay(snake)
. Esto acepta una tupla como las otras funciones, pero como no es utilizado por el programa principal y, por lo tanto, no necesita ser rápido, también aceptará una cadena, para su conveniencia.display((1,1,2,0))
es equivalente adisplay("1120")
Como Lembik menciona en los comentarios, mis resultados son idénticos a OEIS A037245 que no tiene en cuenta las intersecciones durante la rotación. Esto se debe a que para los primeros valores de n no hay diferencia: todas las formas que no se intersectan a sí mismas se pueden alcanzar doblando una serpiente recta. La exactitud del código se puede probar llamando
neighbours()
a una serpiente que no se puede desplegar sin intersección. Como no tiene vecinos, solo contribuirá a la secuencia OEIS, y no a esta. El ejemplo más pequeño que conozco es esta serpiente de longitud 31 que Lembik mencionó, gracias a David K :(1,1,1,1,2,1,2,1,1,1,1,1,1,2,1,1,1,2,1,1,2,2,1,0,1,0,1,1,1,1)
display('111121211111121112112210101111')
da el siguiente resultado:Me encantaría saber de cualquiera que tenga un ejemplo más corto sin vecinos. Sospecho que el ejemplo más corto de este tipo marcará el n más pequeño para el que difieren las dos secuencias.
* Tenga en cuenta que cada incremento de n tarda aproximadamente 3 veces más, por lo que aumentar de n = 11 a n = 13 requiere casi 10 veces el tiempo. Es por eso que PyPy solo permite aumentar n en 2, a pesar de que se ejecuta considerablemente más rápido que el intérprete estándar de Python.
fuente
C ++ 11 - casi funciona :)
Después de leer este artículo , obtuve un poco de sabiduría de ese tipo que aparentemente trabajó durante 25 años en el problema menos complicado de contar caminos que se evitan en una red cuadrada.
Construyendo el ejecutable
Compile con Yo uso MinGW bajo Win7 con g ++ 4.8 para compilaciones "linux", por lo que la portabilidad no está 100% garantizada.
g++ -O3 -std=c++11
También funciona (más o menos) con un proyecto estándar MSVC2013
Al no definir
NDEBUG
, obtiene rastros de la ejecución del algoritmo y un resumen de las configuraciones encontradas.Actuaciones
con o sin tablas hash, el compilador de Microsoft funciona miserablemente: la compilación de g ++ es 3 veces más rápida .
El algoritmo prácticamente no usa memoria.
Dado que la verificación de colisión es aproximadamente en O (n), el tiempo de cálculo debe estar en O (nk n ), con k ligeramente inferior a 3.
En mi [email protected], n = 17 toma aproximadamente 1:30 (aproximadamente 2 millones serpientes / minuto).
No he terminado de optimizar, pero no esperaría más que una ganancia x3, así que básicamente puedo esperar alcanzar quizás n = 20 en menos de una hora, o n = 24 en menos de un día.
Alcanzar la primera forma imposible de doblar conocida (n = 31) tomaría entre unos años y una década, suponiendo que no haya cortes de energía.
Contando formas
Una serpiente de tamaño N tiene articulaciones N-1 .
Cada articulación puede dejarse recta o doblada hacia la izquierda o hacia la derecha (3 posibilidades).
El número de plegamientos posibles es, por lo tanto, 3 N-1 .
Las colisiones reducirán un poco ese número, por lo que el número real es cercano a 2.7 N-1
Sin embargo, muchos de estos pliegues conducen a formas idénticas.
dos formas son idénticas si hay una rotación o una simetría que puede transformar una en la otra.
Definamos un segmento como cualquier parte recta del cuerpo plegado.
Por ejemplo, una serpiente de tamaño 5 doblada en la segunda articulación tendría 2 segmentos (uno de 2 unidades de largo y el segundo de 3 unidades de largo).
El primer segmento se llamará cabeza y la última cola .
Por convención, orientamos la cabeza de las serpientes horizontalmente con el cuerpo apuntando hacia la derecha (como en la primera figura del OP).
Designamos una figura dada con una lista de longitudes de segmento con signo, con longitudes positivas que indican un plegado a la derecha y negativas a un plegado a la izquierda.
La longitud inicial es positiva por convención.
Separar segmentos y curvas
Si consideramos solo las diferentes formas en que una serpiente de longitud N puede dividirse en segmentos, terminamos con un reparto idéntico a las composiciones de N.
Usando el mismo algoritmo que se muestra en la página wiki, es fácil generar todas las 2 particiones N-1 posibles de la serpiente.
Cada partición a su vez generará todos los plegamientos posibles aplicando curvas a la izquierda o derecha a todas sus uniones. Uno de estos plegamientos se llamará configuración .
Todas las particiones posibles se pueden representar mediante un número entero de N-1 bits, donde cada bit representa la presencia de una unión. Llamaremos a este entero un generador .
Particiones de poda
Al notar que doblar una partición dada desde la cabeza hacia abajo es equivalente a doblar la partición simétrica desde la cola hacia arriba, podemos encontrar todas las parejas de particiones simétricas y eliminar una de cada dos.
El generador de una partición simétrica es el generador de la partición escrito en orden de bits inverso, que es trivialmente fácil y barato de detectar.
Esto eliminará casi la mitad de las particiones posibles, con la excepción de las particiones con generadores "palindrómicos" que no se modifican por la inversión de bits (por ejemplo, 00100100).
Cuidando las simetrías horizontales
Con nuestras convenciones (una serpiente comienza a apuntar a la derecha), la primera curva aplicada a la derecha producirá una familia de pliegues que serán simétricos horizontales de los que difieren solo en la primera curva.
Si decidimos que la primera curva siempre estará a la derecha, eliminaremos todas las simétricas horizontales de una sola vez.
Limpiando los palíndromos
Estos dos cortes son eficientes, pero no suficientes para cuidar estos molestos palíndromos.
La verificación más exhaustiva en el caso general es la siguiente:
considere una configuración C con una partición palindrómica.
Podríamos verificar cada nueva configuración contra las otras 3. Sin embargo, dado que ya generamos solo configuraciones que comienzan con un giro a la derecha, solo tenemos una simetría posible para verificar:
Esa es la única configuración que posiblemente podemos duplicar.
Eliminando duplicados sin almacenamiento
Mi enfoque inicial fue almacenar todas las configuraciones en una gran tabla hash, para eliminar duplicados al verificar la presencia de una configuración simétrica previamente calculada.
Gracias al artículo mencionado anteriormente, quedó claro que, dado que las particiones y los pliegues se almacenan como campos de bits, se pueden comparar como cualquier valor numérico.
Entonces, para eliminar un miembro de un par simétrico, simplemente puede comparar ambos elementos y mantener sistemáticamente el más pequeño (o el más grande, como desee).
Por lo tanto, probar una configuración para duplicación equivale a calcular la partición simétrica, y si ambas son idénticas, el plegamiento. No se necesita memoria en absoluto.
Orden de generación
Claramente, la verificación de colisión será la parte que más tiempo requiera, por lo que reducir estos cálculos es un gran ahorro de tiempo.
Una posible solución es tener una "serpiente de muñeca de trapo" que comenzará en una configuración plana y se doblará gradualmente, para evitar volver a calcular toda la geometría de la serpiente para cada configuración posible.
Al elegir el orden en el que se prueban las configuraciones, de modo que como máximo se almacene una muñeca de trapo para cada número total de uniones, podemos limitar el número de instancias a N-1.
Utilizo una exploración recursiva del sake desde la cola hacia abajo, agregando una sola articulación en cada nivel. Por lo tanto, se crea una nueva instancia de ragdoll sobre la configuración principal, con una sola curva adicional.
Esto significa que las curvas se aplican en un orden secuencial, lo que parece ser suficiente para evitar auto colisiones en casi todos los casos.
Cuando se detecta la auto-colisión, las curvas que conducen al movimiento ofensivo se aplican en todas las órdenes posibles hasta que se encuentre un plegado legítimo o se agoten todas las combinaciones.
Control estático
Antes incluso de pensar en las partes móviles, me pareció más eficiente probar la forma final estática de una serpiente para autointercepciones.
Esto se hace dibujando la serpiente en una cuadrícula. Cada punto posible se traza desde la cabeza hacia abajo. Si hay una auto-intersección, al menos un par de puntos caerán en la misma ubicación. Esto requiere exactamente N parcelas para cualquier configuración de serpiente, durante un tiempo constante de O (N).
La principal ventaja de este enfoque es que la prueba estática por sí sola simplemente seleccionará rutas válidas de auto evitación en una red cuadrada, lo que permite probar todo el algoritmo al inhibir la detección de colisión dinámica y asegurarse de que encontremos el recuento correcto de tales rutas.
Control dinámico
Cuando una serpiente se pliega alrededor de una articulación, cada segmento girado barrerá un área cuya forma es cualquier cosa menos trivial.
Claramente, puede verificar las colisiones probando la inclusión en todas las áreas barridas individualmente. Una verificación global sería más eficiente, pero dada la complejidad de las áreas no puedo pensar en ninguna (excepto tal vez usando una GPU para dibujar todas las áreas y realizar una verificación de impacto global).
Dado que la prueba estática se ocupa de las posiciones inicial y final de cada segmento, solo necesitamos verificar las intersecciones con los arcos barridos por cada segmento giratorio.
Después de una interesante discusión con trichoplax y un poco de JavaScript para orientarme, se me ocurrió este método:
Para tratar de ponerlo en pocas palabras, si llamas
(fuente: free.fr )
Para cualquier segmento que no contenga I , el área barrida está unida por 2 arcos (y 2 segmentos ya atendidos por la verificación estática).
Si I cae dentro del segmento, el arco barrido por I también debe tenerse en cuenta.
Esto significa que podemos verificar cada segmento inmóvil contra cada segmento giratorio con 2 o 3 intersecciones de segmento con arco
Usé geometría vectorial para evitar las funciones trigonométricas por completo.
Las operaciones vectoriales producen código compacto y (relativamente) legible.
La intersección de segmento a arco requiere un vector de punto flotante, pero la lógica debe ser inmune a los errores de redondeo.
Encontré esta solución elegante y eficiente en una oscura publicación del foro. Me pregunto por qué no se publicita más ampliamente.
¿Funciona?
La inhibición de la detección dinámica de colisión produce el recuento correcto de rutas de autoevaluación hasta n = 19, por lo que estoy bastante seguro de que el diseño global funciona.
La detección dinámica de colisión produce resultados consistentes, aunque falta la comprobación de curvas en diferente orden (por ahora).
Como consecuencia, el programa cuenta las serpientes que pueden doblarse de la cabeza hacia abajo (es decir, con las articulaciones plegadas en orden de distancia creciente de la cabeza).
fuente