Кількість доступних змійних орієнтацій


11

Це завдання не стосується гри Snake.

Уявіть 2d змію, утворену малюванням горизонтальної лінії довжини n. У цілих точках уздовж її тіла ця змія може обертати своє тіло на 90 градусів. Якщо ми визначимо передню частину змії, що знаходиться вліво зліва, для початку обертання перемістить задню частину змії, а передня частина залишиться поставленою. Роблячи багаторазові обертання, він може зробити безліч різних форм тіла змії.

Правила

  1. Одна частина тіла змії не може перекриватися іншою.
  2. Повинно бути можливим досягти остаточної орієнтації без того, щоб між частинами тіла змії перетиналися між собою. У цій проблемі дві точки, які стосуються, вважаються перекритими.
  3. Я вважаю змію та її зворотну сторону такою ж формою.

Завдання

Що стосується обертання, перекладу та дзеркальної симетрії, яка загальна кількість різних форм тіла змії може бути виготовлена?

Приклад обертання частини тіла змій. Уявіть, n=10що змія знаходиться в початковій орієнтації прямої лінії. Тепер обертайте в точці на 490 градусів проти годинникової стрілки. Ми отримуємо змію , 4щоб 10(хвіст змії) лежав вертикально і змії від 0до 4лежачої горизонтально. Зараз змія має один прямий кут у своєму тілі.

Ось кілька прикладів завдяки Мартіну Бюттнеру.

Починаємо з горизонтальної змії.

введіть тут опис зображення

Тепер обертаємося з позиції 4.

введіть тут опис зображення

Ми закінчуємо після обертання в цій орієнтації.

введіть тут опис зображення

Тепер розглянемо цю орієнтацію різної змії.

введіть тут опис зображення

Зараз ми можемо побачити незаконне переміщення там, де було б перекриття, спричинене під час обертання.

приклад зіткнення

Оцінка

Ваш показник найбільший, nза який ваш код може вирішити проблему менше ніж за одну хвилину на моєму комп’ютері.

Коли обертання відбудеться, воно перемістить половину змії разом із нею. Нам доводиться турбуватися про те, чи може якась із цієї частини, яка обертається, перекривати частину змії під час обертання. Для простоти можна припустити, що змія має ширину нуля. Ви можете обертатись лише в певній точці змії до 90 градусів за годинниковою стрілкою. Бо ви ніколи не можете повністю скласти змію надвоє, оскільки це призвело б до двох обертів в одній і тій же точці в одному напрямку.

Форми, які неможливо зробити

Простий приклад форми, яку неможливо зробити, - це велика T. Більш складна версія є

введіть тут опис зображення

(Дякую Харальду Ханче-Олсену за цей приклад)

У цьому прикладі всі сусідні горизонтальні лінії розташовані один на одного, як і вертикальні. Тому немає правового переходу з цієї позиції, і оскільки проблема є зворотною, тому немає можливості дістатися з вихідної позиції.

Мови та бібліотеки

Ви можете використовувати будь-яку мову, у якій є доступний компілятор / перекладач / тощо. для Linux та будь-яких бібліотек, які також вільно доступні для Linux.

Моя машина Часи синхронізуються на моїй машині. Це стандартна установка ubuntu на восьмиядерний процесор AMD FX-8350. Це також означає, що мені потрібно мати можливість запускати ваш код. Як наслідок, використовуйте лише доступне безкоштовне програмне забезпечення та додайте повні інструкції щодо компіляції та запуску коду.


1
@TApicella Дякую за запитання. Коли я кажу: "Коли відбувається обертання, воно перемістить половину змії з нею", я не мав на увазі 50 відсотків. Я тільки мав на увазі частину перед точкою обертання і частину після неї. Якщо ви обертаєтесь від 0 вздовж змії, ви все обертаєте!

2
@TApicella Про ваше друге запитання. Справа в тому, що немає законної ротації з позиції, яку я дав. Якщо це було доступно, необхідно мати можливість повернутися до горизонтальної початкової орієнтації послідовністю обертів (зворотною від тих, які ви взяли б для досягнення кінцевої орієнтації.) Чи можете ви описати один законний поворот, який, на вашу думку, ви можете зробити. з цієї позиції? Щоб було зрозуміло, змія не росте. Він завжди залишається однакової довжини.

3
@TApicella Це здається, що ви очікуєте, що змія росте. Його розмір фіксований. Ви починаєте з однієї довгої змії, і все, що вам дозволяється, - скласти її частини на 90 градусів. З поточного положення ви взагалі не можете застосувати жодної складки, яка б призвела до попереднього етапу змії.
Мартін Ендер

1
Чи можете ви складати в точці більше одного разу (вперед і назад)? Якщо ви можете, це робить його досить складним.
randomra

