C ++ 11 - funktioniert fast :)
Nachdem ich diesen Artikel gelesen hatte , sammelte ich ein bisschen Weisheit von dem Typ, der anscheinend 25 Jahre lang an dem weniger komplizierten Problem gearbeitet hatte, selbstvermeidende Pfade auf einem quadratischen Gitter zu zählen.
#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;
}
Erstellen der ausführbaren Datei
Kompilieren mit
Ich verwende MinGW unter Win7 mit g ++ 4.8 für "Linux" -Builds, sodass die Portabilität nicht zu 100% garantiert ist.g++ -O3 -std=c++11
Es funktioniert auch (irgendwie) mit einem Standard-MSVC2013-Projekt
Durch Aufheben der Definition erhalten NDEBUG
Sie Spuren der Algorithmusausführung und eine Zusammenfassung der gefundenen Konfigurationen.
Aufführungen
Mit oder ohne Hash-Tabellen arbeitet der Microsoft-Compiler miserabel: Die Erstellung von g ++ ist dreimal schneller .
Der Algorithmus verwendet praktisch keinen Speicher.
Da die Kollisionsprüfung ungefähr in O (n) liegt, sollte die Rechenzeit in O (nk n ) liegen, wobei k etwas niedriger als 3 ist.
Auf meinem i3-2100@3.1GHz dauert n = 17 ungefähr 1:30 (ungefähr 2 Millionen) Schlangen / Minute).
Ich bin noch nicht mit der Optimierung fertig, aber ich würde nicht mehr als einen x3-Gewinn erwarten, also kann ich im Grunde hoffen, unter einer Stunde vielleicht n = 20 oder unter einem Tag n = 24 zu erreichen.
Das Erreichen der ersten bekannten nicht biegbaren Form (n = 31) würde zwischen einigen Jahren und einem Jahrzehnt dauern, sofern keine Stromausfälle vorliegen.
Formen zählen
Eine Schlange der Größe N hat N-1- Gelenke.
Jedes Gelenk kann gerade gelassen oder nach links oder rechts gebogen werden (3 Möglichkeiten).
Die Anzahl der möglichen Faltungen beträgt somit 3 N-1 .
Kollisionen reduzieren diese Zahl etwas, sodass die tatsächliche Zahl nahe bei 2,7 N-1 liegt
Viele solcher Faltungen führen jedoch zu identischen Formen.
Zwei Formen sind identisch, wenn es entweder eine Rotation oder eine Symmetrie gibt , die sich ineinander verwandeln kann.
Definieren wir ein Segment als einen geraden Teil des gefalteten Körpers.
Zum Beispiel würde eine Schlange der Größe 5, die am 2. Gelenk gefaltet ist, 2 Segmente haben (eines 2 Einheiten lang und das zweite 3 Einheiten lang).
Das erste Segment wird Kopf und der letzte Schwanz genannt .
Konventionell richten wir den Kopf der Schlangen horizontal aus, wobei der Körper nach rechts zeigt (wie in der ersten Abbildung des OP).
Wir bezeichnen eine gegebene Figur mit einer Liste vorzeichenbehafteter Segmentlängen, wobei positive Längen eine rechte Faltung und negative eine linke Faltung anzeigen.
Die Anfangslänge ist gemäß Konvention positiv.
Segmente und Biegungen trennen
Wenn wir nur die verschiedenen Möglichkeiten betrachten, wie eine Schlange der Länge N in Segmente aufgeteilt werden kann, erhalten wir eine Aufteilung, die mit den Zusammensetzungen von N identisch ist .
Mit dem gleichen Algorithmus wie auf der Wiki-Seite gezeigt, ist es einfach, alle 2 N-1 möglichen Partitionen der Schlange zu generieren .
Jede Trennwand erzeugt wiederum alle möglichen Faltungen, indem entweder alle ihre Gelenke nach links oder rechts gebogen werden. Eine solche Faltung wird als Konfiguration bezeichnet .
Alle möglichen Partitionen können durch eine ganze Zahl von N-1 Bits dargestellt werden, wobei jedes Bit das Vorhandensein einer Verbindung darstellt. Wir werden diese ganze Zahl einen Generator nennen .
Trennwände
Wenn wir feststellen, dass das Biegen einer bestimmten Partition vom Kopf nach unten dem Biegen der symetrischen Partition vom Schwanz nach oben entspricht, können wir alle Paare symetrischer Partitionen finden und eins von zwei eliminieren.
Der Generator einer symmetrischen Partition ist der Generator der Partition, der in umgekehrter Bitreihenfolge geschrieben ist, was trivial einfach und billig zu erkennen ist.
Dadurch wird fast die Hälfte der möglichen Partitionen eliminiert, mit Ausnahme der Partitionen mit "palindromischen" Generatoren, die durch Bitumkehr unverändert bleiben (z. B. 00100100).
Auf horizontale Symetrien achten
Mit unseren Konventionen (eine Schlange zeigt nach rechts) erzeugt die allererste Biegung, die nach rechts angewendet wird, eine Familie von Faltungen, die horizontal symmetrisch zu denen sind, die sich nur durch die erste Biegung unterscheiden.
Wenn wir entscheiden, dass die erste Biegung immer rechts ist, eliminieren wir alle horizontalen Symmetrien auf einen Schlag.
Palindrome aufwischen
Diese beiden Schnitte sind effizient, aber nicht genug, um diese lästigen Palindrome zu behandeln.
Die gründlichste Überprüfung im allgemeinen Fall ist wie folgt:
Betrachten Sie eine Konfiguration C mit einer palindromischen Partition.
- Wenn wir jede Biegung in C invertieren , erhalten wir eine horizontale Symmetrie von C.
- Wenn wir C umkehren (Biegen vom Schwanz nach oben anwenden), wird dieselbe Figur nach rechts gedreht
- Wenn wir beide C umkehren und umkehren, wird dieselbe Figur nach links gedreht.
Wir könnten jede neue Konfiguration mit den 3 anderen vergleichen. Da wir jedoch bereits Konfigurationen generieren, die mit einer Rechtskurve beginnen, müssen wir nur eine mögliche Symmetrie überprüfen:
- Das umgekehrte C beginnt mit einer Linkskurve, die konstruktionsbedingt nicht dupliziert werden kann
- Von den umgekehrten und invertierten Umkehrkonfigurationen beginnt nur eine mit einer Rechtskurve.
Dies ist die einzige Konfiguration, die wir möglicherweise duplizieren können.
Eliminieren von Duplikaten ohne Speicher
Mein ursprünglicher Ansatz bestand darin, alle Konfigurationen in einer riesigen Hash-Tabelle zu speichern, um Duplikate zu beseitigen, indem ich das Vorhandensein einer zuvor berechneten symetrischen Konfiguration überprüfe.
Dank des oben genannten Artikels wurde klar, dass Partitionen und Faltungen, die als Bitfelder gespeichert sind, wie jeder numerische Wert verglichen werden können.
Um ein Mitglied eines symetrischen Paares zu eliminieren, können Sie einfach beide Elemente vergleichen und systematisch das kleinste (oder das größte, wie Sie möchten) beibehalten.
Das Testen einer Konfiguration auf Duplizierung bedeutet also, die symetrische Partition und, wenn beide identisch sind, die Faltung zu berechnen. Es wird überhaupt kein Speicher benötigt.
Reihenfolge der Erzeugung
Die Kollisionsprüfung ist eindeutig der zeitaufwändigste Teil, daher ist die Reduzierung dieser Berechnungen eine große Zeitersparnis.
Eine mögliche Lösung besteht darin, eine "Ragdoll-Schlange" zu haben, die in einer flachen Konfiguration beginnt und allmählich gebogen wird, um zu vermeiden, dass die gesamte Schlangengeometrie für jede mögliche Konfiguration neu berechnet wird.
Durch Auswahl der Reihenfolge, in der die Konfigurationen getestet werden, sodass höchstens eine Stoffpuppe für jede Gesamtzahl von Gelenken gespeichert wird, können wir die Anzahl der Instanzen auf N-1 begrenzen.
Ich benutze einen rekursiven Scan des Sake vom Schwanz abwärts und füge auf jeder Ebene ein einzelnes Gelenk hinzu. Daher wird eine neue Ragdoll-Instanz auf der übergeordneten Konfiguration mit einer einzigen zusätzlichen Biegung erstellt.
Dies bedeutet, dass Biegungen in einer sequentiellen Reihenfolge angewendet werden, was in fast allen Fällen ausreicht, um Selbstkollisionen zu vermeiden.
Wenn eine Selbstkollision erkannt wird, werden die Biegungen, die zu der beleidigenden Bewegung führen, in allen möglichen Reihenfolgen angewendet, bis eine legitime Faltung gefunden wird oder alle Kombinationen erschöpft sind.
Statische Prüfung
Bevor ich überhaupt über bewegliche Teile nachdachte, fand ich es effizienter, die statische Endform einer Schlange auf Selbstüberschneidungen zu testen.
Dies geschieht durch Zeichnen der Schlange auf einem Gitter. Jeder mögliche Punkt wird vom Kopf abwärts aufgezeichnet. Wenn es eine Selbstüberschneidung gibt, fallen mindestens zwei Punkte auf dieselbe Stelle. Dies erfordert genau N Diagramme für jede Schlangenkonfiguration für eine konstante O (N) -Zeit.
Der Hauptvorteil dieses Ansatzes besteht darin, dass der statische Test allein einfach gültige selbstvermeidende Pfade auf einem quadratischen Gitter auswählt, wodurch der gesamte Algorithmus getestet werden kann, indem die dynamische Kollisionserkennung verhindert und sichergestellt wird, dass die richtige Anzahl solcher Pfade gefunden wird.
Dynamische Prüfung
Wenn sich eine Schlange um ein Gelenk faltet, fegt jedes gedrehte Segment einen Bereich, dessen Form alles andere als trivial ist.
Natürlich können Sie Kollisionen überprüfen, indem Sie die Einbeziehung in alle diese überstrichenen Bereiche einzeln testen. Eine globale Überprüfung wäre effizienter, aber angesichts der Komplexität der Bereiche kann ich mir keine vorstellen (außer vielleicht die Verwendung einer GPU, um alle Bereiche zu zeichnen und eine globale Trefferprüfung durchzuführen).
Da der statische Test die Start- und Endpositionen jedes Segments berücksichtigt, müssen wir nur die Schnittpunkte mit dem überprüfen Bögendie von jedem rotierenden Segment überstrichen werden.
Nach einer interessanten Diskussion mit Trichoplax und ein bisschen JavaScript , um mich zu orientieren, kam ich auf diese Methode:
Um zu versuchen, es in ein paar Worten auszudrücken, wenn Sie anrufen
- C. das Rotationszentrum,
- S ein rotierendes Segment beliebiger Länge und Richtung, das nicht enthält C ,
- L die Linie verlängert sich S.
- H die zu L orthogonale Linie durch C. ,
- I der Schnittpunkt von L und H ,
(Quelle: free.fr )
Für jedes Segment, das nicht enthält I , wird der überstrichene Bereich durch 2 Bögen begrenzt (und 2 Segmente, die bereits durch die statische Prüfung ).
Wenn ich in das Segment falle, muss auch der von I überstrichene Bogen berücksichtigt werden.
Dies bedeutet, dass wir jedes unbewegliche Segment gegen jedes rotierende Segment mit 2 oder 3 Segment-mit-Bogen-Schnittpunkten prüfen können
Ich habe die Vektorgeometrie verwendet, um trigonometrische Funktionen insgesamt zu vermeiden.
Vektoroperationen erzeugen kompakten und (relativ) lesbaren Code.
Der Schnittpunkt von Segment zu Bogen erfordert einen Gleitkomma-Vektor, die Logik sollte jedoch immun gegen Rundungsfehler sein.
Ich fand diese elegante und effiziente Lösung in einem obskuren Forumsbeitrag. Ich frage mich, warum es nicht weiter verbreitet ist.
Funktioniert es?
Das Sperren der dynamischen Kollisionserkennung führt zu einer korrekten Anzahl von Pfaden mit Selbstvermeidung bis zu n = 19, daher bin ich ziemlich sicher, dass das globale Layout funktioniert.
Die dynamische Kollisionserkennung führt zu konsistenten Ergebnissen, obwohl die Überprüfung von Biegungen in unterschiedlicher Reihenfolge (vorerst) fehlt.
Infolgedessen zählt das Programm Schlangen, die vom Kopf nach unten gebogen werden können (dh mit gefalteten Gelenken in der Reihenfolge des zunehmenden Abstands vom Kopf).