C ++ 11 - casi funciona :)
Después de leer este artículo , obtuve un poco de sabiduría de ese tipo que aparentemente trabajó durante 25 años en el problema menos complicado de contar caminos que se evitan en una red cuadrada.
#include <cassert>
#include <ctime>
#include <sstream>
#include <vector>
#include <algorithm> // sort
using namespace std;
// theroretical max snake lenght (the code would need a few decades to process that value)
#define MAX_LENGTH ((int)(1+8*sizeof(unsigned)))
#ifndef _MSC_VER
#ifndef QT_DEBUG // using Qt IDE for g++ builds
#define NDEBUG
#endif
#endif
#ifdef NDEBUG
inline void tprintf(const char *, ...){}
#else
#define tprintf printf
#endif
void panic(const char * msg)
{
printf("PANIC: %s\n", msg);
exit(-1);
}
// ============================================================================
// fast bit reversal
// ============================================================================
unsigned bit_reverse(register unsigned x, unsigned len)
{
x = (((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1));
x = (((x & 0xcccccccc) >> 2) | ((x & 0x33333333) << 2));
x = (((x & 0xf0f0f0f0) >> 4) | ((x & 0x0f0f0f0f) << 4));
x = (((x & 0xff00ff00) >> 8) | ((x & 0x00ff00ff) << 8));
return((x >> 16) | (x << 16)) >> (32-len);
}
// ============================================================================
// 2D geometry (restricted to integer coordinates and right angle rotations)
// ============================================================================
// points using integer- or float-valued coordinates
template<typename T>struct tTypedPoint;
typedef int tCoord;
typedef double tFloatCoord;
typedef tTypedPoint<tCoord> tPoint;
typedef tTypedPoint<tFloatCoord> tFloatPoint;
template <typename T>
struct tTypedPoint {
T x, y;
template<typename U> tTypedPoint(const tTypedPoint<U>& from) : x((T)from.x), y((T)from.y) {} // conversion constructor
tTypedPoint() {}
tTypedPoint(T x, T y) : x(x), y(y) {}
tTypedPoint(const tTypedPoint& p) { *this = p; }
tTypedPoint operator+ (const tTypedPoint & p) const { return{ x + p.x, y + p.y }; }
tTypedPoint operator- (const tTypedPoint & p) const { return{ x - p.x, y - p.y }; }
tTypedPoint operator* (T scalar) const { return{ x * scalar, y * scalar }; }
tTypedPoint operator/ (T scalar) const { return{ x / scalar, y / scalar }; }
bool operator== (const tTypedPoint & p) const { return x == p.x && y == p.y; }
bool operator!= (const tTypedPoint & p) const { return !operator==(p); }
T dot(const tTypedPoint &p) const { return x*p.x + y * p.y; } // dot product
int cross(const tTypedPoint &p) const { return x*p.y - y * p.x; } // z component of cross product
T norm2(void) const { return dot(*this); }
// works only with direction = 1 (90° right) or -1 (90° left)
tTypedPoint rotate(int direction) const { return{ direction * y, -direction * x }; }
tTypedPoint rotate(int direction, const tTypedPoint & center) const { return (*this - center).rotate(direction) + center; }
// used to compute length of a ragdoll snake segment
unsigned manhattan_distance(const tPoint & p) const { return abs(x-p.x) + abs(y-p.y); }
};
struct tArc {
tPoint c; // circle center
tFloatPoint middle_vector; // vector splitting the arc in half
tCoord middle_vector_norm2; // precomputed for speed
tFloatCoord dp_limit;
tArc() {}
tArc(tPoint c, tPoint p, int direction) : c(c)
{
tPoint r = p - c;
tPoint end = r.rotate(direction);
middle_vector = ((tFloatPoint)(r+end)) / sqrt(2); // works only for +-90° rotations. The vector should be normalized to circle radius in the general case
middle_vector_norm2 = r.norm2();
dp_limit = ((tFloatPoint)r).dot(middle_vector);
assert (middle_vector == tPoint(0, 0) || dp_limit != 0);
}
bool contains(tFloatPoint p) // p must be a point on the circle
{
if ((p-c).dot(middle_vector) >= dp_limit)
{
return true;
}
else return false;
}
};
// returns the point of line (p1 p2) that is closest to c
// handles degenerate case p1 = p2
tPoint line_closest_point(tPoint p1, tPoint p2, tPoint c)
{
if (p1 == p2) return{ p1.x, p1.y };
tPoint p1p2 = p2 - p1;
tPoint p1c = c - p1;
tPoint disp = (p1p2 * p1c.dot(p1p2)) / p1p2.norm2();
return p1 + disp;
}
// variant of closest point computation that checks if the projection falls within the segment
bool closest_point_within(tPoint p1, tPoint p2, tPoint c, tPoint & res)
{
tPoint p1p2 = p2 - p1;
tPoint p1c = c - p1;
tCoord nk = p1c.dot(p1p2);
if (nk <= 0) return false;
tCoord n = p1p2.norm2();
if (nk >= n) return false;
res = p1 + p1p2 * (nk / n);
return true;
}
// tests intersection of line (p1 p2) with an arc
bool inter_seg_arc(tPoint p1, tPoint p2, tArc arc)
{
tPoint m = line_closest_point(p1, p2, arc.c);
tCoord r2 = arc.middle_vector_norm2;
tPoint cm = m - arc.c;
tCoord h2 = cm.norm2();
if (r2 < h2) return false; // no circle intersection
tPoint p1p2 = p2 - p1;
tCoord n2p1p2 = p1p2.norm2();
// works because by construction p is on (p1 p2)
auto in_segment = [&](const tFloatPoint & p) -> bool
{
tFloatCoord nk = p1p2.dot(p - p1);
return nk >= 0 && nk <= n2p1p2;
};
if (r2 == h2) return arc.contains(m) && in_segment(m); // tangent intersection
//if (p1 == p2) return false; // degenerate segment located inside circle
assert(p1 != p2);
tFloatPoint u = (tFloatPoint)p1p2 * sqrt((r2-h2)/n2p1p2); // displacement on (p1 p2) from m to one intersection point
tFloatPoint i1 = m + u;
if (arc.contains(i1) && in_segment(i1)) return true;
tFloatPoint i2 = m - u;
return arc.contains(i2) && in_segment(i2);
}
// ============================================================================
// compact storage of a configuration (64 bits)
// ============================================================================
struct sConfiguration {
unsigned partition;
unsigned folding;
explicit sConfiguration() {}
sConfiguration(unsigned partition, unsigned folding) : partition(partition), folding(folding) {}
// add a bend
sConfiguration bend(unsigned joint, int rotation) const
{
sConfiguration res;
unsigned joint_mask = 1 << joint;
res.partition = partition | joint_mask;
res.folding = folding;
if (rotation == -1) res.folding |= joint_mask;
return res;
}
// textual representation
string text(unsigned length) const
{
ostringstream res;
unsigned f = folding;
unsigned p = partition;
int segment_len = 1;
int direction = 1;
for (size_t i = 1; i != length; i++)
{
if (p & 1)
{
res << segment_len * direction << ',';
direction = (f & 1) ? -1 : 1;
segment_len = 1;
}
else segment_len++;
p >>= 1;
f >>= 1;
}
res << segment_len * direction;
return res.str();
}
// for final sorting
bool operator< (const sConfiguration& c) const
{
return (partition == c.partition) ? folding < c.folding : partition < c.partition;
}
};
// ============================================================================
// static snake geometry checking grid
// ============================================================================
typedef unsigned tConfId;
class tGrid {
vector<tConfId>point;
tConfId current;
size_t snake_len;
int min_x, max_x, min_y, max_y;
size_t x_size, y_size;
size_t raw_index(const tPoint& p) { bound_check(p); return (p.x - min_x) + (p.y - min_y) * x_size; }
void bound_check(const tPoint& p) const { assert(p.x >= min_x && p.x <= max_x && p.y >= min_y && p.y <= max_y); }
void set(const tPoint& p)
{
point[raw_index(p)] = current;
}
bool check(const tPoint& p)
{
if (point[raw_index(p)] == current) return false;
set(p);
return true;
}
public:
tGrid(int len) : current(-1), snake_len(len)
{
min_x = -max(len - 3, 0);
max_x = max(len - 0, 0);
min_y = -max(len - 1, 0);
max_y = max(len - 4, 0);
x_size = max_x - min_x + 1;
y_size = max_y - min_y + 1;
point.assign(x_size * y_size, current);
}
bool check(sConfiguration c)
{
current++;
tPoint d(1, 0);
tPoint p(0, 0);
set(p);
for (size_t i = 1; i != snake_len; i++)
{
p = p + d;
if (!check(p)) return false;
if (c.partition & 1) d = d.rotate((c.folding & 1) ? -1 : 1);
c.folding >>= 1;
c.partition >>= 1;
}
return check(p + d);
}
};
// ============================================================================
// snake ragdoll
// ============================================================================
class tSnakeDoll {
vector<tPoint>point; // snake geometry. Head at (0,0) pointing right
// allows to check for collision with the area swept by a rotating segment
struct rotatedSegment {
struct segment { tPoint a, b; };
tPoint org;
segment end;
tArc arc[3];
bool extra_arc; // see if third arc is needed
// empty constructor to avoid wasting time in vector initializations
rotatedSegment(){}
// copy constructor is mandatory for vectors *but* shall never be used, since we carefully pre-allocate vector memory
rotatedSegment(const rotatedSegment &){ assert(!"rotatedSegment should never have been copy-constructed"); }
// rotate a segment
rotatedSegment(tPoint pivot, int rotation, tPoint o1, tPoint o2)
{
arc[0] = tArc(pivot, o1, rotation);
arc[1] = tArc(pivot, o2, rotation);
tPoint middle;
extra_arc = closest_point_within(o1, o2, pivot, middle);
if (extra_arc) arc[2] = tArc(pivot, middle, rotation);
org = o1;
end = { o1.rotate(rotation, pivot), o2.rotate(rotation, pivot) };
}
// check if a segment intersects the area swept during rotation
bool intersects(tPoint p1, tPoint p2) const
{
auto print_arc = [&](int a) { tprintf("(%d,%d)(%d,%d) -> %d (%d,%d)[%f,%f]", p1.x, p1.y, p2.x, p2.y, a, arc[a].c.x, arc[a].c.y, arc[a].middle_vector.x, arc[a].middle_vector.y); };
if (p1 == org) return false; // pivot is the only point allowed to intersect
if (inter_seg_arc(p1, p2, arc[0]))
{
print_arc(0);
return true;
}
if (inter_seg_arc(p1, p2, arc[1]))
{
print_arc(1);
return true;
}
if (extra_arc && inter_seg_arc(p1, p2, arc[2]))
{
print_arc(2);
return true;
}
return false;
}
};
public:
sConfiguration configuration;
bool valid;
// holds results of a folding attempt
class snakeFolding {
friend class tSnakeDoll;
vector<rotatedSegment>segment; // rotated segments
unsigned joint;
int direction;
size_t i_rotate;
// pre-allocate rotated segments
void reserve(size_t length)
{
segment.clear(); // this supposedly does not release vector storage memory
segment.reserve(length);
}
// handle one segment rotation
void rotate(tPoint pivot, int rotation, tPoint o1, tPoint o2)
{
segment.emplace_back(pivot, rotation, o1, o2);
}
public:
// nothing done during construction
snakeFolding(unsigned size)
{
segment.reserve (size);
}
};
// empty default constructor to avoid wasting time in array/vector inits
tSnakeDoll() {}
// constructs ragdoll from compressed configuration
tSnakeDoll(unsigned size, unsigned generator, unsigned folding) : point(size), configuration(generator,folding)
{
tPoint direction(1, 0);
tPoint current = { 0, 0 };
size_t p = 0;
point[p++] = current;
for (size_t i = 1; i != size; i++)
{
current = current + direction;
if (generator & 1)
{
direction.rotate((folding & 1) ? -1 : 1);
point[p++] = current;
}
folding >>= 1;
generator >>= 1;
}
point[p++] = current;
point.resize(p);
}
// constructs the initial flat snake
tSnakeDoll(int size) : point(2), configuration(0,0), valid(true)
{
point[0] = { 0, 0 };
point[1] = { size, 0 };
}
// constructs a new folding with one added rotation
tSnakeDoll(const tSnakeDoll & parent, unsigned joint, int rotation, tGrid& grid)
{
// update configuration
configuration = parent.configuration.bend(joint, rotation);
// locate folding point
unsigned p_joint = joint+1;
tPoint pivot;
size_t i_rotate = 0;
for (size_t i = 1; i != parent.point.size(); i++)
{
unsigned len = parent.point[i].manhattan_distance(parent.point[i - 1]);
if (len > p_joint)
{
pivot = parent.point[i - 1] + ((parent.point[i] - parent.point[i - 1]) / len) * p_joint;
i_rotate = i;
break;
}
else p_joint -= len;
}
// rotate around joint
snakeFolding fold (parent.point.size() - i_rotate);
fold.rotate(pivot, rotation, pivot, parent.point[i_rotate]);
for (size_t i = i_rotate + 1; i != parent.point.size(); i++) fold.rotate(pivot, rotation, parent.point[i - 1], parent.point[i]);
// copy unmoved points
point.resize(parent.point.size()+1);
size_t i;
for (i = 0; i != i_rotate; i++) point[i] = parent.point[i];
// copy rotated points
for (; i != parent.point.size(); i++) point[i] = fold.segment[i - i_rotate].end.a;
point[i] = fold.segment[i - 1 - i_rotate].end.b;
// static configuration check
valid = grid.check (configuration);
// check collisions with swept arcs
if (valid && parent.valid) // ;!; parent.valid test is temporary
{
for (const rotatedSegment & s : fold.segment)
for (size_t i = 0; i != i_rotate; i++)
{
if (s.intersects(point[i+1], point[i]))
{
//printf("! %s => %s\n", parent.trace().c_str(), trace().c_str());//;!;
valid = false;
break;
}
}
}
}
// trace
string trace(void) const
{
size_t len = 0;
for (size_t i = 1; i != point.size(); i++) len += point[i - 1].manhattan_distance(point[i]);
return configuration.text(len);
}
};
// ============================================================================
// snake twisting engine
// ============================================================================
class cSnakeFolder {
int length;
unsigned num_joints;
tGrid grid;
// filter redundant configurations
bool is_unique (sConfiguration c)
{
unsigned reverse_p = bit_reverse(c.partition, num_joints);
if (reverse_p < c.partition)
{
tprintf("P cut %s\n", c.text(length).c_str());
return false;
}
else if (reverse_p == c.partition) // filter redundant foldings
{
unsigned first_joint_mask = c.partition & (-c.partition); // insulates leftmost bit
unsigned reverse_f = bit_reverse(c.folding, num_joints);
if (reverse_f & first_joint_mask) reverse_f = ~reverse_f & c.partition;
if (reverse_f > c.folding)
{
tprintf("F cut %s\n", c.text(length).c_str());
return false;
}
}
return true;
}
// recursive folding
void fold(tSnakeDoll snake, unsigned first_joint)
{
// count unique configurations
if (snake.valid && is_unique(snake.configuration)) num_configurations++;
// try to bend remaining joints
for (size_t joint = first_joint; joint != num_joints; joint++)
{
// right bend
tprintf("%s -> %s\n", snake.configuration.text(length).c_str(), snake.configuration.bend(joint,1).text(length).c_str());
fold(tSnakeDoll(snake, joint, 1, grid), joint + 1);
// left bend, except for the first joint
if (snake.configuration.partition != 0)
{
tprintf("%s -> %s\n", snake.configuration.text(length).c_str(), snake.configuration.bend(joint, -1).text(length).c_str());
fold(tSnakeDoll(snake, joint, -1, grid), joint + 1);
}
}
}
public:
// count of found configurations
unsigned num_configurations;
// constructor does all the work :)
cSnakeFolder(int n) : length(n), grid(n), num_configurations(0)
{
num_joints = length - 1;
// launch recursive folding
fold(tSnakeDoll(length), 0);
}
};
// ============================================================================
// here we go
// ============================================================================
int main(int argc, char * argv[])
{
#ifdef NDEBUG
if (argc != 2) panic("give me a snake length or else");
int length = atoi(argv[1]);
#else
(void)argc; (void)argv;
int length = 12;
#endif // NDEBUG
if (length <= 0 || length >= MAX_LENGTH) panic("a snake of that length is hardly foldable");
time_t start = time(NULL);
cSnakeFolder snakes(length);
time_t duration = time(NULL) - start;
printf ("Found %d configuration%c of length %d in %lds\n", snakes.num_configurations, (snakes.num_configurations == 1) ? '\0' : 's', length, duration);
return 0;
}
Construyendo el ejecutable
Compile con
Yo uso MinGW bajo Win7 con g ++ 4.8 para compilaciones "linux", por lo que la portabilidad no está 100% garantizada.g++ -O3 -std=c++11
También funciona (más o menos) con un proyecto estándar MSVC2013
Al no definir NDEBUG
, obtiene rastros de la ejecución del algoritmo y un resumen de las configuraciones encontradas.
Actuaciones
con o sin tablas hash, el compilador de Microsoft funciona miserablemente: la compilación de g ++ es 3 veces más rápida .
El algoritmo prácticamente no usa memoria.
Dado que la verificación de colisión es aproximadamente en O (n), el tiempo de cálculo debe estar en O (nk n ), con k ligeramente inferior a 3.
En mi i3-2100@3.1GHz, n = 17 toma aproximadamente 1:30 (aproximadamente 2 millones serpientes / minuto).
No he terminado de optimizar, pero no esperaría más que una ganancia x3, así que básicamente puedo esperar alcanzar quizás n = 20 en menos de una hora, o n = 24 en menos de un día.
Alcanzar la primera forma imposible de doblar conocida (n = 31) tomaría entre unos años y una década, suponiendo que no haya cortes de energía.
Contando formas
Una serpiente de tamaño N tiene articulaciones N-1 .
Cada articulación puede dejarse recta o doblada hacia la izquierda o hacia la derecha (3 posibilidades).
El número de plegamientos posibles es, por lo tanto, 3 N-1 .
Las colisiones reducirán un poco ese número, por lo que el número real es cercano a 2.7 N-1
Sin embargo, muchos de estos pliegues conducen a formas idénticas.
dos formas son idénticas si hay una rotación o una simetría que puede transformar una en la otra.
Definamos un segmento como cualquier parte recta del cuerpo plegado.
Por ejemplo, una serpiente de tamaño 5 doblada en la segunda articulación tendría 2 segmentos (uno de 2 unidades de largo y el segundo de 3 unidades de largo).
El primer segmento se llamará cabeza y la última cola .
Por convención, orientamos la cabeza de las serpientes horizontalmente con el cuerpo apuntando hacia la derecha (como en la primera figura del OP).
Designamos una figura dada con una lista de longitudes de segmento con signo, con longitudes positivas que indican un plegado a la derecha y negativas a un plegado a la izquierda.
La longitud inicial es positiva por convención.
Separar segmentos y curvas
Si consideramos solo las diferentes formas en que una serpiente de longitud N puede dividirse en segmentos, terminamos con un reparto idéntico a las composiciones de N.
Usando el mismo algoritmo que se muestra en la página wiki, es fácil generar todas las 2 particiones N-1 posibles de la serpiente.
Cada partición a su vez generará todos los plegamientos posibles aplicando curvas a la izquierda o derecha a todas sus uniones. Uno de estos plegamientos se llamará configuración .
Todas las particiones posibles se pueden representar mediante un número entero de N-1 bits, donde cada bit representa la presencia de una unión. Llamaremos a este entero un generador .
Particiones de poda
Al notar que doblar una partición dada desde la cabeza hacia abajo es equivalente a doblar la partición simétrica desde la cola hacia arriba, podemos encontrar todas las parejas de particiones simétricas y eliminar una de cada dos.
El generador de una partición simétrica es el generador de la partición escrito en orden de bits inverso, que es trivialmente fácil y barato de detectar.
Esto eliminará casi la mitad de las particiones posibles, con la excepción de las particiones con generadores "palindrómicos" que no se modifican por la inversión de bits (por ejemplo, 00100100).
Cuidando las simetrías horizontales
Con nuestras convenciones (una serpiente comienza a apuntar a la derecha), la primera curva aplicada a la derecha producirá una familia de pliegues que serán simétricos horizontales de los que difieren solo en la primera curva.
Si decidimos que la primera curva siempre estará a la derecha, eliminaremos todas las simétricas horizontales de una sola vez.
Limpiando los palíndromos
Estos dos cortes son eficientes, pero no suficientes para cuidar estos molestos palíndromos.
La verificación más exhaustiva en el caso general es la siguiente:
considere una configuración C con una partición palindrómica.
- Si invertimos cada curva en C, terminamos con una simétrica horizontal de C.
- si invertimos C (aplicando curvas desde la cola hacia arriba), obtenemos la misma figura rotada a la derecha
- Si ambos invertimos e invertimos C, obtenemos la misma figura rotada a la izquierda.
Podríamos verificar cada nueva configuración contra las otras 3. Sin embargo, dado que ya generamos solo configuraciones que comienzan con un giro a la derecha, solo tenemos una simetría posible para verificar:
- la C invertida comenzará con un giro a la izquierda, que por construcción es imposible de duplicar
- fuera de las configuraciones invertidas e invertidas invertidas, solo una comenzará con un giro a la derecha.
Esa es la única configuración que posiblemente podemos duplicar.
Eliminando duplicados sin almacenamiento
Mi enfoque inicial fue almacenar todas las configuraciones en una gran tabla hash, para eliminar duplicados al verificar la presencia de una configuración simétrica previamente calculada.
Gracias al artículo mencionado anteriormente, quedó claro que, dado que las particiones y los pliegues se almacenan como campos de bits, se pueden comparar como cualquier valor numérico.
Entonces, para eliminar un miembro de un par simétrico, simplemente puede comparar ambos elementos y mantener sistemáticamente el más pequeño (o el más grande, como desee).
Por lo tanto, probar una configuración para duplicación equivale a calcular la partición simétrica, y si ambas son idénticas, el plegamiento. No se necesita memoria en absoluto.
Orden de generación
Claramente, la verificación de colisión será la parte que más tiempo requiera, por lo que reducir estos cálculos es un gran ahorro de tiempo.
Una posible solución es tener una "serpiente de muñeca de trapo" que comenzará en una configuración plana y se doblará gradualmente, para evitar volver a calcular toda la geometría de la serpiente para cada configuración posible.
Al elegir el orden en el que se prueban las configuraciones, de modo que como máximo se almacene una muñeca de trapo para cada número total de uniones, podemos limitar el número de instancias a N-1.
Utilizo una exploración recursiva del sake desde la cola hacia abajo, agregando una sola articulación en cada nivel. Por lo tanto, se crea una nueva instancia de ragdoll sobre la configuración principal, con una sola curva adicional.
Esto significa que las curvas se aplican en un orden secuencial, lo que parece ser suficiente para evitar auto colisiones en casi todos los casos.
Cuando se detecta la auto-colisión, las curvas que conducen al movimiento ofensivo se aplican en todas las órdenes posibles hasta que se encuentre un plegado legítimo o se agoten todas las combinaciones.
Control estático
Antes incluso de pensar en las partes móviles, me pareció más eficiente probar la forma final estática de una serpiente para autointercepciones.
Esto se hace dibujando la serpiente en una cuadrícula. Cada punto posible se traza desde la cabeza hacia abajo. Si hay una auto-intersección, al menos un par de puntos caerán en la misma ubicación. Esto requiere exactamente N parcelas para cualquier configuración de serpiente, durante un tiempo constante de O (N).
La principal ventaja de este enfoque es que la prueba estática por sí sola simplemente seleccionará rutas válidas de auto evitación en una red cuadrada, lo que permite probar todo el algoritmo al inhibir la detección de colisión dinámica y asegurarse de que encontremos el recuento correcto de tales rutas.
Control dinámico
Cuando una serpiente se pliega alrededor de una articulación, cada segmento girado barrerá un área cuya forma es cualquier cosa menos trivial.
Claramente, puede verificar las colisiones probando la inclusión en todas las áreas barridas individualmente. Una verificación global sería más eficiente, pero dada la complejidad de las áreas no puedo pensar en ninguna (excepto tal vez usando una GPU para dibujar todas las áreas y realizar una verificación de impacto global).
Dado que la prueba estática se ocupa de las posiciones inicial y final de cada segmento, solo necesitamos verificar las intersecciones con los arcos barridos por cada segmento giratorio.
Después de una interesante discusión con trichoplax y un poco de JavaScript para orientarme, se me ocurrió este método:
Para tratar de ponerlo en pocas palabras, si llamas
- C el centro de rotación,
- S un segmento giratorio de longitud arbitraria y dirección que no contiene C ,
- L la línea que prolonga S
- H la línea ortogonal a L que pasa por C ,
- I la intersección de L y H ,
(fuente: free.fr )
Para cualquier segmento que no contenga I , el área barrida está unida por 2 arcos (y 2 segmentos ya atendidos por la verificación estática).
Si I cae dentro del segmento, el arco barrido por I también debe tenerse en cuenta.
Esto significa que podemos verificar cada segmento inmóvil contra cada segmento giratorio con 2 o 3 intersecciones de segmento con arco
Usé geometría vectorial para evitar las funciones trigonométricas por completo.
Las operaciones vectoriales producen código compacto y (relativamente) legible.
La intersección de segmento a arco requiere un vector de punto flotante, pero la lógica debe ser inmune a los errores de redondeo.
Encontré esta solución elegante y eficiente en una oscura publicación del foro. Me pregunto por qué no se publicita más ampliamente.
¿Funciona?
La inhibición de la detección dinámica de colisión produce el recuento correcto de rutas de autoevaluación hasta n = 19, por lo que estoy bastante seguro de que el diseño global funciona.
La detección dinámica de colisión produce resultados consistentes, aunque falta la comprobación de curvas en diferente orden (por ahora).
Como consecuencia, el programa cuenta las serpientes que pueden doblarse de la cabeza hacia abajo (es decir, con las articulaciones plegadas en orden de distancia creciente de la cabeza).