堆栈猫,62 + 4 = 66字节
*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
需要使用-ln
命令行标志(因此+4个字节)运行。打印0
复合数字和质1
数。
在线尝试!
我认为这是第一个非同寻常的Stack Cats程序。
说明
快速的Stack Cats简介:
- Stack Cats在无限的堆栈带上操作,带头指向当前堆栈。每个堆栈最初都填充有无限数量的零。我通常会在用词时忽略这些零,所以当我说“堆栈底部”时,我的意思是最低的非零值;如果我说“堆栈为空”,则意味着它上只有零。
- 在程序启动之前,将a
-1
压入初始堆栈,然后将整个输入压入其顶部。在这种情况下,由于该-n
标志,输入被读取为十进制整数。
- 在程序末尾,当前堆栈用于输出。如果
-1
底部有一个,它将被忽略。同样,由于该-n
标志,堆栈中的值仅以换行分隔的十进制整数形式打印。
- Stack Cats是一种可逆的程序语言:每段代码都可以撤消(没有Stack Cats跟踪明确的历史记录)。更具体地说,要反转任何一段代码,您只需对其进行镜像,例如
<<(\-_)
变成(_-/)>>
。这个设计目标对语言中存在哪些类型的运算符和控制流构造以及可以在全局内存状态上计算哪些类型的功能施加了相当严格的限制。
最重要的是,每个Stack Cats程序都必须是自对称的。您可能会注意到,上述源代码并非如此。这就是-l
标志的作用:使用第一个字符为中心,将代码隐式镜像到左侧。因此,实际程序是:
[<(*>=*(:)*[(>*{[[>[:<[>>_(_-<<(-!>)>(>-)):]<^:>!->}<*)*[^:<)*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
用整个代码有效地编程是非常不平凡和不直观的,并且还没有真正弄清楚人类如何做到这一点。我们已经强行将这样的程序强制用于更简单的任务,但是无法手工接近该程序。幸运的是,我们找到了一种基本模式,可让您忽略程序的一半。尽管这肯定不是最佳选择,但它是当前在Stack Cats中有效编程的唯一已知方法。
因此,在此答案中,所述模式的模板是这样的(执行方式存在一些可变性):
[<(...)*(...)>]
程序启动时,堆栈磁带如下所示(对于input 4
,例如):
4
... -1 ...
0
^
将[
堆栈的顶部向左移动(和磁带头一起)-我们称之为“推动”。然后<
仅移动磁带头。因此,在执行前两个命令后,我们遇到了这种情况:
... 4 -1 ...
0 0 0
^
现在,这(...)
是一个可以很容易用作条件的循环:只有当当前堆栈的顶部为正数时,才进入和离开该循环。由于当前为零,因此我们跳过了程序的整个前半部分。现在,中心命令为*
。这很简单XOR 1
,即,它切换堆栈顶部的最低有效位,在这种情况下,将0
变为1
:
... 1 4 -1 ...
0 0 0
^
现在我们遇到了的镜像(...)
。这一次,堆栈的顶部是积极的,我们做的输入代码。在研究括号内的内容之前,让我解释一下如何结束:我们要确保在此块的末尾,我们将磁带头重新设置为正值(这样,循环单次迭代之后终止并且被简单地用作线性有条件的),该堆栈向右保持输出和堆权利该保持-1
。在这种情况下,我们确实离开了循环,>
移至输出值]
并将其推入,-1
因此我们有一个干净的输出栈。
就是这样。现在,在括号内,我们可以执行任何我们想检查原始性的操作,只要确保按末段中的最后一段所述进行设置即可(可以通过推动和移动磁头轻松地完成此操作)。我首先尝试用威尔逊定理解决问题,但最终超过了100个字节,因为平方因数计算实际上在Stack Cats中非常昂贵(至少我没有找到一个捷径)。因此,我改为使用审判庭,事实证明这要简单得多。让我们看一下第一个线性位:
>:^]
您已经看过其中两个命令。此外,:
交换当前堆栈的前两个值,^
并对第二个值执行XOR运算,将其转换为前一个值。这形成:^
了一种常见的模式,可以在空堆栈上复制一个值(我们在该值的顶部拉一个零,然后将其变成0 XOR x = x
)。因此,在此之后,我们的磁带部分如下所示:
4
... 1 4 -1 ...
0 0 0
^
我实现的试验分割算法不适用于input 1
,因此在这种情况下,我们应该跳过代码。我们可以轻松地将映射1
到0
正值,而将其他所有值都映射到正值*
,因此我们可以这样做:
*(*...)
那就是我们1
变成0
,如果确实得到了,则跳过大部分代码0
,但是在内部,我们立即撤消了,*
以便我们取回输入值。我们只需要再次确保在括号的结尾处以正值结尾,以免它们开始循环。在条件内,我们向右移动一个堆栈>
,然后开始主试验除法循环:
{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}
花括号(而不是括号)定义了另一种循环:这是一个do-while循环,这意味着它始终至少运行一次迭代。另一个区别是终止条件:进入循环时,Stack Cat会记住当前堆栈的最高值(0
在我们的示例中)。然后循环将运行,直到在迭代结束时再次看到相同的值为止。这对我们来说很方便:在每次迭代中,我们只需计算下一个潜在除数的余数,然后将其移到该堆栈上,就可以开始循环了。当我们找到一个除数时,余数为0
,循环停止。我们将尝试从开始的除数n-1
,然后将其递减到1
。这意味着a)我们知道这将在我们到达时终止1
b)我们可以通过检查我们尝试的最后一个除数来确定数字是否为质数(如果为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
with 向上拉,:
然后将其推回到-1
with上]
,然后使用将剩余部分移到条件堆栈上<]]
。这就是试验除法循环的结束:一直持续到我们剩下零为止,在这种情况下,左侧的堆栈包含n
最大除数为(n
)。
循环结束之后,就*<
在我们1
再次将路径与输入连接之前。该*
只是将零到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
(prime)变为0
,将0
(composite)变为正值1
,而所有其他正值仍将保持正值。现在我们只需要区分0
和肯定。那是我们使用另一个的地方(:)
。如果堆栈的顶部是0
(并且输入是素数),则只需跳过。但是,如果堆栈的顶部为正(并且输入是一个复合数字),则会将其与交换1
,因此我们现在可以0
使用复合和1
对于素数-只有两个不同的值。当然,它们与我们要输出的内容相反,但这很容易用另一个来解决*
。
现在,所有剩下的就是恢复预计我们周围的架构堆栈的模式:带头为正值的,导致堆栈到右侧的顶部,和一个-1
上的堆栈权说。这=<*
是为了什么。=
交换两个相邻堆栈的顶部,从而将移到-1
结果的右侧,例如4
再次输入:
2 0
1 3
... 1 4 1 -1 ...
0 0 0 0 0
^
然后,我们只向左移动,<
然后将变成零*
。就是这样。
如果您想深入了解程序的工作方式,则可以使用调试选项。添加该-d
标志并"
在您想要查看当前内存状态的任何位置插入(例如,像这样),或者使用该-D
标志获得整个程序的完整跟踪。另外,您可以使用Timwi的EsotericIDE,其中包括带有逐步调试器的Stack Cats解释器。