Ajuda, estou preso em um triângulo de Sierpinski!

44

Desenhar o triângulo de Sierpinski foi feito até a morte . Há outras coisas interessantes que podemos fazer com isso. Se apertarmos os olhos com força o triângulo, podemos ver os triângulos de cabeça para baixo como nós de um gráfico fractal. Vamos encontrar o caminho desse gráfico!

Primeiro, vamos atribuir um número a cada nó. O maior triângulo invertido será o nó zero e, em seguida, desceremos camada por camada (largura primeiro), atribuindo números consecutivos na ordem superior esquerda esquerda:

insira a descrição da imagem aqui
Clique para uma versão maior, onde os números pequenos são um pouco menos embaçados.

(Claro, esse padrão continuar ad infinitum dentro dos triângulos azuis.) Outra forma de definir a numeração é que o nó central tem o índice 0, e os filhos de nó i(triângulos adjacentes do lado de menor escala) têm índices 3i+1, 3i+2e 3i+3.

Como nos movemos neste gráfico? Existem até seis etapas naturais que você pode seguir em qualquer triângulo:

  • Sempre é possível mover-se pelo ponto médio de uma das arestas para um dos três filhos do nó atual. Vamos designar esses movimentos como N, SWe SE. Por exemplo, se estamos atualmente no nó 2, elas levariam a nós 7, 8, 9, respectivamente. Outros movimentos pelas bordas (para descendentes indiretos) não são permitidos.
  • Também é possível mover-se através de um dos três cantos, desde que não toque a borda do triângulo, para o pai direto ou um dos dois ancestrais indiretos. Vamos designar esses movimentos como S, NEe NW. Por exemplo, se atualmente estamos no nó 31, Slevaria a 10, NEseria inválido e NWlevaria a 0.

O desafio

Dados dois números inteiros não negativos xe y, encontre o caminho mais curto de xpara y, usando apenas os seis movimentos descritos acima. Se houver vários caminhos mais curtos, imprima qualquer um deles.

Observe que seu código deve funcionar além dos 5 níveis descritos no diagrama acima. Você pode assumir isso x, y < 1743392200. Isso garante que eles caibam dentro de um número inteiro assinado de 32 bits. Observe que isso corresponde a 20 níveis da árvore.

Seu código deve processar qualquer entrada válida em menos de 5 segundos . Embora isso exclua uma pesquisa de força bruta pela primeira vez, deve ser uma restrição bastante frouxa - minha implementação de referência lida com entradas arbitrárias para profundidade 1000 em meio segundo (ou seja, números de ~ 480 dígitos para os nós).

Você pode escrever um programa ou função, recebendo entrada via STDIN (ou alternativa mais próxima), argumento da linha de comando ou argumento da função e emitindo o resultado via STDOUT (ou alternativa mais próxima), valor de retorno da função ou parâmetro da função (saída).

A saída deve ser uma lista simples, sem ambiguidades das cordas N, S, NE, NW, SE, SW, usando qualquer separador razoável (espaços, Linefeeds, vírgulas, ","...).

Aplicam-se as regras padrão de .

Casos de teste

Os primeiros casos de teste podem ser trabalhados manualmente, usando o diagrama acima. Os outros garantem que as respostas sejam suficientemente eficientes. Para aqueles, pode haver outras soluções do mesmo comprimento que não estão listadas.

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
Martin Ender
fonte

Respostas:

5

Ruby, 195 194 190 184 bytes

Original: Com desculpas a Ell , já que essa é essencialmente uma porta de sua resposta e muito obrigado a Maçaneta pela ajuda na depuração dessa resposta. Provavelmente existe outro algoritmo para esse problema - algo a ver com *f[x[0,**however far x matches with y**],y]- mas vou salvá-lo por outro tempo.

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]]}

EDIT: O algoritmo ganancioso não funciona h[299792458, 1000000]. Eu retornei a contagem de bytes para 195, enquanto reparo meu algoritmo mais uma vez. Foi corrigido apenas para a contagem de bytes subir para 203. Suspiro

Em construção: Este programa usa um algoritmo guloso para encontrar ancestrais comuns x[0,j]==y[0,j](nota: pode haver vários ancestrais comuns). O algoritmo baseia-se muito vagamente na pesquisa recursiva de antepassados ​​de Ell . A primeira metade das instruções resultantes são sobre como chegar a esse ancestral comum e a segunda metade está chegando a y com base em y[j..-1].

Nota: a[n]aqui retorna uma numeração bijetiva de base 3 usando os dígitos em 2,1,0vez de 1,2,3.

Como exemplo, vamos percorrer f[59,17]ou f[[2,0,2,1],[2,1,1]]. Aqui j == 1. Para chegar x[0,j], nós vamos 0ou NW. Então, para chegar y, ir [1,1]ouSW 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
fonte
45

Python 2, 208 205 200 bytes

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)

Uma função, fpegando um par de números de nós e retornando o caminho mais curto como uma lista de cadeias.

Explicação

Começamos empregando um esquema de endereçamento diferente para os triângulos; o endereço de cada triângulo é uma sequência, definida da seguinte forma:

  • O endereço do triângulo central é a string vazia.

  • Os endereços do norte, sul-oeste, e as crianças do sudeste de cada triângulo são formadas anexando 0, 1e 2, respectivamente, para o endereço do triângulo.

Essencialmente, o endereço de cada triângulo codifica o caminho (mais curto) do triângulo central para ele. A primeira coisa que nosso programa faz é converter os números dos triângulos de entrada nos endereços correspondentes.

figura 1

Clique na imagem para uma versão maior.

Os movimentos possíveis em cada triângulo são facilmente determinados a partir do endereço:

  • Para mover para o norte, o sul-oeste, e as crianças sul-leste, nós simplesmente acrescentar 0, 1e 2, respectivamente, para o endereço.

  • Para mover para o sul, norte-leste, e ancestrais norte-oeste, encontramos a última (mais à direita) ocorrência de 0, 1e 2, respectivamente, e aparar o endereço para a esquerda dele. Se não houver 0, 1ou 2no endereço, o ancestral correspondente não existe. Por exemplo, para mover para o ancestral noroeste de 112(ou seja, seu pai), encontramos a última ocorrência de 2in 112, que é o último caractere, e recortamos o endereço à esquerda dele, fornecendo-nos 11; para mover para o ancestral nordeste, encontramos a última ocorrência de 1in 112, que é o segundo caractere, e recortamos o endereço à esquerda dele, fornecendo-nos 1; no entanto, 112não possui ancestral sul, pois não há0 em seu endereço.

Observe algumas coisas sobre um par de endereços xe y:

  • Se xé uma subcadeia inicial de y, então yé um descendente de xe, portanto, o caminho mais curto de xpara ysimplesmente segue o filho correspondente de cada triângulo entre xe y; em outras palavras, podemos substituir cada 0, 1e 2no y[len(x):]com N, SWe SE, respectivamente.

  • Caso contrário, iseja o índice da primeira incompatibilidade entre xe y. Não existe um caminho xpara o yqual não passe x[:i](que é o mesmo que y[:i]), ou seja, o primeiro ancestral comum de xe y. Portanto, qualquer caminho de xpara ydeve chegar a x[:i], ou um de seus ancestrais, vamos chamar esse triângulo ze continuar y. Para chegar de xaté z, seguimos os ancestrais conforme descrito acima. O caminho mais curto de zaté yé dado pelo ponto anterior.

Se xé uma substring inicial de y, então o caminho mais curto de xpara yé fornecido facilmente pelo primeiro marcador acima. Caso contrário, deixamos jser o menor dos índices dos últimos ocorrências 0, 1e 2no x. Se jfor maior do que, ou igual a, o índice do primeiro incompatibilidade entre xe y, i, nós simplesmente adicionar o movimento correspondente ( S, NEou NW, respectivamente) para o caminho, cortar xà esquerda de j, e continuar. As coisas ficam mais complicadas se jfor menor do que i, desde então, podemos chegar ymais rápido ascendendo x[:j]diretamente ao ancestral comum e descendo todo o caminho atéy, ou poderemos chegar a um ancestral comum diferente xe ymais próximo y, ascendendo a um ancestral diferente xà direita de i, e indo daí para ymais rápido. Por exemplo, para ir de 1222para 1, o caminho mais curto é primeiro ascender ao triângulo central (cujo endereço é a cadeia vazia) e depois descer para 1, ou seja, o primeiro movimento nos leva à esquerda do ponto de incompatibilidade. no entanto, para passar de 1222para 12, o caminho mais curto é subir para 122e depois para 12, ou seja, o primeiro movimento nos mantém à direita do ponto de incompatibilidade.

Então, como encontramos o caminho mais curto? O programa "oficial" usa uma abordagem de força bruta, tentando todos os movimentos possíveis para qualquer um dos ancestrais sempre que xnão for uma substring inicial de y. Não é tão ruim quanto parece! Resolve todos os casos de teste combinados em um ou dois segundos.

Mas, novamente, podemos fazer muito melhor: se houver mais de um ancestral diretamente acessível à esquerda do ponto de incompatibilidade, precisamos apenas testar o mais à direita e se houver mais de um ancestral diretamente acessível ao À direita do ponto de incompatibilidade, precisamos apenas testar o mais à esquerda. Isso gera um algoritmo de tempo linear, com a duração de x(ou seja, a profundidade do triângulo de origem, ou um tempo proporcional ao logaritmo do número do triângulo de origem), que amplia os casos de teste ainda maiores. O programa a seguir implementa esse algoritmo, pelo menos em essência - devido ao golfe, sua complexidade é, de fato, pior que linear, mas ainda é muito rápida.

