
Analizando un problema olímpico, recorreremos los sinuosos corredores de la generación de laberintos y su paso, y también veremos que en el lenguaje Julia la simplicidad de la implementación de algoritmos bordea su pseudocódigo.
Desafío
El laberinto es un cuadrado a cuadros de 10 por 10, en algunas celdas hay obstáculos, y en una celda hay una salida. El robot se encuentra en un laberinto y puede ejecutar 4 comandos: mover una celda hacia abajo, arriba, derecha o izquierda. Si el robot intenta ir más allá de los límites del laberinto o entrar en la jaula con un obstáculo, entonces permanece en su lugar. Si el robot entra por la salida, entonces sale del laberinto e ignora más comandos. Escriba un programa para el robot, ejecutando el cual el robot en cualquier caso llega a la salida, independientemente de la celda en la que se encontraba al principio. El programa debe consistir en no más de 1000 equipos.

No hay entrada Necesita escribir un programa para una condición específica especificada
El laberinto.
La versión del laberinto que puedes copiar. 0 - celda libre, 1 - obstáculo, x -
salida
0011010011 0100001000 0110x00000 0010000100 0000111000 0000100100 0000010010 0100101010 0011001010 1000011000
Una línea que consta de caracteres U, D, R, L de longitud no más de 1000
Preparación
Los que no trabajaron con gráficos en Julia necesitan descargar paquetes
using Pkg Pkg.add("Plots") Pkg.add("Colors") Pkg.add("Images") Pkg.build("Images")
Es más conveniente trabajar en Jupyter, ya que las imágenes se mostrarán directamente durante el trabajo. Aquí puede encontrar información sobre la instalación, así como una introducción y tareas para principiantes.
En la condición de nuestra tarea, hay una versión del laberinto para copiar
S0 = "0011010011 0100001000 0110x00000 0010000100 0000111000 0000100100 0000010010 0100101010 0011001010 1000011000"
Para dibujar un laberinto, debes hacer una matriz. Como no queremos poner espacios manualmente, trabajaremos con la línea:
S1 = prod(s-> s*' ', '['*S0*']')
Reemplazando la incómoda letra x con un número y analizando la cadena, obtenemos una matriz entera que define nuestro laberinto. Luego, para mayor conveniencia, cambie los unos a ceros y los ceros a unos, y encierre el laberinto con una pared:
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
En la matriz
length(M) 144
células y de ellos
sum(M)-9 70
sobre el cual puedes caminar, es decir, posibles posiciones iniciales. Puede mostrar el resultado construyendo un histograma bidimensional
using Plots heatmap(M, yaxis = :flip)

Verificación de la solución
En primer lugar, debe elaborar procedimientos que verifiquen la corrección de la solución propuesta. Establezca el tipo compuesto Point
para usar la terminología de puntos y coordenadas:
mutable struct Point x::Int64
Y ahora necesita aprender cómo traducir una secuencia de comandos que el robot pueda entender en código.
La idea es bastante simple: hay una coordenada inicial, aparece una línea con un algoritmo que indica cómo caminar. Tomamos una letra y miramos cuál de las celdas vecinas para pisar. A la coordenada actual agregamos el valor que está almacenado en esta celda. Es decir, si hay cero (muro), no nos hemos movido a ningún lado, de lo contrario, hemos dado un pequeño paso en la dirección requerida.
Usando el poder de la metaprogramación, puede reemplazar la secuencia de direcciones entrante con un código voluminoso y uniforme y ejecutarlo
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];")
Pero este método tiene una serie de inconvenientes, por lo tanto, utilizaremos los operadores condicionales clásicos. Por cierto, el hecho de que el número 9 indique la salida es un pequeño truco: para no verificar cada celda, y si es la salida, iniciamos el movimiento agregando el valor almacenado en una celda específica. Cuando el robot entra en la salida, se agrega un número deliberadamente grande a su coordenada, de modo que el robot vuela más allá de los límites de la matriz, lo que puede detectarse como un error al usar el bloque:
try
Por lo tanto, implementamos una función que verificará si el robot llega a la salida a partir del punto c
ejecución de comandos desde la cadena 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
Recopilemos una función que iterará sobre todas las posiciones iniciales
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
Comprueba los comandos:
S = "RRRLDUUURRRUUURRRRLLLRRUULDDDDRRRRDDDRRRUUU" heatmap(M, yaxis = :flip) test(S)

