Il numero di orientamenti per serpenti raggiungibili


11

Questa sfida non riguarda il gioco Snake.

Immagina un serpente 2d formato disegnando una linea orizzontale di lunghezza n . In punti interi lungo il suo corpo, questo serpente può ruotare il suo corpo di 90 gradi. Se per prima cosa definiamo la parte anteriore del serpente che si trova all'estrema sinistra, la rotazione sposta la parte posteriore del serpente e la parte anteriore rimarrà in posizione. Eseguendo rotazioni ripetute può creare molte forme diverse del corpo del serpente.

Regole

  1. Una parte del corpo del serpente non può sovrapporsi a un'altra.
  2. Deve essere possibile raggiungere l'orientamento finale senza che parti del corpo del serpente si sovrappongano nel mezzo. Due punti che toccano vengono considerati come sovrapposti in questo problema.
  3. Considero un serpente e il suo rovescio come la stessa forma.

Compito

Fino alla rotazione, alla traduzione e alla simmetria dello specchio, qual è il numero totale di diverse forme del corpo del serpente che possono essere fatte?

Esempio di rotazione di una parte del corpo del serpente. Immagina n=10e il serpente è nel suo orientamento iniziale di una linea retta. Ora ruota di 490 gradi in senso antiorario. Otteniamo il serpente dal 4al 10(la coda del serpente) giace verticalmente e il serpente da 0a 4posizione orizzontale. Il serpente ora ha un angolo retto nel suo corpo.

Ecco alcuni esempi grazie a Martin Büttner.

Iniziamo con il serpente orizzontale.

inserisci qui la descrizione dell'immagine

Ora ruotiamo dalla posizione 4.

inserisci qui la descrizione dell'immagine

Finiamo dopo la rotazione in questo orientamento.

inserisci qui la descrizione dell'immagine

Ora consideriamo questo orientamento di un serpente diverso.

inserisci qui la descrizione dell'immagine

Ora possiamo vedere una mossa illegale in cui si verificherebbe una sovrapposizione durante la rotazione.

esempio di collisione

Punto

Il tuo punteggio è il più grande nper il quale il tuo codice può risolvere il problema in meno di un minuto sul mio computer.

Quando si verifica una rotazione, con essa si sposterà metà del serpente. Dobbiamo preoccuparci se una qualsiasi di questa parte che viene ruotata potrebbe sovrapporsi a una parte del serpente durante la rotazione. Per semplicità possiamo supporre che il serpente abbia larghezza zero. Puoi ruotare solo in un determinato punto del serpente fino a 90 gradi in senso orario o antiorario. Perché, non puoi mai piegare completamente il serpente in due poiché ciò avrebbe comportato due rotazioni nello stesso punto nella stessa direzione.

Forme che non possono essere create

Un semplice esempio di una forma che non può essere fatta è un capitale T. Una versione più sofisticata è

inserisci qui la descrizione dell'immagine

(Grazie a Harald Hanche-Olsen per questo esempio)

In questo esempio tutte le linee orizzontali adiacenti sono separate da 1 come quelle verticali. Non vi è quindi alcun passaggio legale da questa posizione e poiché il problema è reversibile, non è quindi possibile arrivarci dalla posizione iniziale.

Lingue e biblioteche

Puoi usare qualsiasi lingua che abbia un compilatore / interprete / ecc. Liberamente disponibile. per Linux e tutte le librerie che sono anche disponibili gratuitamente per Linux.

La mia macchina I tempi verranno eseguiti sulla mia macchina. Questa è un'installazione ubuntu standard su un processore a otto core AMD FX-8350. Questo significa anche che devo essere in grado di eseguire il tuo codice. Di conseguenza, utilizzare solo software gratuito facilmente disponibile e includere istruzioni complete su come compilare ed eseguire il codice.


1
@TApicella Grazie per le domande. Quando dico "Quando si verifica una rotazione si sposterà la metà del serpente con esso" Non intendevo il 50 percento. Mi riferivo solo alla parte prima del punto di rotazione e alla parte successiva. Se ruoti da 0 lungo il serpente, ruoti tutto!

