Forming Polyominoes with a Chain of Rods
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
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 A
s exceeds the number of C
s 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).