Помогите, я в ловушке в треугольнике Серпинского!

44

Рисование треугольника Серпинского было сделано до смерти . Есть и другие интересные вещи, которые мы можем сделать с этим, хотя. Если мы достаточно прищуримся в треугольнике, мы можем рассматривать перевернутые треугольники как узлы фрактального графа. Давайте найдем способ обойти этот график!

Сначала давайте назначим номер каждому узлу. Самый большой перевернутый треугольник будет нулевым узлом, а затем мы просто спустимся слой за слоем (сначала в ширину), назначив последовательные числа в порядке сверху-слева-справа:

введите описание изображения здесь
Нажмите, чтобы увеличить версию, где маленькие цифры немного менее размыты.

(Конечно, эта модель продолжает до бесконечности внутри синих треугольников.) Еще один способ определить нумерацию, что центральный узел имеет индекс 0, а дети узла i(смежные треугольники следующего меньшего масштаба) имеют индексы 3i+1, 3i+2и 3i+3.

Как мы перемещаемся по этому графику? Есть шесть естественных шагов, которые можно сделать из любого данного треугольника:

  • Можно всегда перемещаться через середину одного из ребер к одному из трех дочерних узлов текущего узла. Мы будем обозначать эти шаги как N, SWи SE. Например , если мы в настоящее время на узле 2, это приведет к узлам 7, 8, 9соответственно. Другие ходы по краям (непрямым потомкам) запрещены.
  • Можно также пройти через один из трех углов, если он не касается края треугольника, ни непосредственному родителю, ни одному из двух косвенных предков. Мы будем обозначать эти шаги как S, NEи NW. Например, если мы в настоящее время находимся на узле 31, Sприведет к 10, NEбудет недействительным и NWприведет к 0.

Соревнование

Дайте два неотрицательных целых числа xи yнайдите кратчайший путь от xдо y, используя только шесть ходов, описанных выше. Если существует несколько кратчайших путей, выведите любой из них.

Обратите внимание, что ваш код должен работать не только для 5 уровней, изображенных на диаграмме выше. Вы можете предположить это x, y < 1743392200. Это гарантирует, что они помещаются в 32-разрядное целое число со знаком. Обратите внимание, что это соответствует 20 уровням дерева.

Ваш код должен обработать любой действительный ввод менее чем за 5 секунд . Хотя это исключает грубый поиск в ширину, это должно быть довольно слабое ограничение - моя эталонная реализация обрабатывает произвольный ввод для глубины 1000 за полсекунды (это ~ 480-значные числа для узлов).

Вы можете написать программу или функцию, принимая ввод через STDIN (или ближайшую альтернативу), аргумент командной строки или аргумент функции и выводя результат через STDOUT (или ближайшую альтернативу), возвращаемое значение функции или параметр функции (out).

Выход должен быть плоским, недвусмысленным список строк N, S, NE, NW, SE, SW, используя любой разумный разделитель (пробелы, символы перевода строки, запятые, ","...).

Применяются стандартные правила .

Тестовые случаи

Первые несколько тестовых случаев могут быть разработаны вручную, используя диаграмму выше. Другие обеспечивают, чтобы ответы были достаточно эффективными. Для них могут быть другие решения той же длины, которые не перечислены.

0 40                    => N N N N
66 67                   => S SW N N N
30 2                    => NW NW -or- NE SW
93 2                    => NE SW
120 61                  => NW NW NW NW N SE SW N
1493682877 0            => S S NW NW
0 368460408             => SW SW N N SW SW SE SW SW N SE N N SW SW N SE SE
1371432130 1242824      => NW NW NE NW N SE SW SW SW SE SE SW N N N N SW
520174 1675046339       => NE NW NE NE SE SE SW SW N SE N SW N SW SE N N N N SE SE SW SW
312602548 940907702     => NE NW S SW N N SW SE SE SE SW SE N N SW SE SE SE SW
1238153746 1371016873   => NE NE NE SE N N SW N N SW N SE SE SW N SW N N SE N SE N
547211529 1386725128    => S S S NE NW N N SE N SW N SE SW SE SW N SE SE N SE SW SW N
1162261466 1743392199   => NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE
Мартин Эндер
источник

Ответы:

5

Рубин, 195 194 190 184 байта

Оригинал: С извинениями перед Ell , поскольку это, по сути, порт их ответа, и большое спасибо Doorknob за помощь в отладке этого ответа. Вероятно, есть другой алгоритм для этой проблемы - что-то связанное с этим *f[x[0,**however far x matches with y**],y]- но я оставлю это в другой раз.

