Haskell , 166 154 байта
(-12 байт, благодаря Laikoni, (понимание zip и списка вместо zipWith и lambda, лучший способ генерации первой строки))
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)
Попробуйте онлайн!
Объяснение:
Функция i#n
рисует ASCII-треугольник высоты 2^n
после i
шагов итерации.
Используемое внутреннее кодирование кодирует пустые позиции как 1
и полные позиции как 0
. Поэтому первая линия треугольника закодирована так же, как и [1,1,1..0..1,1,1]
с 2^n-1
обеих сторон от нуля. Чтобы построить этот список, мы начнем со списка x=1<$[2..2^n]
, то есть со списком, [2..2^n]
на который все сопоставлено 1
. Затем мы строим полный список какx++0:x
Оператор k!p
(подробное объяснение ниже), учитывая индекс строки k
и соответствующий ей, p
генерирует бесконечный список следующих строк p
. Мы вызываем его с 1
помощью стартовой линии, описанной выше, чтобы получить весь треугольник, а затем только первые 2^n
строки. Затем мы просто печатаем каждую строку, заменяя ее 1
пробелом и 0
на M
(путем доступа к списку "M "
в местоположении 0
или 1
).
Оператор k!p
определяется следующим образом:
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]]
Во- первых, мы создаем три версии p
: 1:p
что является p
с 1
префиксом, p
себя и tail p++[1]
что все , кроме первого элемента p
, с 1
прилагается. Затем мы упаковываем эти три списка, давая нам эффективно все элементы p
со своими левыми и правыми соседями, как (l,m,r)
. Мы используем понимание списка, чтобы затем вычислить соответствующее значение в новой строке:
m*l*r+(m*(l*r-l-r)+1)*0^mod k(2^(n-i))
Чтобы понять это выражение, нам нужно понять, что нужно рассмотреть два основных случая: либо мы просто расширяем предыдущую строку, либо мы находимся в точке, где начинается пустое место в треугольнике. В первом случае мы имеем заполненное пятно, если любое из соседних пятен заполнено. Это можно рассчитать как m*l*r
; если любой из этих трех равен нулю, то новое значение равно нулю. Другой случай немного сложнее. Здесь нам в основном нужно обнаружение краев. В следующей таблице приведены восемь возможных окрестностей с результирующим значением в новой строке:
000 001 010 011 100 101 110 111
1 1 1 0 1 1 0 1
Простая формула для получения этой таблицы будет 1-m*r*(1-l)-m*l*(1-r)
упрощена до m*(2*l*r-l-r)+1
. Теперь нам нужно выбрать между этими двумя случаями, где мы используем номер строки k
. Если mod k (2^(n-i)) == 0
мы должны использовать второй случай, в противном случае мы используем первый случай. Таким 0^(mod k(2^n-i))
образом, этот термин означает, 0
что мы должны использовать первый случай и 1
если мы должны использовать второй случай. В результате мы можем использовать
m*l*r+(m*(l*r-l-r)+1)*0^mod k(2^(n-i))
в общем - если мы используем первый случай, мы просто получаем m*l*r
, а во втором случае добавляется дополнительный термин, давая общую сумму m*(2*l*r-l-r)+1
.