GNU Prolog,98个字节
b(x,0,x).
b(T/H,N,H):-N#=A+B+1,b(H,A,_),b(T,B,J),H@>=J.
c(X,Y):-findall(A,b(A,X,_),L),length(L,Y).
这个答案是一个很好的例子,说明了Prolog如何甚至可以使用最简单的I / O格式。它通过描述问题而不是解决问题的算法以真正的Prolog风格工作:它指定了什么才算是合法的泡沫安排,要求Prolog生成所有这些泡沫安排,然后对它们进行计数。生成需要55个字符(程序的前两行)。计数和I / O采用其他43(第三行,以及将这两部分分开的换行)。我敢打赌,OP不会导致语言与I / O纠缠,这不是问题!(注意:Stack Exchange的语法突出显示使它更难阅读,而不是更容易,因此我将其关闭了)。
说明
让我们从一个实际上不起作用的类似程序的伪代码版本开始:
b(Bubbles,Count) if map(b,Bubbles,BubbleCounts)
and sum(BubbleCounts,InteriorCount)
and Count is InteriorCount + 1
and is_sorted(Bubbles).
c(Count,NPossibilities) if listof(Bubbles,b(Bubbles,Count),List)
and length(List,NPossibilities).
应该很清楚地知道它是如何b
工作的:我们通过排序列表(这是多集的简单实现,导致相等的多集比较相等)来表示气泡,单个气泡[]
的计数为1,较大的气泡的计数为1等于内部气泡总数加1。对于4的计数,此程序将(如果可行)生成以下列表:
[[],[],[],[]]
[[],[],[[]]]
[[],[[],[]]]
[[],[[[]]]]
[[[]],[[]]]
[[[],[],[]]]
[[[],[[]]]]
[[[[],[]]]]
[[[[[]]]]]
出于某些原因,该程序不适合作为答案,但是最紧迫的是Prolog实际上没有map
谓词(编写它会占用太多字节)。因此,相反,我们将程序编写如下:
b([], 0).
b([Head|Tail],Count) if b(Head,HeadCount)
and b(Tail,TailCount)
and Count is HeadCount + TailCount + 1
and is_sorted([Head|Tail]).
c(Count,NPossibilities) if listof(Bubbles,b(Bubbles,Count),List)
and length(List,NPossibilities).
这里的另一个主要问题是,由于Prolog评估顺序的工作方式,运行时它将陷入无限循环。但是,我们可以通过重新安排程序来解决无限循环:
b([], 0).
b([Head|Tail],Count) if Count #= HeadCount + TailCount + 1
and b(Head,HeadCount)
and b(Tail,TailCount)
and is_sorted([Head|Tail]).
c(Count,NPossibilities) if listof(Bubbles,b(Bubbles,Count),List)
and length(List,NPossibilities).
这看起来可能很怪异-我们先将计数加在一起,然后才知道它们是什么-但是GNU Prolog #=
能够处理这种非因果算法,因为它是的第一行b
,并且HeadCount
和TailCount
必须都小于Count
(已知),它用作自然地限制递归项可以匹配多少次的方法,从而使程序始终终止。
下一步是打高尔夫球。使用单字符变量名称,使用:-
for if
和,
for的缩写and
,使用setof
而不是listof
(它具有较短的名称并在这种情况下产生相同的结果)和使用sort0(X,X)
而不是is_sorted(X)
(因为is_sorted
实际上不是真正的函数,我做了)
b([],0).
b([H|T],N):-N#=A+B+1,b(H,A),b(T,B),sort0([H|T],[H|T]).
c(X,Y):-setof(A,b(A,X),L),length(L,Y).
这很短,但是有可能做得更好。关键的见解是,[H|T]
列表语法确实非常冗长。正如Lisp程序员所知道的,列表基本上只是由cons单元组成的,而这些单元基本上只是元组,并且该程序的任何部分都几乎没有使用列表内置函数。Prolog有几种非常短的元组语法(我最喜欢的是A-B
,但我第二喜欢的是A/B
,我在这里使用它是因为在这种情况下,它会产生易于阅读的调试输出);而且我们还可以nil
在列表的末尾选择我们自己的单字符,而不是被两个字符所困扰[]
(我选择了x
,但基本上任何方法都可以)。因此,除了[H|T]
,我们可以使用T/H
和从中获取输出b
看起来像这样(请注意,元组的排序顺序与列表的排序顺序略有不同,因此它们的顺序与上面的顺序不同):
x/x/x/x/x
x/x/x/(x/x)
x/(x/x)/(x/x)
x/x/(x/x/x)
x/(x/x/x/x)
x/x/(x/(x/x))
x/(x/x/(x/x))
x/(x/(x/x/x))
x/(x/(x/(x/x)))
这比上面的嵌套列表难读,但是有可能。在思维上跳过x
s,并将其解释/()
为气泡(或简单地解释为/
没有内容的退化气泡,如果()
后面没有内容的话),并且元素与上面显示的列表版本具有1对1(如果无序)的对应关系。
当然,此列表表示形式尽管短得多,但仍具有主要缺点。它不是语言内置的内容,因此我们不能sort0
用来检查列表是否已排序。sort0
但是无论如何,它都是非常冗长的,所以手动执行操作并不会造成很大的损失(实际上,在[H|T]
列表表示中手动执行操作的字节数完全相同)。这里的关键见解是,该方案的书面检查,看是否列表进行排序,如果它的尾巴进行排序,如果它的尾巴进行排序,依此类推; 有很多多余的检查,我们可以加以利用。取而代之的是,我们将检查以确保前两个元素是正确的(这确保了在检查列表本身及其所有后缀之后,列表将最终进行排序)。
第一个元素很容易访问;那只是列表的头H
。但是,第二个元素很难访问,并且可能不存在。幸运的是,x
它少于我们正在考虑的所有元组(通过Prolog的广义比较运算符@>=
),因此我们可以认为单例列表的“第二个元素”是这样x
,程序就可以正常工作。至于实际访问第二个元素,最有趣的方法是在中添加第三个参数(out参数)b
,该参数x
在基本情况和H
递归情况下返回;这意味着我们可以从第二个递归调用的输出B
中获取尾部的头部,当然,尾部的头部是列表的第二个元素。所以b
看起来现在这个样子:
b(x,0,x).
b(T/H,N,H):-N#=A+B+1,b(H,A,_),b(T,B,J),H@>=J.
基本情况非常简单(空列表,返回计数为0,空列表的“第一个元素”为x
)。递归情况的开始方式与以前相同(只是使用T/H
符号而不是[H|T]
,并H
作为额外的out参数);我们从头部的递归调用中忽略了多余的参数,但将其存储在J
尾部的递归调用中。然后,我们要做的就是确保H
大于或等于J
(即“如果列表中至少有两个元素,则第一个大于或等于第二个元素),以确保列表最终排序。
不幸的是,setof
如果我们尝试将的先前定义c
与的这个新定义一起使用,将是一个合适的选择b
,因为它对待未使用的out参数的方式与SQL差不多GROUP BY
,这完全不是我们想要的。可以对其进行重新配置以执行我们想要的操作,但是重新配置会花费很多字符。相反,我们使用findall
,它具有更方便的默认行为,并且仅长了两个字符,从而为我们提供了以下定义c
:
c(X,Y):-findall(A,b(A,X,_),L),length(L,Y).
这就是完整的程序;首先生成气泡模式,然后花费整个字节数对其进行计数(我们需要相当长的时间findall
才能将生成器转换为列表,然后不幸地冗长地命名length
以检查该列表的长度,并为函数声明添加了样板)。