19
\$\begingroup\$

Background

Consider a (closed) chain of rods, each of which has integer length. How many distinct hole-free polyominoes can you form with a given chain? Or in other words, how many different non-self-intersecting polygons with axis-aligned sides can you form with a given chain?

Let's look at an example. Consider a particular chain consisting of 8 rods of length 1 and 2, which we can represent as [1, 1, 2, 2, 1, 1, 2, 2]. Up to rotations and translations, there are only 8 possible polyominoes (we do count different reflections):

enter image description here

This first rod is dark blue, and then we traverse the polygon in a counter-clockwise sense.

The sense of rotation doesn't affect the result in the above example. But let's consider another chain, [3, 1, 1, 1, 2, 1, 1], which yields the following 3 polyominoes:

enter image description here

Notice that we do not include a reflection of the last polyomino, because it would require clockwise traversal.

If we had a more flexible chain of the same length, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], we would actually be able to form both reflections among some other polyoninoes, totalling 9:

enter image description here

The Challenge

Given a description of a chain, as an array or similar, determine the number of distinct polyominoes you can form (up to rotations and translations) by using the rods in order while going around the perimeter in a counter-clockwise sense.

Please write a full program and include commands to compile (if applicable) and run your code from the command line. Please also include a link to a free compiler/interpreter for your language.

Your program should read the input from STDIN. The first line will contain an integer M. The next M lines will be test cases, each of which will be a space-separated list of rod lengths. Your program should print M lines to STDOUT, each of which consists of a single integer - the number of distinct polyominoes that can be formed.

You must only use a single thread.