Todos los puntos de partida fueron probados, y solo 10 condujeron a la salida. Es necesario construir rutas desde cada punto hasta la salida.
Antes de sumergirnos en la generación y el paso de los laberintos, guardaremos nuestros resultados. Utilizaremos el paquete Images.jl que ofrece muchas posibilidades en el campo del procesamiento de imágenes (un buen ejemplo ). Una de sus herramientas de soporte es el paquete Colors.jl , que amplía la capacidad de Julia para trabajar con colores.
using Images clrs(x) = x==9 ? RGB(1.,0.5,0) : RGB(x,x,x) maze = clrs.(M)

Búsqueda de profundidad
Implementó la idea de un artículo sobre el Habr .
La idea es simple: crear una cuadrícula de muros
M = 10 N = 10 A = [ i&j&1 for i in 0:N, j in 0:M ]

luego, en bucle, atravesamos la ruta y las ramas. Definimos una función que encuentra áreas vecinas no visitadas (las designaremos, digamos, por un deuce) y devuelve uno de estos vecinos (si no hay vecinos no visitados, devuelve un punto de bandera):
function neighbours2(A,p, n, m) nbrs = [Point(px, p.y+2),
Romperemos las paredes así:
function breakwall(A, newp,oldp)
Algoritmo
- Actualice la celda inicial y márquela como visitada.
- Si bien hay celdas no visitadas
1. Si la celda actual tiene "vecinos" no visitados
1. Empuje la celda actual en la pila
2. Seleccione una celda aleatoria de vecino
3. Retire el muro entre la celda actual y la seleccionada
4. Actualice la celda seleccionada y márquela como visitada.
2. De lo contrario, si la pila no está vacía
1. Saque la jaula de la pila
2. Hazlo actual
3. De lo contrario
1. Seleccione una celda aleatoria no visitada, actualícela y márquela como visitada.
Código del 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)


