疑难杂症


23

编写一个程序,根据结的结构绘制一个二维结图。打结恰恰是听起来的样子:一圈被绑住的绳子。在数学中,结图显示了一根绳子在其自身上方或下方交叉以形成结的位置。一些示例结图如下所示:

在此处输入图片说明

绳子交叉的那条线有一个断点。

输入:描述结的输入是整数数组。绳子交叉n次的结可以表示为n个整数的数组,每个整数的值都在[0,n-1]范围内。让我们把这种阵列ķ

要获得描述结的数组,请对段0到n-1中的每一个进行编号。段0应该通向段1,应该通向段2,应该通向段3,依此类推,直到段n-1环回并通向段0。当另一段绳子越过段时,段结束(由图中的折线表示)。让我们来看看最简单的结-三叶结。在对段进行编号后,段2越过段0时结束。当段0越过时,段1结束;当段1越过时,段2结束。因此,描述结的数组为[2,0,1]。通常,段x从段x-1 mod n离开的地方开始,到段K [x]越过它的地方结束。

下图显示了结图,带有标记的段和描述结的相应数组。

在此处输入图片说明

顶部的三幅图是真实的结,而底部的三幅图是绳索的环,它们自己交叉但实际上并未打结(但仍具有相应的代码)。

您的任务是编写一个函数,该函数采用一个整数K数组(您可以称其为不同)来描述一个结(或实际上未打结的绳索环),并生成相应的结图,如上所述。例子。如果可以,请提供代码的原始版本或说明,并提供示例代码输出。相同的结通常可以用多种不同的方式表示,但是如果函数输出满足的结图将输入作为其可能的表示之一,则您的解决方案是有效的。

这是代码高尔夫球,因此以字节为单位的最短代码获胜。代表绳索的线的厚度可以为1像素,但是越下和越线必须清楚地区分(绳索的断口尺寸应比绳索的厚度大至少在任一侧至少一个像素) 。

我将对依赖于内置结理论功能的答案进行投票,但是最后选择的答案不能依赖于内置结理论功能。

我对我的记法所了解的一切:我相信有些值序列似乎并不对应于任何结或不结。例如,似乎无法绘制序列[2、3、4、0、1]。

除此之外,假设您进行了交叉,并从该交叉处开始,沿着绳索的一个方向前进,并用成功更大的积分值标记遇到的每个未标记的交叉处。对于交替的结,有一种简单的算法可以将我的符号转换为这样的标签,对于交替的结,将这个标签转换为高斯代码很简单:

template<size_t n> array<int, 2*n> LabelAlternatingKnot(array<int, n> end_at)
{
    array<int, n> end_of;
    for(int i=0;i<n;++i) end_of[end_at[i]] = i;
    array<int, 2*n> p;
    for(int& i : p) i = -1;
    int unique = 0;
    for(int i=0;i<n;i++)
    {
        if(p[2*i] < 0)
        {
            p[2*i] = unique;
            p[2*end_of[i] + 1] = unique;
            ++unique; 
        }
        if(p[2*i+1] < 0)
        {
            p[2*i+1] = unique;
            p[2*end_at[i]] = unique;
            ++unique;
        }
    }
    return p;
}
template<size_t n> auto GetGaussCode(array<int, n> end_at)
{
    auto crossings = LabelAlternatingKnot(end_at);
    for(int& i : crossings) ++i;
    for(int i=1;i<2*n;i+=2) crossings[i] = -crossings[i];
    return crossings;
}

4
您可能应该禁止内置插件这样做。(在这一点上,如果Mathematica 没有一个,我会感到震惊。)

7
@ ais523现在我无法KnotData在Mathematica中使用...:'(
JungHwan Min

1
我对您用于结交叉图的表示法感到好奇。它有名字吗?两个不相等的结是否可能具有相同的数组?
xnor 2016年

2
@ ais523:Mathematica完全具有Knot内置功能!用法示例:<< Units`; Convert[Knot, Mile/Hour]yields 1.1507794480235425 Mile/Hour。(我认为这很有趣,无论它是对还是错;但实际上是对的。)
Greg Martin

1
[0],[0,1],[0,1,2],[1,0]和其他各种数组都产生与“ unknot”等效的“结”,但是输出简单的循环在这些情况中的任何一种。符号[0]非常具体地表示一圈绳,该绳本身恰好相交一次,并且很容易分辨绘制到屏幕上的图形是否满足输入符号。
J. Antonio Perez

Answers:


22

GNU Prolog的,622个 634 850代码页668字节

更新:该程序的先前版本有时使交叉口过紧,以致无法正确渲染,这违反了规范。我添加了一些额外的代码来防止这种情况。

更新:显然,PPCG规则需要额外的代码才能使程序退出并完全恢复开始时的状态。这会使程序更长一些,并且没有增加算法上的兴趣,但是为了遵守规则,我对其进行了更改。

打高尔夫球的程序

使用GNU Prolog是因为它具有一个约束求解器语法,该语法比可移植Prolog的算术语法稍短,从而节省了一些字节。

y(A,G):-A=1;A= -1;A=G;A is-G.
z(A/B,B,G):-y(A,G),y(B,G),A=\= -B.
v(D,E,G):-E=1,member(_-_,D),p(D);F#=E-1,nth(F,D,M),(M=[_];M=L-S/R,z(L,O,G),J is F+O,nth(J,D,I/U-T/Q),(I=O,Q#=R-1,S=T;K is J+O,R=0,n(S-T-V),y(U,G),U\=O,U=\= -O,I=U,nth(K,D,O/_-V/_))),v(D,F,G).
i([H|K],S):-K=[]->assertz(n(S-H-0));T#=S+1,assertz(n(S-H-T)),i(K,T).
t([],1,_):-!.
t(D,R,G):-S#=R-1,t(E,S,G),H#=G-1,length(F,H),append(F,[[x]|E],D).
s(1,2).
s(-1,1).
s(S,T):-S>1->T=3;T=0.
r(I/O-_,C):-s(I,J),s(O,P),N#=J*4+P+1,nth(N,"│┐┌?└─?┌┘?─┐?┘└│",C).
r([X],C):-X\=y->C=10;C=32.
p([]).
p([H|T]):-r(H,C),put_code(C),!,p(T).
g(4).
g(G):-g(H),G#=H+1.
m(K):-i(K,0),g(G),t(D,G,G),length(D,L),v(D,L,G),abolish(n/1).

算法

这是很难开始的那些问题之一。如何根据给定的符号计算出结的形状并不明显,因为它无法让您知道是要在任意给定位置向左还是向右弯曲线(因此,表示法可能含糊不清)。我的解决方案是有效地使用旧的高尔夫待机系统:编写一个效率极低的程序,该程序会生成所有可能的输出,然后查看它们是否与输入匹配。(这里使用的算法效率更高一些,因为Prolog可以消除偶尔出现的死角,但是它没有足够的信息来提高计算复杂性。)

输出是通过接线图。GNU Prolog似乎想要一个与ASCII一致的单字节字符集,但并不在意使用哪个字符集(因为它将高位字符集视为不透明字符)。结果,我使用了IBM850,它得到了广泛的支持,并具有我们所需的线条图字符。

输出量

该程序按照其边界框大小的顺序搜索所有可能的结图,然后在找到第一个时退出。这是输出的样子m([0]).

 ┌┐
┌│┘
└┘ 

在我的计算机上运行花了290.528秒。该程序不是很有效。我在上将其运行了两个小时m([0,1]),并最终得到了以下结果:

┌┐┌┐
└─│┘
 └┘ 

带注释的非高尔夫版本

Stack Exchange的语法突出显示工具似乎对Prolog使用了错误的注释符号,因此%,该说明使用% #注释(而不是注释,Prolog实际使用了注释)(由于以开头%,因此当然是等效的,但正确突出显示)。

% # Representation of the drawing is: a list of:
% #     indelta/outdelta-segment/distance  (on path)
% # and [x] or [_]                         (off path; [x] for border).
% # A drawing is valid, and describes a knot, if the following apply
% # (where: d[X] is the Xth element of the drawing,
% #         k[S] is the Sth element of the input,
% #         n[S] is S + 1 modulo the number of sections):
% # d[X]=_/O-S-R, R>1 implies d[X+O]=O/_-S-(R-1)
% # d[X]=_/O-S-0 implies d[X+O]=_/_-k[S]-_
% #                  and d[X+O*2]=O/_-n[S]-_
% # all outdeltas are valid deltas (±1 row/column)

% # not technically necessary, but makes it possible to compile the
% # code (and thus makes the program faster to test):
:- dynamic([n/1]).

% # legal delta combinations:
y(A,G):-A=1;A= -1;              % # legal deltas are 1, -1,
        A=G;A is-G.             % # grid size, minus grid size
z(A/B,B,G):-y(A,G),y(B,G),      % # delta components are valid
            A=\= -B.            % # doesn't U-turn
% # z returns the outdelta for convenience (= byte savings) later on

% # We use v (verify) to verify the first E-1 elements of a drawing D.
% # nth is 1-indexed, so we recurse from length(D)+1 down to 2, with
% # 1 being the trivial base case. After verifying, the grid is printed.
% # This version of the program causes v to exit with success after
% # printing one grid (and uses p to do the work of deciding when that is).
v(D,E,G):-E=1,                  % # base case:
          member(_-_,D),        % # ensure the grid is nonempty
          p(D);                 % # print the grid (and exit)

                                % # otherwise, recursive case:
          F#=E-1,nth(F,D,M),    % # check the last unchecked element
          (
            M=[_];              % # either it's not on the path; or
            M=L-S/R,            % # it's structured correctly, and
            z(L,O,G),           % # it has a valid delta;
            J is F+O,           % # find the outdelta'd element index
            nth(J,D,I/U-T/Q),   % # and the outdelta'd element
            (
              I=O,Q#=R-1,S=T;   % # if not an endpoint, points to next pixel
              K is J+O,         % # else find segment beyond the path:
              R=0,              % # it's an endpoint,
              n(S-T-V),         % # it points to the correct segment,
              y(U,G),           % # ensure we can do NOT comparisons on U
              U\=O,U=\= -O,     % # the line we jump is at right angles
              I=U,              % # the line we jump is straight
              nth(K,D,O/_-V/_)  % # the pixel beyond has a correct indelta,
                                % # and it has the correct segment number
            )
          ),
          v(D,F,G).             % # recurse

% # We use i (init) to set up the k, n tables (k and n are fixed for
% # any run of the program, at least). S is the number of elements that
% # have been removed from K so far (initially 0). To save on characters,
% # we combine k and n into a single predicate n.
i([H|K],S):-K=[]->             % # if this is the last element,
            assertz(n(S-H-0)); % # section 0 comes after S;
            T#=S+1,            % # otherwise, add 1 to S,
            assertz(n(S-H-T)), % # that section comes after S,
            i(K,T).            % # and recurse.

% # We use t (template) to create a template drawing. First argument is
% # the drawing, second argument is the number of rows it has plus 1,
% # third argument is the number of columns it has plus 1.
t([],1,_):-!.
t(D,R,G):-S#=R-1,t(E,S,G),      % # recurse,
          H#=G-1,length(F,H),   % # F is most of this row of the grid
          append(F,[[x]|E],D).  % # form the grid with F + border + E

% # We use s (shrink) to map a coordinate into a value in the range 0, 1, 2, 3.
s(1,2).
s(-1,1).
s(S,T):-S>1->T=3;T=0.
% # We use r (representation) to map a grid cell to a character.
r(I/O-_,C):-s(I,J),s(O,P),N#=J*4+P+1,nth(N,"│┐┌?└─?┌┘?─┐?┘└│",C).
r([X],C):-X\=y->C=10;C=32.
% # We use p (print) to pretty-print a grid.
% # The base case allows us to exit after printing one knot.
p([]).
p([H|T]):-r(H,C),put_code(C),!,p(T).

% # We use g (gridsize) to generate grid sizes.
g(4).
g(G):-g(H),G#=H+1.

% # Main program.
m(K):-i(K,0),                  % # initialize n
      g(G),                    % # try all grid sizes
      t(D,G,G),                % # generate a square knot template, size G
      length(D,L),             % # find its length
      v(D,L,G),                % # verify and print one knot
      % # Technically, this doesn't verify the last element of L, but we know
      % # it's a border/newline, and thus can't be incorrect.
      abolish(n/1).            % # reset n for next run; required by PPCG rules

我下载了GNU prolog,将您的代码复制到.txt文件中,将其保存为ascii编码的.pl文件,并在名为m([0])的控制台中
J. Antonio Perez

有什么方法可以使程序效率更高?
J. Antonio Perez

我怀疑该程序可以提高效率,但是没有明显或简单的方法。更改评估顺序以使其顺其自然,而不是从左到右/从上到下,可能会有所帮助,但我不确定它会有多大帮助(而且还会有更多的代码,因此在代码高尔夫球中不可行)。

你明白我为什么不愿意吗?我的意思是...即使最好的代码也需要测试,而且您的解决方案是如此复杂,以至于我什至无法验证它会重现最简单的结([2,0,1]结)。
J. Antonio Perez

我了解,是的。但是,这是代码高尔夫球所固有的,尤其是在Prolog中。该代码实际上非常简单,这就是它运行如此缓慢的原因。关于复杂问题的代码高尔夫几乎总是导致暴力解决方案,该解决方案会根据规范检查所有可能的输出。采取某些措施使程序更有效,这将使程序更长的时间,并且很可能使程序难以理解和证明正确性。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.