Forming Polyominoes with a Chain of Rods

20

3

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

Martin Ender

Posted 2014-12-04T22:38:49.200

Reputation: 184 808

1

@PeterKagey Did you post this comment on the wrong challenge? Looks like it should have gone on this one.

– Martin Ender – 2019-03-18T09:53:38.250

"hole-free" seems superfluous. one contiguous chain can't produce hole'd polyominoes in the first place. – Sparr – 2014-12-04T22:48:46.710

Is multi-threading allowed? And if the threads are in different processes, does each one get to use 1 GB? :P – feersum – 2014-12-04T22:49:26.300

@Sparr It can when the perimeter touches itself at a corner. For instance, see No. 81 here. That one shouldn't be counted.

– Martin Ender – 2014-12-04T22:49:28.613

@feersum For simplicity, I'm going to say no to multi-threading (and will edit the challenge). – Martin Ender – 2014-12-04T22:50:44.417

@MartinBüttner ahh, I mistook touching-at-a-corner as self-intersecting. – Sparr – 2014-12-04T22:52:53.777

@Sparr It's also self-intersecting, as far as the bounding polygon is concerned. The two definitions are supposed to be equivalent, and each was supposed to be valid on its own. I just wanted to provide two different viewpoints. – Martin Ender – 2014-12-04T22:53:35.630

Answers

5

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

feersum

Posted 2014-12-04T22:38:49.200

Reputation: 29 566

doze #defines tho – Soham Chowdhury – 2014-12-15T10:04:58.233

In the absence of any other answers... here, have a bounty! – Sp3000 – 2014-12-17T13:17:16.003

3

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

Sp3000

Posted 2014-12-04T22:38:49.200

Reputation: 58 729