a=->n{n<1?[]:a[~-n/3]+[-n%3]}
f=->x,y{j=x.size
(Array===x)?x==y[0,j]?y[j..-1].map{|m|%w[SE SW N][m]}:x.uniq.map{|m|[%w[NW NE S][m],*f[x[0,x.rindex(m)],y]]}.min_by(&:size):f[a[x],a[y]]}

РЕДАКТИРОВАТЬ: жадный алгоритм не работает для h[299792458, 1000000]. Я вернул счетчик байтов до 195, пока я исправляю свой алгоритм еще раз. Исправлена ​​ошибка, из-за которой количество байтов увеличивалось до 203. Вздох

В процессе разработки: эта программа использует жадный алгоритм для поиска общих предков x[0,j]==y[0,j](примечание: может быть несколько общих предков). Алгоритм очень слабо основан на рекурсивном поиске предков Элл . Первая половина полученных инструкций - как добраться до этого общего предка, а вторая половина - до y на основе y[j..-1].

Примечание: a[n]здесь возвращается биективная нумерация base-3 с использованием цифр 2,1,0вместо 1,2,3.

В качестве примера, давайте пробежимся через f[59,17]или f[[2,0,2,1],[2,1,1]]. Здесь j == 1. Чтобы добраться до x[0,j], мы идем 0или NW. Затем, чтобы добраться до y, идти [1,1]илиSW SW

a=->n{n<1?[]:a[~-n/3]+[-n%3]}
h=->m,n{x=a[m];y=a[n];c=[];j=x.size
(j=x.uniq.map{|m|k=x.rindex(m);x[0..k]==y[0..k]?j:k}.min
c<<%w[NW NE S][x[j]];x=x[0,j])until x==y[0,j]
c+y[j..-1].map{|m|%w[SE SW N][m]}}
Sherlock9
источник
45

Python 2, 208 205 200 байт

A=lambda n:n and A(~-n/3)+[-n%3]or[]
f=lambda x,y:f(A(x),A(y))if x<[]else["SSNEW"[m::3]for m in
y[len(x):]]if x==y[:len(x)]else min([["NNSWE"[m::3]]+f(x[:~x[::-1].index(m)],y)for
m in set(x)],key=len)

Функция, fпринимающая пару номеров узлов и возвращающая кратчайший путь в виде списка строк.

объяснение

Мы начнем с использования другой схемы адресации для треугольников; адрес каждого треугольника является строкой, определенной следующим образом:

  • Адрес центрального треугольника - пустая строка.

  • Адреса на севере, юго-запад и юго-восток детей каждого треугольника образованы добавлением 0, 1и 2, соответственно, по адресу треугольника.

По сути, адрес каждого треугольника кодирует (кратчайший) путь от центрального треугольника к нему. Первое, что делает наша программа, переводит номера входных треугольников на соответствующие адреса.

Рисунок 1

Нажмите на изображение для увеличения.

Возможные ходы в каждом треугольнике легко определяются по адресу:

  • Для перемещения на север, юго-запад и юго-восток детей, мы просто добавить 0, 1и 2, соответственно, по адресу.

  • Для того, чтобы перейти на юг, северо-восток, и северо-запад предки, мы находим последний (крайний правый) возникновение 0, 1и 2, соответственно, и обрезать адрес слева от него. Если нет 0, 1или 2в адресе, то соответствующего предка не существует. Например, чтобы перейти к северо-западному предку 112(то есть, к его родителю), мы находим последнее вхождение 2in 112, которое является последним символом, и обрезаем адрес слева от него, давая нам 11; чтобы перейти к северо-восточному предку, мы находим последнее вхождение 1in 112, которое является вторым символом, и обрезаем адрес слева от него, давая нам 1; однако, 112не имеет южного предка, так как нет0 в свой адрес.

Обратите внимание на несколько вещей о паре адресов xи y:

  • Если xявляется начальной подстрокой y, то yявляется потомком x, и, следовательно, кратчайший путь от xдо yпросто следует за соответствующим потомком каждого треугольника между xи y; Другими словами, мы можем заменить каждый 0, 1и 2в y[len(x):]с N, SWи SE, соответственно.

  • В противном случае, пусть iбудет индекс первого несоответствия между xи y. Там нет пути из xв yтом , что не проходит через x[:i](который так же , как y[:i]), то есть, первый общий предок xи y. Следовательно, любой путь от xдо yдолжен прибыть x[:i]или один из его предков, давайте назовем этот треугольник z, а затем продолжим y. Чтобы прибыть из xв z, мы следуем за предками, как описано выше. Кратчайший путь от zдо yзадается предыдущим маркером.