2
@TApicella Informazioni sulla tua seconda domanda. Il punto è che non c'è rotazione legale dalla posizione che ho dato. Se fosse raggiungibile, deve essere possibile tornare all'orientamento orizzontale iniziale mediante una sequenza di rotazioni (il contrario di quelle che avresti preso per arrivare all'orientamento finale). Puoi descrivere una rotazione legale che pensi di poter fare? da questa posizione? Per essere chiari, il serpente non cresce. Rimane sempre la stessa lunghezza per tutto.

3
@TApicella Sembra che ti aspetti che il serpente cresca. La sua dimensione è fissa però. Inizi con un lungo serpente e tutto ciò che ti è permesso di fare è piegare parti di esso di 90 gradi. Dalla posizione corrente non è possibile applicare alcuna piega che porterebbe a una fase precedente del serpente.
Martin Ender,

1
Puoi piegare in un punto più di una volta (avanti e indietro)? Se puoi, questo lo rende piuttosto complesso.
randomra,

1
@randomra Puoi davvero fintanto che non vai mai oltre i novanta gradi da dritto.

Risposte:


5

Python 3 - punteggio provvisorio: n = 11 (n = 13 con PyPy *)

Poiché non ci sono state risposte nella prima settimana, ecco un esempio in Python per incoraggiare la concorrenza. Ho cercato di renderlo ragionevolmente leggibile in modo che le inefficienze possano essere facilmente identificate per dare idee per altre risposte.

Approccio

  • Inizia con il serpente dritto e trova tutte le posizioni che possono essere legalmente raggiunte in una mossa.
  • Trova tutte le posizioni che possono essere legalmente raggiunte da quelle posizioni, che non sono già state identificate.
  • Ripeti fino a quando non ne trovi più, e restituisci il numero di posizioni trovate del tutto.

Codice

(ora con alcuni test e asserzioni dopo il mio primo tentativo errato)

'''
Snake combinations

A snake is represented by a tuple giving the relative orientation at each joint.
A length n snake has n-1 joints.
Each relative orientation is one of the following:

0: Clockwise 90 degrees
1: Straight
2: Anticlockwise 90 degrees

So a straight snake of length 4 has 3 joints all set to 1:

(1, 1, 1)

x increases to the right
y increases upwards

'''


import turtle


def all_coords(state):
    '''Return list of coords starting from (0,0) heading right.'''
    current = (1, 0)
    heading = 0
    coords = [(0,0), (1,0)]
    for item in state:
        heading += item + 3
        heading %= 4
        offset = ((1,0), (0,1), (-1,0), (0,-1))[heading]
        current = tuple(current[i]+offset[i] for i in (0,1))
        coords.append(current)
    return coords


def line_segments(coords, pivot):
    '''Return list of line segments joining consecutive coords up to pivot-1.'''
    return [(coords[i], coords[i+1]) for i in range(pivot+1)]


def rotation_direction(coords, pivot, coords_in_final_after_pivot):
    '''Return -1 if turning clockwise, 1 if turning anticlockwise.'''
    pivot_coord = coords[pivot + 1]
    initial_coord = coords[pivot + 2]
    final_coord = coords_in_final_after_pivot[0]
    initial_direction = tuple(initial_coord[i] - pivot_coord[i] for i in (0,1))
    final_direction = tuple(final_coord[i] - pivot_coord[i] for i in (0,1))
    return (initial_direction[0] * final_direction[1] -
            initial_direction[1] * final_direction[0]
            )


def intersects(arc, line):
    '''Return True if the arc intersects the line segment.'''
    if line_segment_cuts_circle(arc, line):
        cut_points = points_cutting_circle(arc, line)
        if cut_points and cut_point_is_on_arc(arc, cut_points):
            return True


def line_segment_cuts_circle(arc, line):
    '''Return True if the line endpoints are not both inside or outside.'''
    centre, point, direction = arc
    start, finish = line
    point_distance_squared = distance_squared(centre, point)
    start_distance_squared = distance_squared(centre, start)
    finish_distance_squared = distance_squared(centre, finish)
    start_sign = start_distance_squared - point_distance_squared
    finish_sign = finish_distance_squared - point_distance_squared
    if start_sign * finish_sign <= 0:
        return True


def distance_squared(centre, point):
    '''Return the square of the distance between centre and point.'''
    return sum((point[i] - centre[i]) ** 2 for i in (0,1))


def cut_point_is_on_arc(arc, cut_points):
    '''Return True if any intersection point with circle is on arc.'''
    centre, arc_start, direction = arc
    relative_start = tuple(arc_start[i] - centre[i] for i in (0,1))
    relative_midpoint = ((relative_start[0] - direction*relative_start[1])/2,
                         (relative_start[1] + direction*relative_start[0])/2
                         )
    span_squared = distance_squared(relative_start, relative_midpoint)
    for cut_point in cut_points:
        relative_cut_point = tuple(cut_point[i] - centre[i] for i in (0,1))
        spacing_squared = distance_squared(relative_cut_point,
                                           relative_midpoint
                                           )
        if spacing_squared <= span_squared:
            return True


def points_cutting_circle(arc, line):
    '''Return list of points where line segment cuts circle.'''
    points = []
    start, finish = line
    centre, arc_start, direction = arc
    radius_squared = distance_squared(centre, arc_start)
    length_squared = distance_squared(start, finish)
    relative_start = tuple(start[i] - centre[i] for i in (0,1))
    relative_finish = tuple(finish[i] - centre[i] for i in (0,1))
    relative_midpoint = tuple((relative_start[i] +
                               relative_finish[i]
                               )*0.5 for i in (0,1))
    determinant = (relative_start[0]*relative_finish[1] -
                   relative_finish[0]*relative_start[1]
                   )
    determinant_squared = determinant ** 2
    discriminant = radius_squared * length_squared - determinant_squared
    offset = tuple(finish[i] - start[i] for i in (0,1))
    sgn = (1, -1)[offset[1] < 0]
    root_discriminant = discriminant ** 0.5
    one_over_length_squared = 1 / length_squared
    for sign in (-1, 1):
        x = (determinant * offset[1] +
             sign * sgn * offset[0] * root_discriminant
             ) * one_over_length_squared
        y = (-determinant * offset[0] +
             sign * abs(offset[1]) * root_discriminant
             ) * one_over_length_squared
        check = distance_squared(relative_midpoint, (x,y))
        if check <= length_squared * 0.25:
            points.append((centre[0] + x, centre[1] + y))
    return points


def potential_neighbours(candidate):
    '''Return list of states one turn away from candidate.'''
    states = []
    for i in range(len(candidate)):
        for orientation in range(3):
            if abs(candidate[i] - orientation) == 1:
                state = list(candidate)
                state[i] = orientation
                states.append(tuple(state))
    return states


def reachable(initial, final):
    '''
    Return True if final state can be reached in one legal move.

    >>> reachable((1,0,0), (0,0,0))
    False

    >>> reachable((0,1,0), (0,0,0))
    False

    >>> reachable((0,0,1), (0,0,0))
    False

    >>> reachable((1,2,2), (2,2,2))
    False

    >>> reachable((2,1,2), (2,2,2))
    False

    >>> reachable((2,2,1), (2,2,2))
    False

    >>> reachable((1,2,1,2,1,1,2,2,1), (1,2,1,2,1,1,2,1,1))
    False

    '''
    pivot = -1
    for i in range(len(initial)):
        if initial[i] != final[i]:
            pivot = i
            break

    assert pivot > -1, '''
        No pivot between {} and {}'''.format(initial, final)
    assert initial[pivot + 1:] == final[pivot + 1:], '''
        More than one pivot between {} and {}'''.format(initial, final)

    coords_in_initial = all_coords(initial)
    coords_in_final_after_pivot = all_coords(final)[pivot+2:]
    coords_in_initial_after_pivot = coords_in_initial[pivot+2:]
    line_segments_up_to_pivot = line_segments(coords_in_initial, pivot)

    direction = rotation_direction(coords_in_initial,
                                   pivot,
                                   coords_in_final_after_pivot
                                   )

    pivot_point = coords_in_initial[pivot + 1]

    for point in coords_in_initial_after_pivot:
        arc = (pivot_point, point, direction)
        if any(intersects(arc, line) for line in line_segments_up_to_pivot):
            return False
    return True


def display(snake):
    '''Display a line diagram of the snake.

    Accepts a snake as either a tuple:

    (1, 1, 2, 0)

    or a string:

    "1120"

    '''
    snake = tuple(int(s) for s in snake)
    coords = all_coords(snake)

    turtle.clearscreen()
    t = turtle.Turtle()
    t.hideturtle()
    s = t.screen
    s.tracer(0)

    width, height = s.window_width(), s.window_height()

    x_min = min(coord[0] for coord in coords)
    x_max = max(coord[0] for coord in coords)
    y_min = min(coord[1] for coord in coords)
    y_max = max(coord[1] for coord in coords)
    unit_length = min(width // (x_max - x_min + 1),
                      height // (y_max - y_min + 1)
                      )

    origin_x = -(x_min + x_max) * unit_length // 2
    origin_y = -(y_min + y_max) * unit_length // 2

    pen_width = max(1, unit_length // 20)
    t.pensize(pen_width)
    dot_size = max(4, pen_width * 3)

    t.penup()
    t.setpos(origin_x, origin_y)
    t.pendown()

    t.forward(unit_length)
    for joint in snake:
        t.dot(dot_size)
        t.left((joint - 1) * 90)
        t.forward(unit_length)
    s.update()


def neighbours(origin, excluded=()):
    '''Return list of states reachable in one step.'''
    states = []
    for candidate in potential_neighbours(origin):
        if candidate not in excluded and reachable(origin, candidate):
            states.append(candidate)
    return states


def mirrored_or_backwards(candidates):
    '''Return set of states that are equivalent to a state in candidates.'''
    states = set()
    for candidate in candidates:
        mirrored = tuple(2 - joint for joint in candidate)
        backwards = candidate[::-1]
        mirrored_backwards = mirrored[::-1]
        states |= set((mirrored, backwards, mirrored_backwards))
    return states


def possible_snakes(snake):
    '''
    Return the set of possible arrangements of the given snake.

    >>> possible_snakes((1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1))
    {(1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1)}

    '''
    reached = set()
    candidates = set((snake,))

    while candidates:
        candidate = candidates.pop()
        reached.add(candidate)
        new_candidates = neighbours(candidate, reached)
        reached |= mirrored_or_backwards(new_candidates)
        set_of_new_candidates = set(new_candidates)
        reached |= set_of_new_candidates
        candidates |= set_of_new_candidates

    excluded = set()
    final_answers = set()
    while reached:
        candidate = reached.pop()
        if candidate not in excluded:
            final_answers.add(candidate)
            excluded |= mirrored_or_backwards([candidate])

    return final_answers


def straight_derived_snakes(length):
    '''Return the set of possible arrangements of a snake of this length.'''
    straight_line = (1,) * max(length-1, 0)
    return possible_snakes(straight_line)


if __name__ == '__main__':
    import doctest
    doctest.testmod()
    import sys
    arguments = sys.argv[1:]
    if arguments:
        length = int(arguments[0])
    else:
        length = int(input('Enter the length of the snake:'))
    print(len(straight_derived_snakes(length)))

risultati

Sulla mia macchina il serpente più lungo che può essere calcolato in meno di 1 minuto è la lunghezza 11 (o la lunghezza 13 con PyPy *). Questo è ovviamente solo un punteggio provvisorio fino a quando non scopriamo qual è il punteggio ufficiale dalla macchina di Lembik.

Per il confronto, ecco i risultati per i primi valori di n:

 n | reachable orientations
-----------------------------
 0 | 1
 1 | 1
 2 | 2
 3 | 4
 4 | 9
 5 | 22
 6 | 56
 7 | 147
 8 | 388
 9 | 1047
10 | 2806
11 | 7600
12 | 20437
13 | 55313
14 | 148752
15 | 401629
16 | 1078746
17 | MemoryError (on my machine)

Per favore fatemi sapere se qualcuno di questi risulta essere errato.

Se hai un esempio di un accordo che non dovrebbe essere in grado di essere spiegato, puoi usare la funzione neighbours(snake)per trovare qualsiasi accordo raggiungibile in un solo passaggio, come test del codice. snakeè una tupla di direzioni congiunte (0 per orario, 1 per dritto, 2 per antiorario). Ad esempio (1,1,1) è un serpente dritto di lunghezza 4 (con 3 giunti).

visualizzazione

Per visualizzare un serpente che hai in mente, o uno qualsiasi dei serpenti restituiti neighbours, puoi usare la funzione display(snake). Questo accetta una tupla come le altre funzioni, ma poiché non è utilizzato dal programma principale e quindi non deve essere veloce, accetterà anche una stringa, per comodità.

display((1,1,2,0)) è equivalente a display("1120")

Come menziona Lembik nei commenti, i miei risultati sono identici all'OEIS A037245 che non tiene conto delle intersezioni durante la rotazione. Questo perché per i primi valori di n non c'è alcuna differenza: tutte le forme che non si intersecano da sole possono essere raggiunte piegando un serpente dritto. La correttezza del codice può essere verificata chiamando neighbours()con un serpente che non può essere spiegato senza intersezione. Dato che non ha vicini, contribuirà solo alla sequenza OEIS e non a questa. L'esempio più piccolo di cui sono a conoscenza è questo serpente di lunghezza 31 che Lembik ha menzionato, grazie a David K :

(1,1,1,1,2,1,2,1,1,1,1,1,1,2,1,1,1,2,1,1,2,2,1,0,1,0,1,1,1,1)

display('111121211111121112112210101111') fornisce il seguente output:

Immagine del serpente più corto senza vicini

Suggerimento: se ridimensionate la finestra del display e richiamate nuovamente il display, il serpente verrà adattato alle nuove dimensioni della finestra.

Mi piacerebbe avere notizie da chiunque abbia un esempio più breve senza vicini. Sospetto che l'esempio più breve segnerà la n più piccola per la quale le due sequenze differiscono.


* Si noti che ogni incremento di n richiede circa 3 volte più a lungo, quindi l'aumento da n = 11 a n = 13 richiede quasi 10 volte il tempo. Questo è il motivo per cui PyPy consente solo di aumentare n di 2, anche se funziona in modo considerevolmente più veloce dell'interprete Python standard.


6
Se questo commento ottiene 5 voti, esaminerò l'aggiunta di un'opzione per includere la visualizzazione delle possibili disposizioni, nel caso ciò aiuti con l'analisi.
trichoplax,


@Geobits Penso di aver capito bene questa volta ...
trichoplax,


1
@Jakube Questo è apribile in molti modi, ad es. Nell'ordine comune # 1 # 3 # 2 # 4 # 5 # 6.
randomra,

1

C ++ 11 - quasi funzionante :)

Dopo aver letto questo articolo , ho raccolto un po 'di saggezza da quel ragazzo che apparentemente ha lavorato per 25 anni sul problema meno complicato di contare i percorsi di auto-evitamento su un reticolo quadrato.

#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;
}