1
@randomra Ви дійсно можете до тих пір, поки ви ніколи не ходите далі, ніж на дев'яносто градусів прямо.

Відповіді:


5

Python 3 - попередня оцінка: n = 11 (n = 13 з PyPy *)

Оскільки відповідей у ​​перший тиждень не було, ось приклад Python для заохочення конкуренції. Я намагався зробити його читабельним, щоб легко виявити неефективність, щоб дати ідеї для інших відповідей.

Підхід

  • Почніть з прямої змії і знайдіть усі позиції, до яких можна законно дійти за один хід.
  • Знайдіть усі посади, до яких можна законно дійти з цих посад, які ще не були визначені.
  • Повторіть, поки більше не знайдеться, і поверніть кількість знайдених позицій взагалі.

Код

(зараз із деякими доктестами та твердженнями після моєї неправильної першої спроби)

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

Результати

На моїй машині найдовша змія, яку можна обчислити менше ніж за 1 хвилину, - це довжина 11 (або довжина 13 з PyPy *). Це, очевидно, лише попередня оцінка, поки ми не з'ясуємо, яка офіційна оцінка від машини Лембіка.

Для порівняння, ось результати перших кількох значень 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)

Будь ласка, дайте мені знати, якщо щось із них виявляється невірним.

Якщо у вас є приклад розташування, яке не може бути розгорнуте, ви можете використовувати функцію neighbours(snake)пошуку будь-яких домовленостей, доступних за один крок, як тест коду. snake- кортеж спільних напрямків (0 за годинниковою стрілкою, 1 для прямої, 2 - проти годинникової стрілки). Наприклад (1,1,1) - пряма змія довжиною 4 (з 3 суглобами).

Візуалізація

Щоб уявити змію, яку ви маєте на увазі, або будь-яку із змій, що повернулися neighbours, ви можете використовувати цю функціюdisplay(snake) . Це приймає кортеж, як і інші функції, але оскільки він не використовується основною програмою і тому не потребує швидкої роботи, він також прийме рядок для вашої зручності.

display((1,1,2,0)) еквівалентно display("1120")

Як згадує Лембік у коментарях, мої результати ідентичні OEIS A037245, який не враховує перехрестя під час обертання. Це тому, що для перших кількох значень n немає різниці - всі форми, які не перетинаються, можна досягти, склавши пряму змію. Правильність коду можна перевірити, подзвонивши neighbours()зі змією, яку неможливо розгорнути без перехрестя. Оскільки у нього немає сусідів, він сприятиме лише послідовності OEIS, а не цій. Найменший приклад, який мені відомий, - це ця змія довжиною 31 рік, яку згадував Лембік, завдяки Давидові К :

(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') дає такий вихід:

Зображення найкоротшої змії без сусідів

Порада: Якщо ви зміните розмір вікна, а потім знову зателефонуйте до дисплея, змія підійде до нового розміру вікна.

Я хотів би почути від кожного, у кого коротший приклад, у якого немає сусідів. Я підозрюю, що найкоротший такий приклад позначить найменший n, для якого дві послідовності відрізняються.


* Зауважте, що кожен приріст n займає дуже приблизно в 3 рази більше, тому збільшення від n = 11 до n = 13 вимагає майже в 10 разів більше часу. Ось чому PyPy дозволяє лише збільшити n на 2, навіть незважаючи на те, що він працює значно швидше, ніж стандартний інтерпретатор Python.


6
Якщо цей коментар отримає 5 оновлених результатів, я розгляну, як додати можливість включити візуалізацію можливих домовленостей, якщо це допоможе в аналізі.
трихоплакс


@Geobits Я думаю, що я цього разу зрозумів ...
trichoplax

Зараз здається, що це oeis.org/A037245 від americanscientist.org/isissue/pub/how-to-avoid-yourself !

1
@Jakube Це можна відкрити багатьма способами, наприклад, для спільного порядку № 1 # 3 # 2 # 4 # 5 # 6.
randomra

1

C ++ 11 - майже працює :)

Прочитавши цю статтю , я зібрав шматочки мудрості від того хлопця, який, мабуть, працював протягом 25 років над менш складною проблемою підрахунку доріг, що уникають самовиключення, на квадратній решітці.

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

Побудова виконуваного файлу

Для компіляції я використовую MinGW під Win7 з g ++ 4.8 для "linux" збірок, тому портативність не гарантована на 100%.g++ -O3 -std=c++11

Він також працює (на зразок) зі стандартним проектом MSVC2013

До НЕ певного NDEBUG, ви отримаєте сліди виконання алгоритму і резюме знайдених конфігурацій.

Виступи