Если xявляется исходной подстрокой y, то кратчайший путь от xдо yлегко определяется первой маркированной точкой выше. В противном случае, мы позволяем jбыть наименьшим из показателей последних появлений 0, 1и 2в x. Если jбольше или равно, индекс первого несовпадения xи y, iмы просто добавить соответствующий шаг ( S, NEили NW, соответственно) на пути обрезки xвлево от j, и по- прежнему. Ситуация усложняется, если jменьше, чем i, с тех пор мы могли бы достичь yсамого быстрого, восходя x[:j]непосредственно к общему предку и спускаясь до самогоyили мы могли бы найти другого общего предка, xи yэто ближе к тому, yесли вознестись к другому предку xсправа от него i, и добраться оттуда yбыстрее. Например, чтобы добраться от 1222до 1, кратчайший путь - сначала подняться к центральному треугольнику (адрес которого является пустой строкой), а затем спуститься к 1, т. Е. Первое движение приведет нас слева от точки несоответствия. однако, чтобы добраться от 1222к 12, самый короткий путь - это подняться 122, а затем к 12, т. е. первое движение удерживает нас справа от точки несоответствия.

Итак, как нам найти кратчайший путь? «Официальная» программа использует подход «грубой силы», пытаясь сделать все возможные шаги к любому из предков, когда xне является начальной подстрокой y. Это не так плохо, как кажется! Это решает все тестовые случаи, объединенные, в течение секунды или двух.

Но опять же, мы можем сделать намного лучше: если есть более одного непосредственно достижимого предка слева от точки несоответствия, нам нужно проверить только самого правого, и если имеется более одного непосредственно достижимого предка справа от точки несоответствия, нам нужно проверить только самый левый. В результате получается алгоритм линейного времени по длине x(т. Е. По глубине исходного треугольника или по времени, пропорциональному логарифму числа исходного треугольника), который масштабируется даже в гораздо больших тестовых случаях. Следующая программа реализует этот алгоритм, по крайней мере, по существу - из-за игры в гольф, его сложность, на самом деле, хуже, чем линейная, но он все еще очень быстрый.

Python 2, 271 266 261 байт

def f(x,y):
 exec"g=f;f=[]\nwhile y:f=[-y%3]+f;y=~-y/3\ny=x;"*2;G=["SSNEW"[n::3]for
n in g];P=G+f;p=[];s=0
 while f[s:]:
    i=len(f)+~max(map(f[::-1].index,f[s:]));m=["NNSWE"[f[i]::3]]
    if f[:i]==g[:i]:P=min(p+m+G[i:],P,key=len);s=i+1
    else:p+=m;f=f[:i]
 return P

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

Полученные результаты

Следующий фрагмент можно использовать для запуска тестов для любой версии и получения результатов:

def test(x, y, length):
    path = f(x, y)
    print "%10d %10d  =>  %2d: %s" % (x, y, len(path), " ".join(path))
    assert len(path) == length

#         x           y        Length
test(          0,          40,    4   )
test(         66,          67,    5   )
test(         30,           2,    2   )
test(         93,           2,    2   )
test(        120,          61,    8   )
test( 1493682877,           0,    4   )
test(          0,   368460408,   18   )
test( 1371432130,     1242824,   17   )
test(     520174,  1675046339,   23   )
test(  312602548,   940907702,   19   )
test( 1238153746,  1371016873,   22   )
test(  547211529,  1386725128,   23   )
test( 1162261466,  1743392199,   38   )

Гольф версия

         0         40  =>   4: N N N N
        66         67  =>   5: S SW N N N
        30          2  =>   2: NE SW
        93          2  =>   2: NE SW
       120         61  =>   8: NW NW NW NW N SE SW N
1493682877          0  =>   4: S S NW NW
         0  368460408  =>  18: SW SW N N SW SW SE SW SW N SE N N SW SW N SE SE
1371432130    1242824  =>  17: NW NW NE NW N SE SW SW SW SE SE SW N N N N SW
    520174 1675046339  =>  23: NE NE NE NE SE SE SW SW N SE N SW N SW SE N N N N SE SE SW SW
 312602548  940907702  =>  19: NE NW S SW N N SW SE SE SE SW SE N N SW SE SE SE SW
1238153746 1371016873  =>  22: NE NE NE SE N N SW N N SW N SE SE SW N SW N N SE N SE N
 547211529 1386725128  =>  23: S S S S NW N N SE N SW N SE SW SE SW N SE SE N SE SW SW N