Your program must not use more than 1 GB of memory at any time. (This is not a completely strict limit, but I will monitor your executable's memory usage, and kill any process that consistently uses more than 1 GB, or spikes significantly above it.)

To prevent excessive amounts of pre-computation, your code must not be longer than 20,000 bytes, and you must not read any files.

You must also not optimise towards the specific test cases chosen (e.g. by hardcoding their results). If I suspect that you do, I reserve the right to generate new benchmark sets. The test sets are random, so your program's performance on those should be representative for its performance on arbitrary input. The only assumption you're allowed to make is that the sum of the rod lengths is even.

Scoring

I have provided benchmark sets for chains of N = 10, 11, ..., 20 rods. Each test set contains 50 random chains with lengths between 1 and 4 inclusive.

Your primary score is the largest N for which your program completes the entire test set within 5 minutes (on my machine, under Windows 8). The tie breaker will be the actual time taken by your program on that test set.

If anyone beats the largest test set, I will keep adding larger ones.

Test Cases

You can use the following test cases to check the correctness of your implementation.

Input                            Output

1 1                              0
1 1 1 1                          1
1 1 1 1 1 1                      1
1 1 1 1 1 1 1 1                  3
1 1 1 1 1 1 1 1 1 1              9
1 1 1 1 1 1 1 1 1 1 1 1          36
1 1 1 1 1 1 1 1 1 1 1 1 1 1      157
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1  758
1 1 2 2 1 1 2 2                  8
1 1 2 2 1 1 2 2 1 1              23
1 1 2 2 1 1 2 2 1 1 2 2          69
1 2 1 2 1 2 1 2                  3
1 2 1 2 1 2 1 2 1 2 1 2          37
1 2 3 2 1 2 3 2                  5
1 2 3 2 1 2 3 2 1 2 3 2          23
3 1 1 1 2 1 1                    3
1 2 3 4 5 6 7                    1
1 2 3 4 5 6 7 8                  3
1 2 3 4 5 6 7 8 9 10 11          5
2 1 5 3 3 2 3 3                  4
4 1 6 5 6 3 1 4                  2
3 5 3 5 1 4 1 1 3                5
1 4 3 2 2 5 5 4 6                4
4 1 3 2 1 2 3 3 1 4              18
1 1 1 1 1 2 3 3 2 1              24
3 1 4 1 2 2 1 1 2 4 1 2          107
2 4 2 4 2 2 3 4 2 4 2 3          114

You find an input file with these over here.

Leaderboard

   User          Language       Max N      Time taken (MM:SS:mmm)

1. feersum       C++ 11         19         3:07:430

2. Sp3000        Python 3       18         2:30:181
\$\endgroup\$
  • \$\begingroup\$ "hole-free" seems superfluous. one contiguous chain can't produce hole'd polyominoes in the first place. \$\endgroup\$ – Sparr Dec 4 '14 at 22:48
  • \$\begingroup\$ Is multi-threading allowed? And if the threads are in different processes, does each one get to use 1 GB? :P \$\endgroup\$ – feersum Dec 4 '14 at 22:49
  • \$\begingroup\$ @Sparr It can when the perimeter touches itself at a corner. For instance, see No. 81 here. That one shouldn't be counted. \$\endgroup\$ – Martin Ender Dec 4 '14 at 22:49
  • \$\begingroup\$ @feersum For simplicity, I'm going to say no to multi-threading (and will edit the challenge). \$\endgroup\$ – Martin Ender Dec 4 '14 at 22:50
  • 1
    \$\begingroup\$ @PeterKagey Did you post this comment on the wrong challenge? Looks like it should have gone on this one. \$\endgroup\$ – Martin Ender Mar 18 at 9:53
5
+100
\$\begingroup\$

C++11

Updates: Added the first line of c which breaks out early if the distance is too far from the origin (which was the whole purpose of the variable rlen, but I forgot to write it in the first version). I changed it to use much less memory, but at the cost of time. It now solves N=20 in just under 5 minutes for me.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <map>
#include <ctime>

#define M std::map
#define MS 999
#define l (xM*2+1)

#define KITTENS(A,B)A##B
#define CATS(A,B)KITTENS(A,B)
#define LBL CATS(LBL,__LINE__)
#define unt unsigned
#define SU sizeof(unt)
#define SUB (SU*8)
#define newa (nb^SZ||fail("blob"),nb+++blob)

#define D

struct vec {int x, y;};


unt s[MS*2];
int xM, sl[MS];
vec X[MS];

struct a;
struct a  { M<unt,unt>v;};

#define SZ ((1<<29)/sizeof(a))
a*blob;
unt nb;


int fail(const char*msg)
{
    printf("failed:%s", msg);
    exit(1);
    return 1;
}

struct
{
    unt*m;
    bool operator()(int x, int y) { return m[(x+l*y)/SUB] >> (x+l*y)%SUB & 1; }
    void one(int x, int y) { m[(x+l*y)/SUB] |= 1U << (x+l*y)%SUB; }
    void zero(int x, int y) { m[(x+l*y)/SUB] &= ~(1U << (x+l*y)%SUB); }
} g;

unt c(a*A, vec x, unt rlen, unt sn) {
    if((unt)x.y+abs(x.x) > rlen) return 0;
    if(!rlen) {
        vec *cl=X, *cr=X, *ct=X;
        for(unt i=1; i<sn; i++) {
            #define BLAH(Z,A,B,o,O) \
                if(X[i].A o Z->A || (X[i].A == Z->A && X[i].B O Z->B)) \
                   Z = X+i

            BLAH(cl,x,y,<,>);
            BLAH(cr,x,y,>,<);
            BLAH(ct,y,x,>,>);
        }
        unt syms = 1;
        #define BLA(H,Z) {bool sy=1;for(unt o=0; o<sn; o++) sy &= (int)(1|-(H))*sl[o] == sl[(Z-X+o)%sn]; syms += sy;}
        BLA(~o&1,cl)
        BLA(1,ct)
        BLA(o&1,cr)

        #ifdef D
            //printf("D");for(int i=0;i<sn;i++)printf(" %u",sl[i]);printf("\n");
            if(syms==3) fail("symm");
        #endif

        return syms;
    }
    if(!(x.x|x.y|!sn)) return 0;
    X[sn] = x;

    unt k = 0;
    for(auto it: A->v) {
        int len = it.first;
        bool ve = sn&1;
        int dx = ve?0:len, dy = ve?len:0;

        #define PPCG(O)(x.x O (ve?0:z), x.y O (ve?z:0))
        #define MACR(O) { \
            vec v2 = {x.x O dx, x.y O dy}; \
            if(v2.y<0||(!v2.y&&v2.x<0)||abs(v2.x)>xM||v2.y>xM) \
                goto LBL; \
            for(int z=1; z<=len; z++) \
                if(g PPCG(O)) \
                    goto LBL; \
            for(int z=1; z<=len; z++) \
                g.one PPCG(O); \
            sl[sn] = O len; \
            k += c(blob+it.second, v2, rlen - len, sn+1); \
            for(int z=1; z<=len; z++) \
                g.zero PPCG(O); \
            } LBL: \

    MACR(+);
    MACR(-);
    }

    return k;
}