Costruire l'eseguibile

Compila con g++ -O3 -std=c++11
Uso MinGW sotto Win7 con g ++ 4.8 per build "linux", quindi la portabilità non è garantita al 100%.

Funziona anche (in un certo senso) con un progetto MSVC2013 standard

Da indefinire NDEBUG , si ottengono tracce dell'esecuzione dell'algoritmo e un riepilogo delle configurazioni trovate.

Prestazioni

con o senza tabelle hash, il compilatore Microsoft esegue miseramente: g ++ build è 3 volte più veloce .

L'algoritmo non utilizza praticamente memoria.

Poiché il controllo delle collisioni è approssimativamente in O (n), il tempo di calcolo dovrebbe essere in O (nk n ), con k leggermente inferiore a 3.
Sul mio i3-2100@3.1GHz, n = 17 richiede circa 1:30 (circa 2 milioni serpenti / minuto).

Non ho finito l'ottimizzazione, ma non mi aspetto un guadagno maggiore di x3, quindi in pratica posso sperare di raggiungere forse n = 20 in un'ora o n = 24 in un giorno.

Raggiungere la prima forma inconfondibile nota (n = 31) richiederebbe tra qualche anno e un decennio, senza ipotizzare interruzioni di corrente.

Contare le forme

Un serpente di taglia N ha articolazioni N-1 .
Ogni articolazione può essere lasciata diritta o piegata a sinistra o a destra (3 possibilità).
Il numero di possibili pieghe è quindi 3 N-1 .
Le collisioni ridurranno un po 'quel numero, quindi il numero effettivo è vicino a 2,7 N-1

