Sebuah teka-teki yang agak rumit

23

Tulis sebuah program untuk menggambar diagram 2-D dari sebuah simpul berdasarkan pada struktur simpul tersebut. Simpul persis seperti apa itu: simpul yang diikat. Dalam matematika, diagram simpul menunjukkan di mana seutas tali menyilang atau membentuk simpul. Beberapa contoh diagram simpul ditunjukkan di bawah ini:

masukkan deskripsi gambar di sini

Ada jeda di garis di mana tali menyilang.

Input: input yang menggambarkan simpul adalah array bilangan bulat. Sebuah simpul mana tali menyilang sendiri n kali dapat direpresentasikan sebagai array dari n bilangan bulat, masing-masing dengan nilai dalam rentang [0, n-1]. Mari kita sebut array ini K .

Untuk mendapatkan array yang menggambarkan simpul, beri nomor setiap segmen 0 hingga n-1. Segmen 0 harus mengarah ke segmen 1, yang harus mengarah ke segmen 2, yang harus mengarah ke segmen 3, dan seterusnya sampai segmen n-1 berputar kembali dan mengarah ke segmen 0. Segmen berakhir ketika segmen lain dari tali melintasinya ( diwakili oleh sebuah break di baris dalam diagram). Mari kita ambil simpul yang paling sederhana - simpul trefoil. Setelah kami menomori segmen, segmen 0 berakhir ketika segmen 2 melintasinya; segmen 1 berakhir ketika segmen 0 melintasinya; dan segmen 2 berakhir ketika segmen 1 melewatinya. Dengan demikian, array yang menggambarkan simpul adalah [2, 0, 1]. Secara umum, segmen x dimulai ketika segmen x-1 mod n tinggalkan, dan berakhir di mana segmen K [x] melintasinya.

Gambar di bawah ini menunjukkan diagram simpul, dengan segmen berlabel dan array yang sesuai yang menggambarkan simpul tersebut.

masukkan deskripsi gambar di sini

Tiga diagram teratas adalah simpul sejati, sedangkan tiga diagram bawah adalah simpul tali yang melintang di atas dirinya sendiri tetapi sebenarnya tidak diikat (tetapi masih memiliki kode yang sesuai).

Tugas Anda adalah menulis fungsi yang mengambil array bilangan bulat K (Anda bisa menyebutnya sesuatu yang berbeda) yang menggambarkan simpul (atau simpul tali yang sebenarnya tidak diikat), dan yang menghasilkan diagram simpul yang sesuai, seperti dijelaskan di atas contoh. Jika Anda bisa, berikan versi kode Anda atau penjelasan yang tidak diklik, dan berikan juga sampel hasil kode Anda. Simpul yang sama sering dapat direpresentasikan dalam berbagai cara yang berbeda, tetapi jika diagram simpul yang dihasilkan fungsi Anda mendapat input sebagai salah satu kemungkinan representasi, solusi Anda valid.

Ini adalah kode-golf, jadi kode terpendek dalam byte menang. Garis yang mewakili tali dapat memiliki ketebalan 1 piksel, namun di bawah dan perlintasan-lebih harus dapat dibedakan dengan jelas (ukuran putus tali harus lebih besar dari ketebalan tali dengan setidaknya satu piksel di kedua sisi) .

Saya akan memilih jawaban yang bergantung pada kemampuan teori simpul bawaan, tetapi jawaban yang dipilih pada akhirnya tidak bisa mengandalkan kemampuan teori simpul bawaan.

Semua yang saya tahu tentang notasi saya: Saya percaya bahwa ada urutan nilai yang tampaknya tidak sesuai dengan simpul atau tidak. Misalnya, urutan [2, 3, 4, 0, 1] tampaknya tidak mungkin untuk digambar.

Selain itu, anggaplah Anda mengambil persimpangan dan, mulai dari persimpangan itu, ikuti jalan tali dalam satu arah dan beri label setiap persimpangan yang tidak berlabel yang Anda temui dengan nilai integral yang lebih besar secara berturut-turut. Untuk simpul bolak-balik, ada algoritma sederhana untuk mengubah notasi saya menjadi pelabelan seperti itu, dan untuk simpul bolak-balik itu sepele untuk mengubah pelabelan ini menjadi Kode Gauss:

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
sumber
4
Anda mungkin harus melarang bawaan untuk melakukan ini. (Pada titik ini, saya akan terkejut jika Mathematica tidak memilikinya.)
7
@ ais523 Sekarang saya tidak bisa menggunakan KnotDatadi Mathematica ...: '(
JungHwan Min
1
Saya ingin tahu tentang notasi yang Anda gunakan untuk diagram persimpangan simpul. Apakah itu mempunyai nama? Apakah mungkin untuk dua simpul yang tidak setara memiliki array yang sama?
xnor
2
@ ais523: Mathematica benar-benar memiliki Knotbuiltin! Contoh penggunaan: << Units`; Convert[Knot, Mile/Hour]hasil 1.1507794480235425 Mile/Hour. (Saya pikir ini lucu terlepas dari apakah itu benar atau salah; tetapi sebenarnya benar.)
Greg Martin
1
[0], [0,1], [0,1,2], [1,0] dan berbagai array lainnya semuanya menghasilkan "simpul" yang setara dengan yang tidak diketik, namun menghasilkan loop sederhana akan salah dalam salah satu dari kasus-kasus itu. Notasi [0] secara khusus berarti lilitan tali yang memotong dirinya tepat sekali, dan sangat mudah untuk mengetahui apakah gambar yang ditarik ke layar memenuhi notasi input atau tidak.
J. Antonio Perez

Jawaban:

22

GNU Prolog, 622 634 668 byte di halaman kode 850

Pembaruan : Versi sebelumnya dari program kadang-kadang membuat penyeberangan yang sangat ketat sehingga tidak akan ditampilkan dengan benar, yang melanggar spesifikasi. Saya telah menambahkan beberapa kode tambahan untuk mencegahnya.

Pembaruan : Rupanya aturan PPCG memerlukan kode tambahan untuk membuat program keluar dan mengembalikan status persis seperti di awal. Hal ini membuat program agak lama dan tidak menambahkan minat algoritmik untuknya, tetapi demi kepentingan kepatuhan terhadap peraturan, saya telah mengubahnya.

Program golf

Menggunakan GNU Prolog karena memiliki sintaks solver pemecah yang sedikit lebih pendek dari sintaks aritmatika Prolog portabel, menghemat beberapa byte.

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

Algoritma

Ini adalah salah satu masalah di mana sulit untuk mengetahui bagaimana memulainya. Tidaklah jelas bagaimana cara mengetahui bentuk simpul dari notasi yang diberikan, karena itu tidak memberi tahu Anda apakah Anda harus menekuk garis ke kiri atau ke kanan di lokasi tertentu (dan dengan demikian, notasi mungkin ambigu). Solusi saya adalah, secara efektif, menggunakan siaga golf lama: menulis sebuah program yang sangat tidak efisien yang menghasilkan semua output yang mungkin dan kemudian melihat apakah cocok dengan input. (Algoritma yang digunakan di sini sedikit lebih efisien, karena Prolog dapat membuang jalan buntu sesekali, tetapi tidak memiliki informasi yang cukup untuk meningkatkan kompleksitas komputasi.)

Outputnya adalah melalui seni terminal. GNU Prolog tampaknya menginginkan set karakter byte tunggal yang konsisten dengan ASCII, tetapi tidak peduli yang mana yang digunakan (karena memperlakukan karakter dengan bit set tinggi sebagai buram). Akibatnya, saya menggunakan IBM850, yang didukung secara luas dan memiliki karakter menggambar garis yang kami butuhkan.

Keluaran

Program mencari semua gambar simpul yang mungkin, sesuai dengan ukuran kotak pembatasnya, kemudian keluar ketika menemukan yang pertama. Seperti apa hasilnya m([0]).:

 ┌┐
┌│┘
└┘ 

Ini membutuhkan waktu 290,528 detik untuk berjalan di komputer saya; program ini tidak terlalu efisien. Saya membiarkannya berjalan selama dua jam m([0,1]), dan berakhir dengan ini:

┌┐┌┐
└─│┘
 └┘ 

Versi tidak dikoleksi dengan komentar

Highlighter sintaksis Stack Exchange tampaknya memiliki simbol komentar yang salah untuk Prolog, jadi alih-alih %komentar (yang sebenarnya digunakan Prolog), penjelasan ini menggunakan % #komentar (yang tentu saja setara karena dimulai dengan %, tetapi sorot dengan benar).

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

sumber
Saya mengunduh prolog GNU, menyalin kode Anda ke file .txt, menyimpannya sebagai file .pl ascii-encoded, dan di konsol bernama m ([0])
J. Antonio Perez
Apakah ada cara Anda bisa membuat program lebih efisien?
J. Antonio Perez
Saya menduga program ini dapat dibuat lebih efisien, tetapi tidak ada cara yang jelas atau mudah. Mengubah urutan evaluasi untuk bergerak sepanjang simpul, daripada kiri-ke-kanan / atas-bawah, kemungkinan akan membantu, tapi saya tidak yakin berapa banyak akan membantu (dan itu juga akan menjadi kode yang jauh lebih banyak, sehingga tidak layak dalam kode-golf ).
Anda mengerti mengapa saya enggan, kan? Maksud saya ... bahkan kode terbaik perlu diuji, dan solusi Anda sangat kompleks sehingga saya bahkan tidak dapat memverifikasi bahwa itu akan mereproduksi simpul yang paling sederhana (simpul [2, 0, 1]).
J. Antonio Perez
Saya mengerti ya. Namun, ini semacam inheren dalam kode-golf , terutama ketika menyangkut Prolog. Kode ini sebenarnya sangat sederhana, itulah sebabnya ia berjalan sangat lambat; kode-golf pada masalah yang kompleks hampir selalu mengarah ke solusi brute-force yang memeriksa semua output yang mungkin terhadap spesifikasi. Melakukan sesuatu untuk membuat program lebih efisien akan membuatnya jauh lebih lama, dan sangat mungkin akan membuat lebih sulit untuk memahami dan membuktikan kebenaran juga.