void stuff(a *n, unt j, unt r, unt len1)
{
    unt t=0;
    for(unt i=j; i<j+r; i++) {
        t += s[i];
        if((int)t > xM || (len1 && t>len1)) break;
        if(len1 && t < len1) continue;
        int r2 = r-(i-j)-1;
        if(r2) {
            unt x;
            if(n->v.count(t))
                x = n->v[t];
            else
                n->v[t] = x = newa - blob;
            stuff(blob+x, i+1, r2, 0);
        } else n->v[t] = -1;
    }
}

int main()
{
    time_t tim = time(0);
    blob = new a[SZ];
    int n;
    scanf("%u",&n);
    while(n--) {
        nb = 0;
        unt ns=0, tl=0;
        while(scanf("%u",s+ns)) {
            tl += s[ns];
            if(++ns==MS) return 1;
            if(getchar() < 11) break;
        }
        xM = ~-tl/2;
        g.m = (unt*)calloc((xM+1)*l/SU + 1,4);

        memcpy(s+ns, s, ns*SU);

        unt ans = 0;
        for(unt len1 = 1; (int)len1 <= xM; len1++) {
            a* a0 = newa;
            for(unt i=0; i<ns; i++)
                stuff(a0, i, ns, len1);
            ans += c(a0, {}, tl, 0);
            for(unt i=0; i<nb; i++)
                blob[i].v.clear();
        }
        printf("%d\n", ans/4);
        free(g.m);
    }

    tim = time(0) - tim;
    printf("time:%d",(int)tim);
    return 0;
}

Compile with

g++ --std=c++11 -O3 feersum.cpp -o feersum.exe
\$\endgroup\$
  • \$\begingroup\$ doze #defines tho \$\endgroup\$ – Soham Chowdhury Dec 15 '14 at 10:04
  • \$\begingroup\$ In the absence of any other answers... here, have a bounty! \$\endgroup\$ – Sp3000 Dec 17 '14 at 13:17
3
\$\begingroup\$

Python 3 (with PyPy) — N = 18

ANGLE_COMPLEMENTS = {"A": "C", "F": "F", "C": "A"}
MOVE_ENUMS = {"U": 0, "R": 1, "D": 2, "L": 3}
OPPOSITE_DIR = {"U": "D", "D": "U", "L": "R", "R": "L", "": ""}

def canonical(angle_str):
    return min(angle_str[i:] + angle_str[:i] for i in range(len(angle_str)))

def to_angles(moves):
    """
    Convert a string of UDLR to a string of angles where
      A -> anticlockwise turn
      C -> clockwise turn
      F -> forward
    """

    angles = []

    for i in range(1, len(moves)):
        if moves[i] == moves[i-1]:
            angles.append("F")
        elif (MOVE_ENUMS[moves[i]] - MOVE_ENUMS[moves[i-1]]) % 4 == 1:
            angles.append("C")
        else:
            angles.append("A")

    if moves[0] == moves[len(moves)-1]:
        angles.append("F")
    elif (MOVE_ENUMS[moves[0]] - MOVE_ENUMS[moves[len(moves)-1]]) % 4 == 1:
        angles.append("C")
    else:
        angles.append("A")

    return "".join(angles)

def solve(rods):
    FOUND_ANGLE_STRS = set()

    def _solve(rods, rod_sum, point=(0, 0), moves2=None, visited=None, last_dir=""):
        # Stop when point is too far from origin
        if abs(point[0]) + abs(point[1]) > rod_sum:
            return

        # No more rods, check if we have a valid solution
        if not rods:
            if point == (0, 0):
               angle_str = to_angles("".join(moves2))

               if angle_str.count("A") - angle_str.count("C") == 4:
                   FOUND_ANGLE_STRS.add(canonical(angle_str))

            return

        r = rods.pop(0)

        if not visited:
            visited = set()
            move_dirs = [((r, 0), "R")]
            moves2 = []

        else:
            move_dirs = [((r,0), "R"), ((0,r), "U"), ((-r,0), "L"), ((0,-r), "D")]

        opp_dir = OPPOSITE_DIR[last_dir]

        for move, direction in move_dirs:
            if direction == opp_dir: continue

            new_point = (move[0] + point[0], move[1] + point[1])
            added_visited = set()
            search = True

            for i in range(min(point[0],new_point[0]), max(point[0],new_point[0])+1):
                for j in range(min(point[1],new_point[1]), max(point[1],new_point[1])+1):
                    if (i, j) != point:
                        if (i, j) in visited:
                            search = False

                            for a in added_visited:
                                visited.remove(a)

                            added_visited = set()                            
                            break

                        else:
                            visited.add((i, j))
                            added_visited.add((i, j))

                if not search:
                    break

            if search:
                moves2.append(direction*r)
                _solve(rods, rod_sum-r, new_point, moves2, visited, direction)
                moves2.pop()

            for a in added_visited:
                visited.remove(a)

        rods.insert(0, r)
        return

    _solve(rods, sum(rods))
    return len(FOUND_ANGLE_STRS)

num_rods = int(input())

for i in range(num_rods):
    rods = [int(x) for x in input().split(" ")]
    print(solve(rods))

Run with ./pypy <filename>.


This is the reference implementation I wrote when discussing the question with Martin. It wasn't made with speed in mind and is quite hacky, but it should provide a good baseline to kick things off.

N = 18 takes about 2.5 minutes on my modest laptop.

Algorithm

Rotations are checked by converting each shape into a series of F for forward, A for anticlockwise turn and C for clockwise turn at each lattice point on the boundary of the shape — I call this an angle string. Two shapes are rotationally identical if their angle strings are cyclic permutations. Rather than always checking this by comparing two angle strings directly, when we find a new shape we convert to a canonical form before storing. When we have a new candidate, we convert to the canonical form and check if we already have this (thus exploiting hashing, rather than iterating through the whole set).

The angle string is also used to check that the shape is formed anticlockwise, by making sure that the number of As exceeds the number of Cs by 4.

Self-intersection is naïvely checked by storing every lattice point on the boundary of the shape, and seeing if a point is visited twice.

The core algorithm is simple, placing the first rod to the right, then trying all possibilities for the remaining rods. If the rods reach a point which is too far from the origin (i.e. sum of remaining rod lengths is less than the Manhattan distance of the point from the origin), then we prematurely stop searching that subtree.

Updates (latest first)

  • 6/12: Introduced canonical form, added a few micro-optimisations
  • 5/12: Fixed error in algorithm explanation. Made the quadratic cyclic-permutation-checking algorithm linear using the A,B cyclic permutations iff A substring of B+B method (I have no idea why I didn't do this earlier).
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.