1162261466 1743392199  =>  38: NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE

Эффективная версия

         0         40  =>   4: N N N N
        66         67  =>   5: S SW N N N
        30          2  =>   2: NW NW
        93          2  =>   2: NE SW
       120         61  =>   8: NW NW NW NW N SE SW N
1493682877          0  =>   4: NE S NW NW
         0  368460408  =>  18: SW SW N N SW SW SE SW SW N SE N N SW SW N SE SE
1371432130    1242824  =>  17: NW NW NE NW N SE SW SW SW SE SE SW N N N N SW
    520174 1675046339  =>  23: NE NW NE NE SE SE SW SW N SE N SW N SW SE N N N N SE SE SW SW
 312602548  940907702  =>  19: NE NW S SW N N SW SE SE SE SW SE N N SW SE SE SE SW
1238153746 1371016873  =>  22: NE NE NE SE N N SW N N SW N SE SE SW N SW N N SE N SE N
 547211529 1386725128  =>  23: S S S S NW N N SE N SW N SE SW SE SW N SE SE N SE SW SW N
1162261466 1743392199  =>  38: NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE NE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE SE
флигель
источник
6
Черт, это было быстро. Я не могу сказать вам, как я рад, что каждый раз заставляю вас отвечать на один из моих вызовов. :)
Мартин Эндер
2
@ MartinBüttner Спасибо, это огромный комплимент! FWIW, мне очень нравится решать ваши проблемы. Я мог или не мог бы начать работать над этим, пока он еще был в песочнице ... :)
Ell
2
Схема адресации блестящая. Это круто.
BrainSteel
1
@BrainSteel первое, что мне пришло в голову, это попробовать эту схему адресации, но увидеть, что все это концептуализировано, реализовано и записано менее чем за час, просто потрясающе. +1
Уровень River St
1
@Zymus Я не уверен, что следую, но если вы ссылаетесь на картинку, она не должна соответствовать OP - это другая схема адресации, как описано в посте.
Ell
3

APL (Dyalog Unicode) , 144 132 129 118 133 132 130 124 117 байт SBCS

Большое спасибо Ven и ngn за их помощь в игре в гольф в The APL Orchard , отличном месте для изучения языка APL. ⎕IO←0, Предложения по игре в гольф приветствуются.

Изменить: -12 байт благодаря Ven и ngn, изменив способ nопределения и переключившись с 1-индексации на 0-индексации. -3 из-за исправления ошибки, когда не все переключалось на 0-индексацию. -11 байт из-за изменения как Pи Qкак определены. +15 байт из-за исправления проблемы, когда мой алгоритм был некорректным, большое спасибо ngn за помощь в создании s[⊃⍋|M-s]раздела. -2 байта от перестановки метода поиска пути возврата и +1 байт для исправления ошибки. -2 байта благодаря Адаму от переопределения определения I. -6 байт благодаря ngn от переопределения определения 'S' 'NE' 'NW' 'N' 'SW' 'SE'и от перестановки tопределения (это больше не отдельная переменная). -7 байт благодаря тому, как sопределяется ngn от игры в гольф .

{M←⊃⍸≠⌿↑1+P Q←⍵{(⍵/3)⊤⍺-+/3*⍳⍵}¨⌊31+⍵×2⋄(∪¨↓6 2'SSNENWNNSWSE')[P[I],3+Q↓⍨⊃⌽I←⍬{M≥≢⍵:⍺⋄(⍺∘,∇↑∘⍵)s[⊃⍋|M-s←⌽⊢.⊢⌸⍵]}P]}

Попробуйте онлайн!

Объяснение ошибки в алгоритме

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

С 66 до 5

66  0 2 2 2  0 2 2 2
5   0 1      0 1
       common ancestor

The two ancestors of 0 2 2 2 are:
0 2 2
(empty)

(empty) has the shorter path back to 0 1 as it only needs two forward moves,
while 0 2 2 requires two more backtracks and one more forward move.

С 299792458 по 45687

299792458  0 2 1 1 0 1 1 2 1 1 1 2 1 0 2 2 2 0
45687      0 2 1 1 0 1 1 1 2 2
                          common ancestor

The three ancestors of 299792458 are:
0 2 1 1 0 1 1 2 1 1 1 2 1 0 2 2 2
0 2 1 1 0 1 1 2 1 1 1 2             choose this one
0 2 1 1 0 1 1 2 1 1 1 2 1 0 2 2

And the three ancestors of 0 2 1 1 0 1 1 2 1 1 1 2 are:
0 2 1 1
0 2 1 1 0 1 1 2 1 1
0 2 1 1 0 1 1 2 1 1 1

