*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
Должен запускаться с -ln
флагами командной строки (следовательно, +4 байта). Принты 0
для составных чисел и 1
простых чисел.
Попробуйте онлайн!
Я думаю, что это первая нетривиальная программа Stack Cats.
объяснение
Краткое введение в Stack Cats:
- Stack Cats работает с бесконечной лентой стеков, а головка ленты направлена на текущий стек. Каждый стек изначально заполнен бесконечным количеством нулей. Обычно я игнорирую эти нули в своей формулировке, поэтому, когда я говорю «дно стека», я имею в виду самое низкое ненулевое значение, а если я говорю «стек пуст», я имею в виду, что на нем только нули.
- Перед запуском программы a
-1
помещается в начальный стек, а затем весь ввод помещается поверх этого. В этом случае из-за -n
флага входные данные читаются как десятичное целое число.
- В конце программы текущий стек используется для вывода. Если
-1
внизу есть значок, он будет проигнорирован. Опять же, из-за -n
флага значения из стека просто печатаются как десятичные целые числа, разделенные переводом строки.
- Stack Cats - это обратимый программный язык: любой фрагмент кода можно отменить (без отслеживания явной истории Stack Cats). Точнее говоря, чтобы перевернуть любой кусок кода, вы просто отражаете его, например,
<<(\-_)
становитесь (_-/)>>
. Эта цель разработки накладывает довольно жесткие ограничения на то, какие операторы и конструкции потоков управления существуют в языке, и какие функции можно вычислять в состоянии глобальной памяти.
В довершение всего, каждая программа Stack Cats должна быть самосимметричной. Вы можете заметить, что это не относится к приведенному выше исходному коду. Вот для чего этот -l
флаг: он неявно отражает код слева, используя первый символ для центра. Следовательно, фактическая программа:
[<(*>=*(:)*[(>*{[[>[:<[>>_(_-<<(-!>)>(>-)):]<^:>!->}<*)*[^:<)*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
Эффективное программирование всего кода крайне нетривиально и не интуитивно понятно, и еще не совсем понятно, как человек может это сделать. Мы грубо форсировали такую программу для более простых задач, но не смогли бы подобраться к ней вручную. К счастью, мы нашли базовый шаблон, который позволяет игнорировать одну половину программы. Хотя это, безусловно, неоптимально, в настоящее время это единственный известный способ эффективного программирования в Stack Cats.
Таким образом, в этом ответе шаблон указанного шаблона выглядит следующим образом (есть некоторые различия в том, как он выполняется):
[<(...)*(...)>]
Когда программа запускается, стековая лента выглядит так (например, для ввода 4
):
4
... -1 ...
0
^
В [
движении верхней части стека влево (и лента головы вместе) - мы называем это «толкая». И <
движется лента в одиночку. Итак, после первых двух команд мы получили следующую ситуацию:
... 4 -1 ...
0 0 0
^
Теперь (...)
это цикл, который можно довольно легко использовать в качестве условного: цикл вводится и остается только тогда, когда вершина текущего стека положительна. Поскольку в настоящее время он равен нулю, мы пропускаем всю первую половину программы. Сейчас центр команды есть *
. Это просто XOR 1
, то есть он переключает младший значащий бит вершины стека, и в этом случае превращает 0
в 1
:
... 1 4 -1 ...
0 0 0
^
Теперь мы сталкиваемся с зеркальным отображением (...)
. На этот раз в верхней части стека является положительным , и мы делаем ввести код. Прежде чем мы рассмотрим, что происходит в скобках, позвольте мне объяснить, как мы будем заключать в конце: мы хотим убедиться, что в конце этого блока у нас снова будет положительное значение на ленте (так, чтобы цикл завершается после одной итерации и используется просто как линейный условный оператор), что стек справа содержит вывод и что стек справа от этого содержит a -1
. Если это так, мы покидаем цикл, >
перемещаемся к выходному значению и ]
помещаем его в -1
так, чтобы у нас был чистый стек для вывода.
Ничего не поделаешь. Теперь внутри скобок мы можем делать все, что захотим, чтобы проверить первичность, при условии, что мы гарантируем, что все настроено, как описано в предыдущем параграфе в конце (что может быть легко сделано с некоторым толчком и движением ленты). Сначала я попытался решить проблему с помощью теоремы Уилсона, но в итоге получилось более 100 байтов, потому что факториальное вычисление в квадрате на самом деле довольно дорого в Stack Cats (по крайней мере, я не нашел короткого пути). Так что вместо этого я пошел с пробным разделением, и это действительно оказалось намного проще. Давайте посмотрим на первый линейный бит:
>:^]
Вы уже видели две из этих команд. Кроме того, :
меняются два верхних значения текущего стека и ^
XORs второе значение в верхнее значение. Это создает :^
общий шаблон для дублирования значения в пустом стеке (мы вытягиваем ноль поверх значения, а затем превращаем ноль в 0 XOR x = x
). Итак, после этого раздел нашей ленты выглядит так:
4
... 1 4 -1 ...
0 0 0
^
Алгоритм пробного деления, который я реализовал, не работает для ввода 1
, поэтому в этом случае мы должны пропустить код. Мы можем легко сопоставить 1
с 0
и всем остальным положительных значений с *
, так вот как мы это делаем:
*(*...)
То есть мы превращаемся 1
в 0
пропускаемую большую часть кода, если мы действительно получим 0
, но внутри мы немедленно отменим, *
чтобы мы вернули наше входное значение. Нам просто нужно еще раз убедиться, что мы заканчиваем положительным значением в конце скобок, чтобы они не начинали цикл. Внутри условного выражения мы перемещаем один стек вправо с помощью >
и затем запускаем основной цикл деления проб:
{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}
Скобки (в отличие от круглых скобок) определяют цикл другого типа: это цикл «do-while», то есть он всегда выполняется по крайней мере для одной итерации. Другое отличие - условие завершения: при входе в цикл Stack Cat запоминает верхнее значение текущего стека ( 0
в нашем случае). Цикл будет выполняться до тех пор, пока это же значение снова не появится в конце итерации. Это удобно для нас: в каждой итерации мы просто вычисляем остаток от следующего потенциального делителя и перемещаем его в этот стек, в котором мы запускаем цикл. Когда мы находим делитель, остаток равен 0
и цикл останавливается. Мы попробуем делители, начиная с, n-1
а затем уменьшим их до 1
. Это означает, что а) мы знаем, что это закончится, когда мы достигнем1
самое позднее и б) мы можем затем определить, является ли число простым или нет, проверяя последний делитель, который мы пробовали (если это 1
простое число, в противном случае это не так).
Давайте доберемся до этого. В начале есть короткий линейный участок:
<-!<:^>[:
Вы знаете, что большинство из этих вещей делают сейчас. Новые команды есть -
и !
. Stack Cats не имеет операторов увеличения или уменьшения. Однако он имеет -
(отрицание, т.е. умножение на -1
) и !
(побитовое НЕ, т.е. умножение на -1
и уменьшение). Они могут быть объединены в приращение !-
или уменьшение -!
. Таким образом, мы уменьшаем копию n
верхней части -1
, затем делаем еще одну копию n
в стеке слева, затем извлекаем новый пробный делитель и помещаем его внизу n
. Итак, на первой итерации мы получаем это:
4
3
... 1 4 -1 ...
0 0 0
^
На последующих итерациях 3
завещание будет заменено следующим делителем теста и т. Д. (Тогда как две копии n
всегда будут иметь одинаковое значение в этой точке).
((-<)<(<!-)>>-_)
Это вычисление по модулю. Поскольку циклы заканчиваются на положительных значениях, идея состоит в том, чтобы начинать -n
и многократно добавлять пробный делитель d
к нему, пока мы не получим положительное значение. Как только мы это сделаем, мы вычтем результат из, d
и это даст нам остаток. Сложность в том, что мы не можем просто поместить -n
вершину стека и запустить цикл, который добавляет d
: если вершина стека отрицательна, цикл не будет введен. Таковы ограничения обратимого языка программирования.
Поэтому, чтобы обойти эту проблему, мы начинаем с n
вершины стека, но отрицаем ее только на первой итерации. Опять же, это звучит проще, чем оказывается ...
(-<)
Когда вершина стека положительна (т. Е. Только на первой итерации), мы ее отрицаем -
. Тем не менее, мы не можем просто сделать, (-)
потому что тогда мы не будем выходить из цикла, пока не -
будет применен дважды. Таким образом, мы перемещаем одну ячейку влево, <
потому что мы знаем, что там есть положительное значение 1
. Хорошо, теперь мы надежно отрицаем n
первую итерацию. Но у нас есть новая проблема: головка ленты на первой итерации теперь находится в другом положении, чем на любой другой. Нам нужно закрепить это, прежде чем мы пойдем дальше. Следующее <
перемещает головку ленты влево. Ситуация на первой итерации:
-4
3
... 1 4 -1 ...
0 0 0 0
^
И на второй итерации (помните, что мы добавили d
один раз -n
сейчас):
-1
3
... 1 4 -1 ...
0 0 0
^
Следующее условие снова объединяет эти пути:
(<!-)
На первой итерации головка ленты указывает на ноль, поэтому это полностью пропускается. На последующих итерациях головка ленты указывает на единицу, поэтому мы выполняем это, перемещаемся влево и увеличиваем там ячейку. Поскольку мы знаем, что ячейка начинается с нуля, теперь она всегда будет положительной, поэтому мы можем выйти из цикла. Это гарантирует, что мы всегда получим два стека слева от основного стека и теперь можем вернуться назад >>
. Затем в конце цикла по модулю мы делаем -_
. Вы уже знаете -
. _
заключается в вычитании , что ^
должен XOR: если вершина стека a
и значение Ниже b
он заменяет a
с b-a
. Так как мы первый отрицается , a
хотя, -_
заменяет a
с b+a
, тем самым добавивd
в наш текущий итог.
После завершения цикла (мы достигли положительного значения) лента выглядит так:
2
3
... 1 1 4 -1 ...
0 0 0 0
^
Самое левое значение может быть любым положительным числом. На самом деле это число итераций минус одна. Теперь есть еще один короткий линейный бит:
_<<]>:]<]]
Как я уже говорил ранее, нам нужно вычесть результат из, d
чтобы получить фактический остаток ( 3-2 = 1 = 4 % 3
), поэтому мы просто сделаем _
еще раз. Далее нам нужно очистить стек, который мы увеличивали слева: когда мы пробуем следующий делитель, он снова должен быть равен нулю, чтобы первая итерация работала. Таким образом, мы перемещаемся туда и помещаем это положительное значение в другой стек помощников, <<]
а затем возвращаемся в наш операционный стек с другим >
. Мы вытягиваем d
с :
и возвращаем его обратно на -1
с, ]
а затем перемещаем остаток в наш условный стек с <]]
. Это конец цикла пробного деления: это продолжается до тех пор, пока мы не получим нулевой остаток, и в этом случае стек слева содержитn
Наибольший делитель (кроме n
).
После того, как цикл заканчивается, мы просто *<
соединяем пути с входом 1
снова. Он *
просто превращает ноль в a 1
, который нам понадобится чуть позже, и затем мы переходим к делителю с помощью <
(так что мы находимся в том же стеке, что и для ввода 1
).
На данный момент это помогает сравнить три различных вида входных данных. Во-первых, особый случай, n = 1
когда мы не делали ничего из того, что делали в пробном подразделении:
0
... 1 1 -1 ...
0 0 0
^
Тогда в нашем предыдущем примере n = 4
составное число:
2
1 2 1
... 1 4 -1 1 ...
0 0 0 0
^
И, наконец, n = 3
простое число:
3
1 1 1
... 1 3 -1 1 ...
0 0 0 0
^
Так что для простых чисел у нас есть 1
в этом стеке, а для составных чисел у нас 0
либо положительное число, либо больше, чем 2
. Мы превращаем эту ситуацию в 0
или, что 1
нам нужно, с помощью следующего финального фрагмента кода:
]*(:)*=<*
]
просто толкает это значение вправо. Затем *
используются для упрощения условной ситуации значительно: переключая значащий бит, мы переходим 1
(прайм) в 0
, 0
(композит) в положительное значение 1
, а все остальные положительные значения по- прежнему будут оставаться положительными. Теперь нам просто нужно различать 0
положительное. Вот где мы используем другое (:)
. Если вершина стека 0
(и вход был простым), это просто пропускается. Но если вершина стека положительна (а входное значение было составным числом), это заменяет его на 1
, так что теперь мы имеем 0
для составного и1
для простых чисел - только два разных значения. Конечно, они противоположны тому, что мы хотим вывести, но это легко исправить с помощью другого *
.
Теперь осталось только восстановить шаблон стеков, ожидаемый нашей окружающей средой: ленточная головка с положительным значением, результат в верхней части стека справа и один -1
в стеке справа от этого . Это для чего =<*
. =
меняет местами вершины двух соседних стеков, тем самым перемещая -1
вправо от результата, например, для ввода 4
снова:
2 0
1 3
... 1 4 1 -1 ...
0 0 0 0 0
^
Затем мы просто двигаемся влево <
и превращаем этот ноль в единицу с *
. И это все.
Если вы хотите глубже изучить работу программы, вы можете воспользоваться опциями отладки. Либо добавьте -d
флаг и вставьте "
туда, где вы хотите видеть текущее состояние памяти, например, как это , или используйте -D
флаг, чтобы получить полный след всей программы . В качестве альтернативы вы можете использовать EsotericIDE от Timwi, который включает в себя интерпретатор Stack Cats с пошаговым отладчиком.