Tuttavia, molte di queste pieghe portano a forme identiche.

due forme sono identiche se esiste una rotazione o una simmetria che possono trasformarsi l'una nell'altra.

Definiamo un segmento come qualsiasi parte diritta del corpo piegato.
Ad esempio, un serpente di taglia 5 piegato al 2o giunto avrebbe 2 segmenti (uno lungo 2 unità e il secondo 3 unità lunghe).
Il primo segmento sarà chiamato head e l'ultima coda .

Per convenzione orientiamo la testa del serpente in senso orizzontale con il corpo che punta a destra (come nella prima figura dell'OP).

Designiamo una determinata figura con un elenco di lunghezze di segmento con segno, con lunghezze positive che indicano una piega a destra e quelle negative una piega a sinistra.
La lunghezza iniziale è positiva per convenzione.

Separazione di segmenti e curve

Se consideriamo solo i diversi modi in cui un serpente di lunghezza N può essere suddiviso in segmenti, finiamo con una ripartizione identica alle composizioni di N.

Usando lo stesso algoritmo mostrato nella pagina wiki, è facile generare tutte le 2 partizioni N-1 possibili del serpente.

A sua volta, ogni partizione genererà tutte le possibili pieghe applicando curve a sinistra oa destra su tutti i suoi giunti. Una di queste pieghe verrà chiamata configurazione .

Tutte le possibili partizioni possono essere rappresentate da un numero intero di N-1 bit, in cui ogni bit rappresenta la presenza di un giunto. Chiameremo questo numero intero un generatore .

Partizioni di potatura

Notando che piegare una data partizione dalla testa in giù equivale a piegare la partizione simmetrica dalla coda in su, possiamo trovare tutte le coppie di partizioni simmetriche ed eliminarne una su due.
Il generatore di una partizione simmetrica è il generatore della partizione scritto in ordine di bit inverso, che è banalmente facile ed economico da rilevare.

Ciò eliminerà quasi la metà delle possibili partizioni, le eccezioni sono le partizioni con generatori "palindromici" che rimangono invariati dall'inversione dei bit (ad esempio 00100100).

Curare le simmetrie orizzontali

Con le nostre convenzioni (un serpente inizia a puntare a destra), la prima curva applicata a destra produrrà una famiglia di pieghe che saranno simmetriche orizzontali da quelle che differiscono solo per la prima curva.

Se decidiamo che la prima curva sarà sempre a destra, elimineremo tutte le simmetrie orizzontali in un sol colpo.

Rastrellando i palindromi

Questi due tagli sono efficienti, ma non abbastanza per prendersi cura di questi fastidiosi palindromi.
Il controllo più approfondito nel caso generale è il seguente:

considera una configurazione C con una partizione palindromica.

  • se invertiamo ogni curva in C, finiamo con una simmetria orizzontale di C.
  • se invertiamo C (applicando curve dalla coda in su), otteniamo la stessa figura ruotata a destra
  • se invertiamo e invertiamo C, otteniamo la stessa figura ruotata a sinistra.

Potremmo verificare ogni nuova configurazione rispetto alle altre 3. Tuttavia, poiché già generiamo solo configurazioni che iniziano con una svolta a destra, abbiamo solo una possibile simmetria da controllare:

  • la C invertita inizierà con una svolta a sinistra, che per costruzione è impossibile da duplicare
  • dalle configurazioni invertite e invertite-invertite, solo una inizierà con una svolta a destra.
    Questa è l'unica configurazione che possiamo eventualmente duplicare.

Eliminare i duplicati senza alcuna memoria

Il mio approccio iniziale era quello di memorizzare tutte le configurazioni in una grande tabella hash, per eliminare i duplicati controllando la presenza di una configurazione simmetrica precedentemente calcolata.

Grazie al suddetto articolo, è diventato chiaro che, poiché le partizioni e le pieghe sono archiviate come bitfield, possono essere confrontate come qualsiasi valore numerico.
Quindi, per eliminare un membro di una coppia simmetrica, puoi semplicemente confrontare entrambi gli elementi e mantenere sistematicamente il più piccolo (o il più grande, come preferisci).

Pertanto, testare una configurazione per la duplicazione equivale a calcolare la partizione simmetrica e, se entrambi sono identici, la piegatura. Non è necessaria alcuna memoria.

Ordine di generazione

Chiaramente il controllo delle collisioni sarà la parte che richiede più tempo, quindi ridurre questi calcoli è un notevole risparmio di tempo.

Una possibile soluzione è quella di avere un "serpente ragdoll" che inizierà in una configurazione piatta e si pieghi gradualmente, per evitare di ricalcolare l'intera geometria del serpente per ogni possibile configurazione.

Scegliendo l'ordine in cui vengono testate le configurazioni, in modo che al massimo venga memorizzato un ragdoll per ogni numero totale di giunti, possiamo limitare il numero di istanze a N-1.

Uso una scansione ricorsiva del sake dalla coda in giù, aggiungendo una singola giuntura ad ogni livello. Quindi una nuova istanza ragdoll è costruita sopra la configurazione principale, con una singola curva adizionale.

Ciò significa che le pieghe vengono applicate in un ordine sequenziale, il che sembra essere sufficiente per evitare collisioni in quasi tutti i casi.

Quando viene rilevata l'auto-collisione, le curve che portano alla mossa offensiva vengono applicate in tutti i possibili ordini fino a quando non viene trovata la piegatura legittima o tutte le combinazioni sono esaurite.

Controllo statico

Prima ancora di pensare alle parti in movimento, ho trovato più efficiente testare la forma statica finale di un serpente per autointersezioni.

Questo viene fatto disegnando il serpente su una griglia. Ogni possibile punto viene tracciato dalla testa in giù. Se c'è un autointersezione, almeno una coppia di punti cadrà nella stessa posizione. Ciò richiede esattamente N grafici per qualsiasi configurazione di serpente, per un tempo O (N) costante.

Il vantaggio principale di questo approccio è che il solo test statico selezionerà semplicemente validi percorsi auto-evitanti su un reticolo quadrato, il che consente di testare l'intero algoritmo inibendo il rilevamento dinamico delle collisioni e assicurandosi di trovare il conteggio corretto di tali percorsi.

Controllo dinamico

Quando un serpente si piega attorno a una giuntura, ogni segmento ruotato spazzerà un'area la cui forma è tutt'altro che banale.
Chiaramente puoi controllare le collisioni testando l'inclusione all'interno di tutte queste aree spazzate individualmente. Un controllo globale sarebbe più efficiente, ma data la complessità delle aree di cui non riesco a pensare (tranne forse usare una GPU per disegnare tutte le aree ed eseguire un controllo globale degli accessi).

Poiché il test statico si occupa delle posizioni iniziale e finale di ciascun segmento, dobbiamo solo controllare le intersezioni con gli archi spazzati da ciascun segmento rotante.

Dopo un'interessante discussione con trichoplax e un po 'di JavaScript per orientarmi, ho trovato questo metodo:

Per provare a dirlo in poche parole, se chiami

  • C il centro di rotazione,
  • S un segmento rotante di lunghezza e direzione arbitrarie che non contiene C ,
  • L la linea che prolunga S
  • H la linea ortogonale a L che passa attraverso C ,
  • I l'intersezione di L e H ,

matematica
(fonte: free.fr )

Per ogni segmento che non contiene I , l'area spazzata è limitata da 2 archi (e 2 segmenti già curati dal controllo statico).

Se io cade all'interno del segmento, l'arco spazzato dal devo anche essere preso in considerazione.

Ciò significa che possiamo verificare ogni segmento immobile rispetto a ciascun segmento rotante con intersezioni a 2 o 3 segmenti con arco

Ho usato la geometria vettoriale per evitare del tutto le funzioni trigonometriche.
Le operazioni vettoriali producono codice compatto e (relativamente) leggibile.

L'intersezione da segmento ad arco richiede un vettore in virgola mobile, ma la logica deve essere immune da errori di arrotondamento.
Ho trovato questa soluzione elegante ed efficiente in un oscuro post sul forum. Mi chiedo perché non sia più ampiamente pubblicizzato.

Funziona?

L'inibizione del rilevamento dinamico delle collisioni produce il conteggio dei percorsi auto-evitanti corretti fino a n = 19, quindi sono abbastanza sicuro che il layout globale funzioni.

Il rilevamento dinamico delle collisioni produce risultati coerenti, anche se manca il controllo delle curve in ordine diverso (per ora).
Di conseguenza, il programma conta i serpenti che possono essere piegati dalla testa in giù (cioè con le articolazioni piegate in ordine crescente della distanza dalla testa).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.