A Rather Knotty Conundrum

23

3

Write a program to draw a 2-D diagram of a knot based on the knot's structure. A knot is exactly what it sounds like: a loop of rope that's tied up. In mathematics, a knot diagram shows where a piece of rope crosses over or under itself to form the knot. Some example knot diagrams are shown below:

enter image description here

There's a break in the line where the rope crosses over itself.

Input: the input describing the knot is an array of integers. A knot where the rope crosses over itself n times can be represented as an array of n integers, each with a value in the range [0, n-1]. Let's call this array K.

To get the array describing a knot, number each of the segments 0 through n-1. Segment 0 should lead to segment 1, which should lead to segment 2, which should lead to segment 3, and so on until segment n-1 loops back and leads to segment 0. A segment ends when another segment of rope crosses over it (represented by a break in the line in the diagram). Let's take the simplest knot - the trefoil knot. After we've numbered the segmnents, segment 0 ends when segment 2 crosses over it; segment 1 ends when segment 0 crosses over it; and segment 2 ends when segment 1 crosses over it. Thus, the array describing the knot is [2, 0, 1]. In general, segment x starts where segment x-1 mod n left off, and ends where segment K[x] crosses over it.

The below image show knot diagrams, with labeled segments and the corresponding arrays that describe the knot.

enter image description here

The top three diagrams are true knots, while the bottom three are loops of rope that cross over themself but aren't actually knotted (but which still have corresponding codes).

Your task is to write a function which takes an array of integers K (you could call it something different) that describes a knot (or loop of rope that's not actually knotted), and which produces the corresponding knot diagram, as described in the above examples. If you can, provide an ungolfed version of your code or an explanation, and also provide sample outputs of your code. The same knot can often be represented in multiple different ways, but if the knot diagram your function outputs satisfies has the input as one of it's possible representations, your solution is valid.

This is code-golf, so shortest code in bytes wins. The line representing the rope can have a thickness of 1 pixel, however under and over-crossings must be clearly distinguishable (the size of the break in the rope should be greater than the thickness of the rope by at least a pixel on either side).

I will upvote answers that rely on built-in knot theory capabilities, but the one that's selected in the end can't rely on built-in knot theory capabilities.

Everything I know about my notation: I believe that there are sequences of values that don't seem to correspond to any knot or unknot. For example, the sequence [2, 3, 4, 0, 1] seems to be impossible to draw.

Aside from that, suppose that you take a crossing and, starting from that crossing, follow the path of the rope in one direction and label every unlabeled crossing that you come across with succesively greater integral values. For alternating knots, there is a simple algorithm to convert my notation into such a labeling, and for alternating knots it's trivial to convert this labeling into a Gauss Code:

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;
}

J. Antonio Perez

Posted 2016-12-07T23:20:34.363

Reputation: 1 480

4You should probably ban builtins for doing this. (At this point, I'd be shocked if Mathematica doesn't have one.) – None – 2016-12-07T23:24:05.507

My proposed notation is different from the one mathematica supports, but I added a no-built-ins condition – J. Antonio Perez – 2016-12-07T23:26:39.187

7@ais523 Now I can't use KnotData in Mathematica... :'( – JungHwan Min – 2016-12-08T03:16:10.787

I'll upvote your answer if you figure out how to implement my notation with KnotData! – J. Antonio Perez – 2016-12-08T03:17:10.347

1I'm curious about the notation you use for the knot crossing diagram. Does it have a name? Is it possible for two non-equivalent knots to have the same array? – xnor – 2016-12-09T23:31:26.670

I am unsure on either of those counts. I came up with the notation because it seemed simple enough to explain, and because I liked how elegant it was. I don't believe it's possible for non-equivalent knots to have the same array, but I haven't proven it yet. I do know that there are multiple arrays which all represent the same knot. – J. Antonio Perez – 2016-12-10T00:26:47.720

@JHM I don't think KnotData will be useful here. In order to use KnotData you would need to 1) determine the knot from the K-notation, then 2) produce a diagram of that knot that has that K-notation. The former will not be unique since a knot can have more than one K-notation depending on the diagram, different knots can have the same K-notation (e.g. trefoil and its mirror), and even a single diagram can have more than one K-notation depending on where you start the numbering and which direction you travel. As for the latter, nothing in KnotData allows you to create arbitrary knot diagrams. – ngenisis – 2016-12-10T04:08:35.790

@ngenisis Yep. You're correct. – JungHwan Min – 2016-12-10T05:09:04.687

2@ais523: Mathematica totally has a Knot builtin! Example usage: << Units\; Convert[Knot, Mile/Hour]yields1.1507794480235425 Mile/Hour`. (I think this is funny regardless of whether it's true or false; but it's actually true.) – Greg Martin – 2016-12-10T08:34:42.770

@GregMartin Knot is a unit commonly used in maritime navigation.

– JungHwan Min – 2016-12-10T15:32:36.620

Right, I know that is true—I meant whether it's true that Mathematica has that functionality implemented. – Greg Martin – 2016-12-10T18:26:39.880

I would try this challenge, but all this numerical describing is very ambiguous. [0] is analagous to a loop that is not crossed,, as well as [0,1]. thus [0,1] is equal to [0]. I don't think any program will give you homologous to the examples you provide. Most notably, for the last 3 test cases. – tuskiomi – 2016-12-12T04:48:21.263

1[0], [0,1], [0,1,2], [1,0] and a variety of other arrays all produce "knots" that are equivalent to the unknot, however outputting a simple loop would be incorrect in any of those cases. The notation [0] very specifically means a loop of rope that intersects itself exactly once, and it is very easy to tell whether a figure drawn to the screen satisfies the input notation or not. – J. Antonio Perez – 2016-12-12T06:22:25.177

@JorgePerez but it does not matter weather or not the loop crosses, as the knots are topologically the same thing. they are the same knot. no difference. A program will not be able to simulate the last three test cases any differently from each other. I would also add that your representation is incomplete. The knots that you give for the arrays that you give are analagous to other knots, and can yield more than one proper output. For now, it seems as if your expected outputs are much more complex, and ambiguous than any program can handle with a 1- output limitation. I'm voting to close. – tuskiomi – 2016-12-12T20:14:06.130

1

This should use a different notation (see: http://www.indiana.edu/~knotinfo/descriptions/dt_notation.html)

– Gábor Fekete – 2016-12-13T17:05:50.723

@GáborFekete magnificent! – tuskiomi – 2016-12-15T15:52:11.547

I'm trying to prove your notation wrong but I couldn't yet. There is a funny value which got me thinking: [0, 0, 0] (using your notation) which is basically the unknot with two twists in it. – Gábor Fekete – 2016-12-15T16:48:42.357

1I came up with an algorithm to convert my notation into a gauss code for alternating knots, and added some additional information about it – J. Antonio Perez – 2016-12-15T17:28:37.047

Will inputs be guaranteed to represent alternating knots, or does your notation support non-alternating knots as well? – mbomb007 – 2016-12-19T21:24:33.153

I found a possible problem with your notation. You mapped it onto a Gauss Code, but according to this website, "In general, the Gauss Code for some knot diagram cannot reconstruct an equivalent diagram, but a minor revision of the notation will make the reconstruction possible. The revision is called the Extended Gauss Code." The extension is necessary because otherwise there is no means of knowing what the "handedness" of a crossing is.

– mbomb007 – 2016-12-19T22:17:54.540

It shouldn't matter for the purpose of this challenge if direction doesn't matter, though. – mbomb007 – 2016-12-19T22:23:25.853

I decided to solve this challenge, but optimizing for (algorithmic) speed, not for golfing. I may optimize for golfing later. My program can do [2, 0, 1] in .05s, and [3, 4, 0, 1, 2] in 8s. I've put in on github here: knot

– isaacg – 2017-01-11T13:12:32.117

Answers

22

GNU Prolog, 622 634 668 bytes in code page 850

Update: The previous version of the program was sometimes making crossings so tight that they wouldn't render properly, which violates the spec. I've added in some extra code to prevent that.

Update: Apparently PPCG rules require extra code to make the program exit and restore the state exactly as it was at the start. This makes the program somewhat longer and adds no algorithmic interest to it, but in the interests of rule compliance, I've changed it.

Golfed program

Using GNU Prolog because it has a constraint solver syntax that's slightly shorter than portable Prolog's arithmetic syntax, saving a few bytes.

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).

Algorithm

This is one of those problems where it's hard to know how to start. It's not obvious how to work out the shape of the knot from the notation given, because it doesn't let you know whether you're meant to bend the line to the left or the right at any given location (and as such, the notation might be ambiguous). My solution was, effectively, to use the old golfing standby: write an incredibly inefficient program which generates all possible outputs and then see if they match the input. (The algorithm used here is slightly more efficient, as Prolog can throw out the occasional dead end, but it doesn't have enough information to improve the computational complexity.)

The output is via terminal art. GNU Prolog seems to want a single byte character set that's consistent with ASCII, but doesn't care which one is used (as it treats characters with the high bit set as opaque). As a result, I used IBM850, which is widely supported and has the line drawing characters we need.

Output

The program searches all possible knot images, in the order of the size of their bounding box, then exits when it finds the first. Here's what the output looks like for m([0]).:

 ┌┐
┌│┘
└┘ 

This took 290.528 seconds to run on my computer; the program isn't very efficient. I left it running for two hours on m([0,1]), and ended up with this:

┌┐┌┐
└─│┘
 └┘ 

Ungolfed version with comments

Stack Exchange's syntax highlighter seems to have the wrong comment symbol for Prolog, so instead of % comments (which Prolog actually uses), this explanation uses % # comments (which are of course equivalent due to starting with %, but highlight correctly).

% # 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

user62131

Posted 2016-12-07T23:20:34.363

Reputation:

I downloaded GNU prolog, copied your code into a .txt file, saved it as an ascii-encoded .pl file, and in the console called m([0]) – J. Antonio Perez – 2016-12-11T05:36:58.477

Are there any ways you could make the program more efficient? – J. Antonio Perez – 2016-12-11T06:31:13.600

I suspect the program can be made more efficient, but there's no obvious or easy way. Changing the evaluation order to move along the knot, rather than left-to-right/top-to-bottom, would likely help, but I'm not sure how much it would help (and it'd also be substantially more code, thus not viable in a [tag:code-golf]). – None – 2016-12-11T12:41:21.230

You understand why I'm reluctant, right? I mean... even the best code needs to be tested, and your solution is so complex that I can't even verify that it'll reproduce the very simplest knot (the [2, 0, 1] knot). – J. Antonio Perez – 2016-12-12T06:15:55.437

I understand, yes. However, this is kind-of inherent in [tag:code-golf], especially when it comes to Prolog. The code is actually very simple, which is the reason it runs so slowly; [tag:code-golf] on a complex problem pretty much always leads to a brute-force solution that checks all possible outputs against the specification. Doing something to make the program more efficient would make it substantially longer, and would quite possibly make it harder to understand and prove correct as well. – None – 2016-12-13T00:28:25.807

You should let it run for a few other inputs. Try a slightly larger one and run it for a couple days. – mbomb007 – 2016-12-16T23:01:04.400