Algoritmo de búsqueda de ruta de retroceso:
- Actualice la celda inicial y márquela como visitada.
- Hasta que se encuentre una solución
1. Si la celda actual tiene "vecinos" no visitados
1. Empuje la celda actual en la pila
2. Seleccione una celda aleatoria de vecino
3. Actualice la celda seleccionada y márquela como visitada.
2. De lo contrario, si la pila no está vacía
1. Saque la jaula de la pila
2. Hazlo actual
3. De lo contrario, no hay salida.
Estamos buscando vecinos por el contrario y dentro del radio de una celda, y no a través de una:
function neighbours1(A, p, n, m) nbrs = [Point(px, p.y+1),
Defina un algoritmo para pasar el laberinto con el trazado de la ruta y los intentos fallidos
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 algunos han notado, también se llama una función, como la que generan los algoritmos (Programación múltiple). Cuando lo llame con dos números, resolverá el método de construcción del laberinto, pero si lo llama especificando la imagen y dos puntos (las coordenadas de entrada y salida) como argumento, entonces en la salida obtenemos una imagen con el laberinto pasado
img0 = load("D:/dat/maze111.png") amazeng(img0)

Probemos en nuestro laberinto:
img0 = load("D:/dat/maze12x12.png") n, m = size(img0) amazeng(img0, Point(11,9), Point(4,6) )

Incluso si modifica la función para que se recuerde la ruta, el algoritmo sigue siendo ineficaz debido a los espacios abiertos. Pero los laberintos salen geniales.
Algoritmo aleatorizado de Prim
Tan pronto como comience a dibujar laberintos, no se detendrá. Realicemos otro algoritmo interesante:
- Comience con una cuadrícula llena de paredes.
- Seleccione una celda, márquela como parte del laberinto. Agregue paredes celulares a la lista de paredes.
- Si bien hay paredes en la lista:
- Seleccione un muro aleatorio de la lista. Si solo se visita una de las dos celdas que comparte el muro, entonces:
- Haz que la pared sea un pasaje y marca la celda no visitada como parte del laberinto.
- Agregue paredes celulares adyacentes a la lista de paredes.
- Eliminar el muro de la lista.
Código neighbors(p::Point) = [Point(px, p.y+1),
primaze = prim(19,19); Gray.(primaze)

Resulta más ramificado y no menos impresionante, especialmente el proceso de ensamblaje.
Y ahora implementamos el algoritmo más común para encontrar la ruta más corta:
Método A *
- Se crean 2 listas de vértices, pendientes y ya revisadas. Se agrega un punto de inicio a los pendientes, la lista de revisados hasta ahora está vacía.
- Para cada punto se calcula H - la distancia aproximada desde el punto al objetivo.
- De la lista de puntos a considerar, el punto con el menor H . Dejala X .
- Si X - El objetivo, luego encontramos la ruta.
- Llevamos X de la lista pendiente a la lista de ya revisados.
- Para cada uno de los puntos adyacentes a X (denota este punto vecino Y ), haga lo siguiente:
- Si Y ya en el revisado - sáltelo.
- Si Y aún no está en la lista de espera, agréguela allí.
- Si la lista de puntos a considerar está vacía, pero no hemos alcanzado la meta, entonces la ruta no existe.
Para comenzar, definamos el "punto" de clase que sabrá qué tan lejos está del objetivo:
mutable struct Point_h x::Int64
Ahora definimos una operación de comparación para la estructura, un método para establecer la presencia de un elemento en una matriz, así como la función de eliminar un elemento, encontrar la distancia entre puntos y enumerar vecinos:
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 siempre, ¿se pueden aclarar operadores oscuros con el comando ?
por ejemplo ?splice
?argmin
.
Y, en realidad, el método A * en sí
Código function astar(M, start, final)
Cargamos la imagen con el laberinto y la presentamos en forma de matriz:
img0 = load("D:/dat/maze0.png") mazematrix = Float64.(channelview(img0))
Y construimos una ruta para salir de un punto arbitrario:
s = Point_h(11,9)

Una búsqueda desde todas las posiciones se ve así:

Visto que el agente corriendo siempre tiende al lado de la salida y a menudo se convierte en callejones sin salida, lo que da lugar a pasos innecesarios, y se recuerda toda la ruta. Esto se evita con una versión un poco más compleja del algoritmo A *.
- Se crean 2 listas de vértices, pendientes y ya revisadas. Se agrega un punto de inicio a los pendientes, la lista de revisados hasta ahora está vacía.
- Para cada punto se calcula F=G+H . G - distancia desde el inicio hasta el punto, H - la distancia aproximada desde el punto al objetivo. Cada punto también almacena un enlace al punto del que proviene.
- De la lista de puntos a considerar, el punto con el menor F . Dejala X .
- Si X - El objetivo, luego encontramos la ruta.
- Llevamos X de la lista pendiente a la lista de ya revisados.
- Para cada uno de los puntos adyacentes a X (denota este punto vecino Y ), haga lo siguiente:
- Si Y ya en el revisado - sáltelo.
- Si Y aún no está en la lista de espera: agréguela allí, recordando el enlace a X y habiendo calculado Y.G (esto es X.G + distancia de X antes Y ) y Y.H .
- Si Y en la lista para su consideración - verifique si X.G + distancia de X antes Y<Y.G así que llegamos al punto Y camino más corto, reemplazar Y.G en X.G + distancia de X antes Y y el punto desde el cual llegaron a Y en X .
- Si la lista de puntos a considerar está vacía, pero no hemos alcanzado la meta, entonces la ruta no existe.
Pero incluso con pasos innecesarios, nuestra opción está dentro de los límites de la tarea, por lo que modificar los programas será su tarea.
Hasta el final de la Olimpiada, no quedaba nada, pero aún necesitamos aprender a traducir los resultados de nuestros algoritmos en comandos de la forma "RRLUUDL..."
Y después de todos estos paseos por los laberintos, podemos suponer que la solución es mucho más simple. De hecho, una opción simple comienza de inmediato, pero realmente quería hacer cosas hermosas .
Si colocamos a nuestro artista en un área abierta e iniciamos caminatas aleatorias, dudará cerca de su posición inicial. Pero con la introducción de las paredes, algunas de las direcciones comenzarán a desvanecerse, los grados de libertad serán menores y, con los mismos datos de entrada, el agente se moverá una mayor distancia.
Aquí está nuestra versión de verificar la idoneidad de los equipos para rescatar un robot de un laberinto:
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
Ahora es suficiente generar líneas aleatorias hasta que obtenga una que funcione para todas las posiciones iniciales:
using Random S = randstring("RLUD",200) "RDRRRDLRLUULURUDUUDLLLLLULLUDRRURDLDLULLRLUUUDURUUUULRUDUURUUDLRLLULRLUDRRLRRULLDULRRRRULRLLDULRLDRUDURDRUUDUUDDDDDLURRRRDRDURRRDDLLDUURRRLDRUDLRLLRDDRLRRRDDLLLRUURDRLURDLLUULLLLUURLLULUDULDDLDLLRLDUD" test(S) 41 test completed from 70
Además, era posible no preocuparse por la prueba. Fue suficiente para leer la tarea y generar una cadena aleatoria con la condición 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
Es decir, con una probabilidad del 70%, la línea pasaría las pruebas.
Eso es todo Le deseo al lector una aleatoriedad exitosa, paciencia e intuición para obtener soluciones obvias.
Para los curiosos: enlaces para profundizar en el tema: