Haskell , 166154 bytes
(-12 bytes gracias a Laikoni, (comprensión de zip y lista en lugar de zipWith y lambda, mejor forma de generar la primera línea))
i#n|let k!p=p:(k+1)![m*l*r+(m*(l*r-l-r)+1)*0^mod k(2^(n-i))|(l,m,r)<-zip3(1:p)p$tail p++[1]];x=1<$[2..2^n]=mapM(putStrLn.map("M "!!))$take(2^n)$1!(x++0:x)
Pruébalo en línea!
Explicación:
La función i#n
dibuja un triángulo de altura ASCII 2^n
después de i
pasos de iteración.
La codificación utilizada internamente codifica posiciones vacías como 1
y posiciones completas como 0
. Por lo tanto, la primera línea del triángulo está codificada como [1,1,1..0..1,1,1]
con 2^n-1
unos en ambos lados del cero. Para construir esta lista, comenzamos con la lista x=1<$[2..2^n]
, es decir, la lista [2..2^n]
con todo lo mapeado 1
. Luego, construimos la lista completa comox++0:x
El operador k!p
(explicación detallada a continuación), dado un índice de línea k
y su correspondiente p
genera una lista infinita de líneas que siguen p
. Lo invocamos con 1
y la línea de inicio descrita anteriormente para obtener el triángulo completo, y luego solo tomamos las primeras 2^n
líneas. Luego, simplemente imprimimos cada línea, reemplazando 1
con espacio y 0
con M
(accediendo a la lista "M "
en la ubicación 0
o 1
).
El operador k!p
se define de la siguiente manera:
k!p=p:(k+1)![m*l*r+(m*(l*r-l-r)+1)*0^mod k(2^(n-i))|(l,m,r)<-zip3(1:p)p$tail p++[1]]
Primero, generamos tres versiones de p
: 1:p
que está p
con un 1
antepuesto, en p
sí mismo y tail p++[1]
que es todo menos el primer elemento de p
, con un 1
adjunto. Luego comprimimos estas tres listas, dándonos efectivamente todos los elementos p
con sus vecinos izquierdo y derecho, como (l,m,r)
. Usamos una lista de comprensión para luego calcular el valor correspondiente en la nueva línea:
m*l*r+(m*(l*r-l-r)+1)*0^mod k(2^(n-i))
Para entender esta expresión, debemos darnos cuenta de que hay dos casos básicos a considerar: o simplemente expandimos la línea anterior o estamos en un punto donde comienza un punto vacío en el triángulo. En el primer caso, tenemos un lugar lleno si alguno de los lugares en el vecindario está lleno. Esto se puede calcular como m*l*r
; si alguno de estos tres es cero, entonces el nuevo valor es cero. El otro caso es un poco más complicado. Aquí, básicamente, necesitamos detección de bordes. La siguiente tabla muestra los ocho vecindarios posibles con el valor resultante en la nueva línea:
000 001 010 011 100 101 110 111
1 1 1 0 1 1 0 1
Una fórmula sencilla para obtener esta tabla sería la 1-m*r*(1-l)-m*l*(1-r)
que se simplifica m*(2*l*r-l-r)+1
. Ahora tenemos que elegir entre estos dos casos, que es donde usamos el número de línea k
. Si mod k (2^(n-i)) == 0
, tenemos que usar el segundo caso, de lo contrario, usamos el primer caso. Por lo 0^(mod k(2^n-i))
tanto, el término es 0
si tenemos que usar el primer caso y 1
si tenemos que usar el segundo caso. Como resultado, podemos usar
m*l*r+(m*(l*r-l-r)+1)*0^mod k(2^(n-i))
en total: si usamos el primer caso, simplemente obtenemos m*l*r
, mientras que en el segundo caso, se agrega un término adicional, dando el total general de m*(2*l*r-l-r)+1
.