з хеш-таблицями або без них, компілятор Microsoft спрацьовує сумлінно: збірка g ++ у 3 рази швидша .

Алгоритм практично не використовує пам'ять.

Оскільки перевірка зіткнення знаходиться приблизно в O (n), час обчислення повинен бути в O (nk n ), при k трохи нижче 3.
На моєму i3-2100@3.1GHz, n = 17 займає близько 1:30 (приблизно 2 мільйони змій / хвилина).

Я не робив оптимізації, але я не очікував би більше, ніж виграш x3, тому в основному я можу сподіватися досягти, можливо, n = 20 за годину, або n = 24 за день.

Досягнення першої відомої непоправної форми (n = 31) зайняло б від кількох років до десятиліття, якщо не було відключень електроенергії.

Підрахунок фігур

N розмір змія N-1 суглоби.
Кожен суглоб можна залишити прямо або зігнути вліво або вправо (3 можливості).
Таким чином, кількість можливих складок становить 3 N-1 .
Зіткнення дещо зменшить цю кількість, тому фактична кількість близька до 2,7 Н-1

Однак багато таких складок призводять до однакових форм.

дві форми однакові, якщо є або обертання, або симетрія, яка може перетворити одну в іншу.

Давайте визначимо відрізок як будь-яку пряму частину складеного тіла.
Наприклад, змія розміром 5, складена на другому суглобі, мала б 2 сегменти (один - 2 одиниці, а другий - 3 одиниці).
Перший сегмент буде називатися головою , а останній хвостиком .

За домовленістю ми орієнтуємо голову змій горизонтально тілом, вказуючи праворуч (як на першій фігурі ОП).

Позначимо задану фігуру зі списком довжин підписаних відрізків, при цьому додатні довжини вказують на праву складку, а негативні - на ліву.
Початкова тривалість умовно.

Розділення відрізків і вигинів

Якщо ми розглянемо лише різні способи, як змія довжиною N може бути розбита на сегменти, ми закінчимо переділ, ідентичний композиціям Н.

Використовуючи той же алгоритм, що показаний на сторінці wiki, легко створити всі 2 можливі розділи змії N-1 .

Кожна перегородка, в свою чергу, генерує всі можливі складки, застосовуючи вигини вліво або вправо до всіх її стиків. Одне таке складання буде називатися конфігурацією .

Усі можливі розділи можуть бути представлені цілим числом N-1 бітів, де кожен біт являє собою наявність спільного. Ми будемо називати це ціле число генератором .

Обрізка перегородок

Зауваживши, що згинання даної перегородки від голови вниз рівнозначно згинанню симетричної перегородки від хвоста вгору, ми можемо знайти всі пари симетричних перегородок і усунути одну з двох.
Генератор симетричної секції - це генератор розділу, записаний у зворотному порядку бітів, який виявляється тривіально просто та дешево.

Це дозволить виключити майже половину можливих розділів, виняток становлять перегородки з "паліндромними" генераторами, які не змінюються бітною реверсією (наприклад, 00100100).

Догляд за горизонтальними симетріями

За нашими умовами (змія починає вказувати праворуч), саме перший вигин, застосований праворуч, створить сімейство складок, які будуть горизонтальними симетріями від тих, що відрізняються лише першим вигином.

Якщо ми вирішимо, що перший згин завжди буде праворуч, ми усунемо всі горизонтальні симетрики одним великим махом.

Стирання паліндром

Ці два надрізи ефективні, але недостатньо, щоб піклуватися про ці примхливі паліндроми.
Найбільш ретельна перевірка в загальному випадку полягає в наступному:

розглянемо конфігурацію C з паліндромною перегородкою.

  • якщо перевернути кожен вигин у C, ми закінчимо горизонтальну симетрику C.
  • якщо повернути С (застосувати вигини від хвоста вгору), отримаємо ту саму фігуру, повернуту вправо
  • якщо ми обидва повернемо і перевернемо С, отримаємо ту саму фігуру, повернуту вліво.

Ми могли перевірити кожну нову конфігурацію щодо 3 інших. Однак, оскільки ми вже генеруємо лише конфігурації, починаючи з правого повороту, у нас є лише одна можлива симетрія для перевірки:

  • перевернутий С почнеться з лівого повороту, який за побудовою неможливо дублювати
  • з конвертованих і перевернутих-перевернутих конфігурацій лише одна почнеться з правого повороту.
    Це єдина конфігурація, яку ми можемо дублювати.

Усунення дублікатів без зберігання

Моїм початковим підходом було зберігання всіх конфігурацій у величезній хеш-таблиці, щоб усунути дублікати, перевіривши наявність раніше обчисленої симетричної конфігурації.

Завдяки вищезазначеній статті стало зрозуміло, що, оскільки розділи та складки зберігаються як бітові поля, їх можна порівняти, як будь-яке числове значення.
Отже, щоб усунути одного члена симетричної пари, ви можете просто порівняти обидва елементи та систематично зберігати найменший (або найбільший), як вам подобається.

Таким чином, тестування конфігурації на дублювання становить обчислення симетричного розділу, і якщо обидва однакові, то складання. Пам'ять взагалі не потрібна.

Порядок покоління

Очевидно, що перевірка на зіткнення буде найбільш трудомісткою частиною, тому скорочення цих обчислень є головною економією часу.

Можливим рішенням є створення «зграйкою змії», яка запускатиметься в плоскій конфігурації і буде поступово загинатися, щоб уникнути перерахування всієї геометрії змії для кожної можливої ​​конфігурації.

Вибираючи порядок тестування конфігурацій, щоб максимум зберігати регдол для кожної загальної кількості з'єднань, ми можемо обмежити кількість екземплярів N-1.

Я використовую рекурсивне сканування саке з хвоста вниз, додаючи по одному стику на кожному рівні. Таким чином, новий екземпляр ragdoll будується поверх батьківської конфігурації, з одним додатковим вигином.

Це означає, що вигини застосовуються в послідовному порядку, що, здається, достатньо, щоб уникнути самостійних зіткнень майже у всіх випадках.

При виявленні самостійного зіткнення вигини, що призводять до кривдного руху, застосовуються у всіх можливих порядках, поки не буде знайдено законне складання або не вичерпаються всі комбінації.

Статична перевірка

Перш ніж навіть думати про рухомі частини, я вважав більш ефективним перевірити статичну остаточну форму змії для самоперехрещення.

Це робиться малюванням змії на сітці. Кожна можлива точка побудована від голови вниз. Якщо є самоперетин, принаймні пара точок впаде на ту саму локацію. Для цього потрібно рівно N ділянок для будь-якої конфігурації змії, за постійний час O (N).

Основна перевага такого підходу полягає в тому, що тільки статичний тест буде просто вибирати дійсні шляхи самовиключення на квадратній решітці, що дозволяє протестувати весь алгоритм, гальмуючи динамічне виявлення зіткнень і гарантуючи, що ми знайдемо правильний підрахунок таких шляхів.

Динамічна перевірка

Коли змія складеться навколо одного суглоба, кожен обертається сегмент змітає область, форма якої є не що інше, як банальна.
Зрозуміло, що ви можете перевірити зіткнення, перевіривши включення у всіх таких областях окремо. Глобальна перевірка була б більш ефективною, але, враховуючи складність областей, я не можу придумати жодної (крім, можливо, використання графічного процесора для малювання всіх областей та проведення глобальної перевірки хітів).

Оскільки статичне випробування піклується про початкові та кінцеві позиції кожного сегмента, нам просто потрібно перевірити перехрестя з дугами, що прокочуються кожним обертовим сегментом.

Після цікавої дискусії з трихоплаком та трохи JavaScript, щоб отримати підшипники, я придумав цей метод:

Щоб спробувати сказати це кількома словами, якщо ви телефонуєте

  • C центр обертання,
  • S обертовий відрізок довільної довжини і напрямку, який не містить C ,
  • L лінія, що подовжує S
  • H лінія, ортогональна до L, що проходить через C ,
  • I перетин L і H ,

математика
(джерело: free.fr )

Для будь-якого сегмента, який не містить I , область, що прокочується, пов'язана двома дугами (і 2 сегменти, про які вже піклується статична перевірка).

Якщо я потрапляю в сегмент, дуга, яка прокотилася, я також повинна враховувати.

Це означає, що ми можемо перевірити кожен нерухливий відрізок проти кожного обертового сегмента з 2 або 3 перетином сегмента з дугою

Я використовував векторну геометрію, щоб взагалі уникнути тригонометричних функцій.
Векторні операції створюють компактний і (відносно) читабельний код.

Перетин сегмента до дуги вимагає вектора з плаваючою точкою, але логіка повинна бути несхильною до помилок округлення.
Я знайшов це елегантне та ефективне рішення у неясному дописі на форумі. Цікаво, чому вона не широко розголошується.

Це працює?

Інгібування динамічного виявлення зіткнень призводить до правильної кількості шляхів самовиключення до n = 19, тому я впевнений, що глобальний макет працює.

Динамічне виявлення зіткнень дає стійкі результати, хоча перевірка згинів в іншому порядку відсутня (поки що).
Як наслідок, програма враховує змій, які можна зігнути з голови вниз (тобто зі складеними суглобами в порядку збільшення відстані від голови).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.