0 2 1 1 0 1 1 1 2 2     45687 for reference
              common ancestor

While it seems like `0 2 1 1` is the shorter path,
it actually results in a path that is 8 steps long
(2 backtracks, 6 forward steps to 45687).

Meanwhile, `0 2 1 1 0 1 1 2 1 1` is at an equal distance
to the common ancestor and has the following ancestors:
0 2 1 1
0 2 1 1 0 1 1 2 1
0 2 1 1 0 1 1

0 2 1 1 0 1 1 1 2 2     45687 for reference
              common ancestor

Clearly, this is the superior path, as with three backtracks, we have reached
the point of the common ancestor. With 3 backtracks and 3 forward moves,
we have a path that is 6 steps long.

Пояснение к коду

                         should be an array of 2 integers, x y
SierpinskiPath←{⎕IO0   0-indexing
         P Q←{...}¨⍵   First, the bijective base-3 numeration of x and y
    P Q←{
        n←⌊31+⍵×2   The number of digits in the numeration
        z←+/3*⍳⍵     The number of numerations with  n digits
        (n/3)⊤⍵-z    And a simple decode  (base conversion) of ⍵-z
    }¨⍵              gets us our numerations, our paths

    A←↑1+P Q       We turn (1+P Q) into an 2-by-longest-path-length array 
                    pads with 0s and our alphabet also uses 0s
                   So we add 1 first to avoid erroneous common ancestor matches
    Common←⊃⍸≠⌿A   We find the length of the common ancestor, Common

         I←⍬{...}P   Now we get the shortest backtracking path from P
    I←⍬{
        Common=≢⍵:⍺        If P is shorter than Common, return our backtrack path
        s←⌽⊢.⊢⌸⍵           Get the indices of the most recent N SW SE
        ts[⊃⍋|Common-s]   and keep the index that is closest to Common
                           and favoring the ancestors to the right of
                           Common in a tiebreaker (which is why we reverse ⊢.⊢⌸⍵)
        (⍺,t)∇t↑⍵          Then repeat this with each new index to backtrack
    }P                     and the path left to backtrack through

    BacktrackP[I]    We get our backtrack directions
    Forward←(⊃⌽I)↓Q   and our forward moves to Q
                      starting from the appropriate ancestor
    (∪¨↓6 2'SSNENWNNSWSE')[Backtrack,Forward]     'S' 'NE' 'NW' 'N' 'SW' 'SE'
}                     and return those directions

Альтернативное решение с использованием Dyalog Extended и dfns

Если мы используем функцию ⎕CY 'dfns''s' adic, она реализует нашу биективную нумерацию base-n (которая послужила источником вдохновения для версии, которую я использую) для гораздо меньшего числа байтов. Переключение на Dyalog Extended также экономит большое количество байтов, и вот мы здесь. Большое спасибо Адаму за его помощь в игре в гольф. Предложения по игре в гольф приветствуются!

Изменить: -8 байт из-за изменения, как Pи Qопределены. -14 байт из-за перехода на Dyalog Extended. -2 из-за использования полной программы для удаления скобок dfn {}. +17 байт из-за исправления проблемы, когда мой алгоритм был некорректным, и большое спасибо ngn за помощь в создании s[⊃⍋|M-s]раздела. +1 байт для исправления ошибки. -2 байта, благодаря Адаму за изменение определения I и -1 байт за то, что не забыли поставить мои гольфы в обоих решениях . -3 байта благодаря ngn путем перестановки генерации основных направлений, +1 байт из-за исправления глючного гольфа и -3 байта благодаря ngn путем перестановки tопределения (это больше не отдельная переменная). -7 байт благодаря ngn путем изменения порядка sопределения.

APL (Dyalog Extended) , 123 115 101 99 116 117 114 109 102 байта

M←⊃⍸≠⌿↑1+P Q←(⍳3)∘⌂adic¨⎕⋄(∪¨↓6 2'SSNENWNNSWSE')[P[I],3+Q↓⍨⊃⌽I←⍬{M≥≢⍵:⍺⋄(⍺∘,∇↑∘⍵){⍵[⊃⍋|M-⍵]}⌽⊢.⊢⌸⍵}P]

Попробуйте онлайн!

Sherlock9
источник
Для 66 и 1 это не находит кратчайший путь через 0.
Кристиан Сиверс
@ChristianSievers Вы абсолютно правы, и я пока не знаю, как это исправить. Спасибо, что дал мне знать.
Sherlock9