
Analisando um problema das olimpíadas, percorreremos os corredores sinuosos da geração de labirintos e sua passagem, e também veremos que na linguagem Julia a simplicidade da implementação de algoritmos limita seu pseudo-código.
Desafio
O labirinto é um quadrado quadriculado de 10 por 10, em algumas células existem obstáculos e em uma célula há uma saída. O robô está em um labirinto e pode executar 4 comandos: mover uma célula para baixo, para cima, direita ou esquerda. Se o robô tentar ir além dos limites do labirinto ou entrar na gaiola com um obstáculo, ele permanecerá no lugar. Se o robô entra na saída, sai do labirinto e ignora outros comandos. Escreva um programa para o robô, executando o qual o robô chegue à saída, independentemente da célula em que estava localizado no início. O programa deve consistir em não mais que 1000 equipes.

Não há entrada. Você precisa escrever um programa para uma condição específica especificada
o labirinto.
A versão do labirinto que você pode copiar. 0 - célula livre, 1 - obstáculo, x -
saída.
0011010011 0100001000 0110x00000 0010000100 0000111000 0000100100 0000010010 0100101010 0011001010 1000011000
Uma linha composta por caracteres U, D, R, L de comprimento não superior a 1000
Preparação
Quem não trabalhou com gráficos em Julia precisa baixar pacotes
using Pkg Pkg.add("Plots") Pkg.add("Colors") Pkg.add("Images") Pkg.build("Images")
É mais conveniente trabalhar em Jupyter, pois as imagens serão exibidas diretamente durante o trabalho. Aqui você encontra informações sobre a instalação, além de uma introdução e tarefas para iniciantes.
Na condição de nossa tarefa, há uma versão do labirinto para cópia
S0 = "0011010011 0100001000 0110x00000 0010000100 0000111000 0000100100 0000010010 0100101010 0011001010 1000011000"
Para desenhar um labirinto, você precisa fazer uma matriz. Como não queremos colocar espaços manualmente, trabalharemos com a linha:
S1 = prod(s-> s*' ', '['*S0*']')
Substituindo a letra estranha x por um número e analisando a string, obtemos uma matriz inteira definindo nosso labirinto. Então, para maior comodidade, troque os para zeros e os para zeros, e coloque o labirinto na parede:
S2 = replace(S1, 'x'=>'9') M0 = S2 |> Meta.parse |> eval m,n = size(M0) M1 = replace(M0, 1=>0, 0=>1) M = zeros(Int64,m+2,n+2) for i in 2:m+1, j in 2:n+1 M[i,j] = M1[i-1,j-1] end M
Na matriz
length(M) 144
células e a partir deles
sum(M)-9 70
em que você pode andar, ou seja, possíveis posições iniciais. Você pode exibir o resultado construindo um histograma bidimensional
using Plots heatmap(M, yaxis = :flip)