Python 2, 271 266 261 bytes

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

Observe que, diferentemente da versão mais curta, esta versão foi escrita especificamente para não usar recursão na conversão dos valores de entrada em seus endereços correspondentes, para que ele possa manipular valores muito grandes sem sobrecarregar a pilha.

Resultados

O seguinte trecho pode ser usado para executar os testes, para qualquer versão, e gerar os resultados:

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   )

Versão Golfed

         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

Versão eficiente

         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
Ell
fonte
6
Droga, isso foi rápido. Eu não posso te dizer o quão feliz isso me faz toda vez que eu faço você responder a um dos meus desafios. :)
Martin Ender
2
@ MartinBüttner Obrigado, é um grande elogio! FWIW, gosto imensamente de resolver seus desafios. Eu poderia, ou não pode, começaram a trabalhar em um presente, enquanto ele ainda estava na caixa de areia ... :)
Ell
2
O esquema de endereçamento é brilhante. Isso é incrível.
BrainSteel
1
A @BrainSteel, a primeira coisa que me ocorreu foi tentar esse esquema de endereçamento, mas ver a coisa toda conceituada, implementada e escrita em menos de uma hora é incrível. +1
Level River St
1
@Zymus Eu não estou certo que eu siga, mas se você está se referindo a imagem, não é suposto para coincidir com o OP-este é um esquema de endereçamento diferente, como descrito no post,.
Ell
3

APL (Dyalog Unicode) , 144 132 129 118 133 132 130 124 117 bytes SBCS

Muito obrigado a Ven e ngn por sua ajuda no golfe no The APL Orchard , um ótimo lugar para aprender a língua APL. ⎕IO←0. Sugestões de golfe são bem-vindas.

Editar: -12 bytes graças a Ven e ngn, alterando como né definido e alternando da indexação 1 para a indexação 0. -3 devido à correção de um erro no qual nem tudo foi alterado para indexação 0. -11 bytes devido à alteração de como Pe Qsão definidos. +15 bytes devido à correção de um problema em que meu algoritmo estava incorreto com muitos agradecimentos ao ngn pela ajuda na definição da s[⊃⍋|M-s]seção. -2 bytes de reorganizar o método de encontrar o caminho de retorno e +1 byte para a correção de erros. -2 bytes graças a Adám por reorganizar a definição de I. -6 bytes graças ao ngn de reorganizar a definição 'S' 'NE' 'NW' 'N' 'SW' 'SE'e de reorganizar como té definido (não é mais uma variável separada). -7 bytes graças a ngn de golfing como sé definido.

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

Experimente online!

Uma explicação do erro no algoritmo

O problema básico é que pensei que o caminho mais curto passava diretamente pelo ancestral comum e não podia, de fato, passar por um ancestral do ancestral comum. Isso está incorreto, como os exemplos a seguir demonstrarão.

De 66 a 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.

De 299792458 a 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.

Explicação do código

                         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

Solução alternativa usando Dyalog Extended e dfns

Se usarmos ⎕CY 'dfns'a adicfunção, ela implementará nossa numeração bijetiva base-n (que foi a inspiração para a versão que eu uso) por muito menos bytes. Mudar para o Dyalog Extended também economiza bastante bytes, e aqui estamos. Muito obrigado a Adám por sua ajuda no golfe. Sugestões de golfe são bem-vindas!

Editar: -8 bytes devido à alteração de como Pe Qsão definidos. -14 bytes devido à mudança para o Dyalog Extended. -2 devido ao uso de um programa completo para remover os colchetes dfn {}. +17 bytes devido a fixação de um problema onde meu algoritmo foi incorreto com muitos agradecimentos para NGN para ajudar com descobrir a s[⊃⍋|M-s]seção. +1 byte para correção de bugs. -2 bytes graças a Adám por reorganizar a definição I e -1 byte de lembrar de colocar meus golfs em ambas as soluções . -3 bytes graças ao ngn, reorganizando a geração das direções cardinais, +1 byte para corrigir um buggy e -3 bytes graças ao ngn, reorganizando como té definido (não é mais uma variável separada). -7 bytes graças ao ngn, reorganizando como sé definido.

APL (Dyalog extendida) , 123 115 101 99 116 117 114 109 102 bytes

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

Experimente online!

Sherlock9
fonte
Para 66 e 1, isso não encontra o caminho mais curto via 0.
Christian Sievers
@ChristianSievers Você está absolutamente certo e não tenho certeza de como consertar isso ainda. Obrigado por me avisar.
Sherlock9