Verificação da solução
Primeiro de tudo, você precisa elaborar procedimentos que verifiquem a exatidão da solução proposta. Defina o tipo composto Point
para usar a terminologia de pontos e coordenadas:
mutable struct Point x::Int64
E agora você precisa aprender como converter uma sequência de comandos que sejam compreensíveis para o robô em código.
A ideia é bem simples: existe uma coordenada inicial, uma linha surge com um algoritmo que diz como andar. Pegamos uma letra e olhamos para qual das células vizinhas pisar. À coordenada atual, adicionamos o valor armazenado nesta célula. Ou seja, se houver zero (parede), não nos movemos para lugar algum; caso contrário, demos um pequeno passo na direção necessária.
Usando o poder da metaprogramação, você pode substituir a sequência de instruções recebida por um código uniforme e volumoso e executá-lo
S = "RRLDUURULDDRDDRRRUUU" S1 = replace(S , "R"=>"c.y+=M[cx, c.y+1];") S1 = replace(S1, "L"=>"cy-=M[cx, cy-1];") S1 = replace(S1, "U"=>"c.x+=M[c.x+1, cy];") S1 = replace(S1, "D"=>"cx-=M[cx-1, cy];")
Mas esse método tem uma série de inconvenientes, portanto, usaremos os operadores condicionais clássicos. A propósito, o fato de a saída ser indicada pelo número 9 é um pequeno truque: para não verificar cada célula e se é a saída, iniciamos o movimento adicionando o valor armazenado em uma célula específica. Quando o robô pisa na saída, um número deliberadamente grande é adicionado à sua coordenada, de forma que o robô voe além dos limites da matriz, o que pode ser detectado como um erro usando o bloco:
try
Assim, implementamos uma função que verifica se o robô alcança a saída a partir do ponto c
executando comandos a partir da string str
:
function isexit(str, c) scatter!([cy],[cx]) try for s in str if s == 'R' c.y+=M[cx, c.y+1]; elseif s == 'L' cy-=M[cx, cy-1]; elseif s == 'U' c.x+=M[c.x+1, cy]; elseif s == 'D' cx-=M[cx-1, cy]; else println("Error! Use only R, L, U, D") end end catch return true end return false end
Vamos coletar uma função que irá percorrer todas as posições iniciais
function test(Str) k = 0 for i in 2:m+1, j in 2:n+1 if M[i,j] == 1 c = Point(i, j) a = isexit(S,c) if a k +=1
Verifique os comandos:
S = "RRRLDUUURRRUUURRRRLLLRRUULDDDDRRRRDDDRRRUUU" heatmap(M, yaxis = :flip) test(S)

Todos os pontos de partida foram testados e apenas 10 levaram à saída. É necessário construir rotas de cada ponto até a saída.
Antes de mergulhar na geração e passagem dos labirintos, salvaremos nossos resultados. Usaremos o pacote Images.jl que oferece muitas possibilidades no campo de processamento de imagens (um bom exemplo ). Uma de suas ferramentas de suporte é o pacote Colors.jl , que expande a capacidade de Julia de trabalhar com cores.
using Images clrs(x) = x==9 ? RGB(1.,0.5,0) : RGB(x,x,x) maze = clrs.(M)

Pesquisa em profundidade
Implementou a ideia de um artigo no Habr .
A ideia é simples: criar uma grade de paredes
M = 10 N = 10 A = [ i&j&1 for i in 0:N, j in 0:M ]

depois, percorrendo o caminho, rompemos a rota e os galhos. Definimos uma função que encontra áreas vizinhas não visitadas (nós as designaremos, digamos, por um empate) e retorna um desses vizinhos (se não houver vizinhos não visitados, ele retornará um sinalizador):
function neighbours2(A,p, n, m) nbrs = [Point(px, p.y+2),
Vamos quebrar as paredes assim:
function breakwall(A, newp,oldp)
Algoritmo
- Atualize a célula inicial e marque-a como visitada.
- Enquanto houver células não visitadas
1. Se a célula atual tiver "vizinhos" não visitados
1. Empurre a célula atual para a pilha
2. Selecione uma célula aleatória da vizinha
3. Remova a parede entre a célula atual e a célula selecionada.
4. Atualize a célula selecionada e marque-a como visitada.
2. Caso contrário, se a pilha não estiver vazia
1. Puxe a gaiola para fora da pilha
2. Faça atual
3. Caso contrário
1. Selecione uma célula não visitada aleatória, atualize-a e marque-a como visitada.
Código do programa function amazeng(n, m) M = [ 2(i&j&1) for i in 0:n, j in 0:m ]; p = Point(2,2)
lbrnt = amazeng(36, 48)


Retrocedendo o algoritmo de pesquisa de caminho:
- Atualize a célula inicial e marque-a como visitada.
- Até que uma solução seja encontrada
1. Se a célula atual tiver "vizinhos" não visitados
1. Empurre a célula atual para a pilha
2. Selecione uma célula aleatória da vizinha
3. Atualize a célula selecionada e marque-a como visitada.
2. Caso contrário, se a pilha não estiver vazia
1. Puxe a gaiola para fora da pilha
2. Faça atual
3. Caso contrário, não há saída.
Estamos procurando vizinhos, pelo contrário, dentro do raio de uma célula, e não através de uma:
function neighbours1(A, p, n, m) nbrs = [Point(px, p.y+1),
Defina um algoritmo para passar o labirinto desenhando a rota e tentativas malsucedidas
function amazeng(img, start, exit) M = Float64.(channelview(img)) n, m = size(M) p = start M[exit.x,exit.y] = 1 lifo = [] push!(lifo, p) while px != exit.x || py != exit.y M[px,py] = 0.4 np = neighbours1(M, p, n, m) if np.x == np.y == 0 M[px,py] = 0.75 p = pop!(lifo)
Como alguns notaram, também é chamada uma função, como a que os algoritmos geram (agendamento múltiplo). Quando você o chama com dois números, ele elaborará o método de construção do labirinto, mas se você o especificar, especificando a imagem e dois pontos (as coordenadas da entrada e da saída) como argumento, na saída, obteremos uma imagem com o labirinto passado
img0 = load("D:/dat/maze111.png") amazeng(img0)

Vamos tentar no nosso labirinto:
img0 = load("D:/dat/maze12x12.png") n, m = size(img0) amazeng(img0, Point(11,9), Point(4,6) )

Mesmo se você modificar a função para que a rota seja lembrada, o algoritmo ainda será ineficaz devido aos espaços abertos. Mas os labirintos saem ótimos.
Algoritmo aleatório de Prim
Assim que você começa a desenhar labirintos, você não para. Vamos executar outro algoritmo interessante:
- Comece com uma grade cheia de paredes.
- Selecione uma célula, marque-a como parte do labirinto. Adicione paredes celulares à lista de paredes.
- Enquanto houver paredes na lista:
- Selecione uma parede aleatória da lista. Se apenas uma das duas células que o muro compartilha for visitada, então:
- Faça uma passagem na parede e marque a célula não visitada como parte do labirinto.
- Adicione paredes celulares adjacentes à lista de paredes.
- Remova a parede da lista.
Código neighbors(p::Point) = [Point(px, p.y+1),
primaze = prim(19,19); Gray.(primaze)

Acontece mais ramificado e não menos impressionante, especialmente o processo de montagem.
E agora implementamos o algoritmo mais comum para encontrar a rota mais curta:
Método A *
- 2 listas de vértices são criadas - pendentes e já revisadas. Um ponto inicial é adicionado aos pendentes; a lista de revisados até o momento está vazia.
- Para cada ponto é calculado H - a distância aproximada do ponto ao alvo.
- Na lista de pontos a serem considerados, o ponto com o menor valor H . Deixe ela X .
- Se X - o objetivo, então encontramos a rota.
- Nós carregamos X da lista pendente para a lista de já revisados.
- Para cada um dos pontos adjacentes a X (denotar esse ponto vizinho Y ), faça o seguinte:
- Se Y já está na revisão - pule.
- Se Y ainda não está na lista de espera - adicione-o lá.
- Se a lista de pontos a considerar estiver vazia, mas ainda não atingimos a meta, a rota não existe.
Para começar, vamos definir o "ponto" da classe que saberá a que distância está do alvo:
mutable struct Point_h x::Int64
Agora, definimos uma operação de comparação para a estrutura, um método para estabelecer a presença de um elemento em uma matriz, bem como a função de remover um elemento, encontrar a distância entre pontos e listar vizinhos:
Escondido import Base: in, == ==(a::Point_h, b::Point_h) = ax==bx && ay==by function in(p::Point_h, Arr::Array{Point_h,1}) for a in Arr if a == p return true end end return false end function splicemin!(Arr)
Como sempre, operadores obscuros podem ser esclarecidos usando o comando ?
por exemplo ?splice
?argmin
.
E, na verdade, o próprio método A *
Código function astar(M, start, final)
Carregamos a imagem com o labirinto e a apresentamos na forma de uma matriz:
img0 = load("D:/dat/maze0.png") mazematrix = Float64.(channelview(img0))
E nós construímos uma rota para sair de um ponto arbitrário:
s = Point_h(11,9)

Uma pesquisa de todas as posições é assim:

Visto que o agente correndo sempre tende para o lado da saída e geralmente se transforma em becos sem saída, o que dá origem a etapas desnecessárias, e todo o percurso é lembrado. Isso é evitado por uma versão um pouco mais complexa do algoritmo A *.
- 2 listas de vértices são criadas - pendentes e já revisadas. Um ponto inicial é adicionado aos pendentes; a lista de revisados até o momento está vazia.
- Para cada ponto é calculado F=G+H . G - distância do início ao ponto, H - a distância aproximada do ponto ao alvo. Cada ponto também armazena um link para o ponto de onde veio.
- Na lista de pontos a serem considerados, o ponto com o menor valor F . Deixe ela X .
- Se X - o objetivo, então encontramos a rota.
- Nós carregamos X da lista pendente para a lista de já revisados.
- Para cada um dos pontos adjacentes a X (denotar esse ponto vizinho Y ), faça o seguinte:
- Se Y já está na revisão - pule.
- Se Y ainda não está na lista de espera - adicione-o, lembrando o link para X e tendo calculado Y.G (isto é X.G + distância de X antes Y ) e Y.H .
- Se Y na lista para consideração - verifique se X.G + distância de X antes Y<Y.G então chegamos ao ponto Y maneira mais curta, substitua Y.G em X.G + distância de X antes Y e o ponto de onde eles vieram Y em X .
- Se a lista de pontos a considerar estiver vazia, mas ainda não atingimos a meta, a rota não existe.
Porém, mesmo com etapas desnecessárias, nossa opção está dentro dos limites da tarefa; portanto, modificar os programas será sua tarefa.
Até o final da Olimpíada, não havia mais nada, mas ainda precisamos aprender a traduzir os resultados de nossos algoritmos em comandos do formato "RRLUUDL..."
E depois de todas essas caminhadas pelos labirintos, podemos assumir que a solução é muito mais simples. De fato, uma opção simples implora imediatamente, mas eu realmente queria fazer coisas bonitas .
Se colocarmos nosso artista em uma área aberta e iniciarmos passeios aleatórios, ele hesitará perto de sua posição inicial. Mas com a introdução das paredes, parte das direções começará a desaparecer, os graus de liberdade se tornarão menores e, com os mesmos dados de entrada, o agente se moverá uma distância maior.
Aqui está a nossa versão de verificar as equipes quanto à adequação para resgatar um robô de um labirinto:
Código M = [ 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 1 1 0 0 0 0 1 0 1 1 1 1 0 1 1 1 0 0 1 0 0 1 9 1 1 1 1 1 0 0 1 1 0 1 1 1 1 0 1 1 0 0 1 1 1 1 0 0 0 1 1 1 0 0 1 1 1 1 0 1 1 0 1 1 0 0 1 1 1 1 1 0 1 1 0 1 0 0 1 0 1 1 0 1 0 1 0 1 0 0 1 1 0 0 1 1 0 1 0 1 0 0 0 1 1 1 1 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0] mutable struct Point
Agora é suficiente gerar linhas aleatórias até você obter uma que funcione para todas as posições iniciais:
using Random S = randstring("RLUD",200) "RDRRRDLRLUULURUDUUDLLLLLULLUDRRURDLDLULLRLUUUDURUUUULRUDUURUUDLRLLULRLUDRRLRRULLDULRRRRULRLLDULRLDRUDURDRUUDUUDDDDDLURRRRDRDURRRDDLLDUURRRLDRUDLRLLRDDRLRRRDDLLLRUURDRLURDLLUULLLLUURLLULUDULDDLDLLRLDUD" test(S) 41 test completed from 70
Além disso, foi possível não se preocupar com o teste. Foi o suficiente para ler a tarefa e gerar uma sequência aleatória com a condição máxima permitida:
for i in 1:20 S = randstring("RLUD",1000) test(S) end 70 test completed from 70 70 test completed from 70 70 test completed from 70 70 test completed from 70 55 test completed from 70
Ou seja, com uma probabilidade de 70%, a linha passaria nos testes.
Isso é tudo. Desejo ao leitor uma aleatoriedade, paciência e intuição bem-sucedidas para soluções óbvias.
Para os curiosos - links para aprofundar o tópico: