En bucle en espiral
Un amigo necesitaba un algoritmo que le permitiera recorrer los elementos de una matriz NxM (N y M son impares). Se me ocurrió una solución, pero quería ver si mis compañeros de SO podían encontrar una solución mejor.
Estoy publicando mi solución como respuesta a esta pregunta.
Salida de ejemplo:
Para una matriz de 3x3, el resultado debería ser:
(0, 0) (1, 0) (1, 1) (0, 1) (-1, 1) (-1, 0) (-1, -1) (0, -1) (1, -1 )
Además, el algoritmo debe admitir matrices no cuadradas, por lo que, por ejemplo, para una matriz de 5x3, el resultado debe ser:
(0, 0) (1, 0) (1, 1) (0, 1) (-1, 1) (-1, 0) (-1, -1) (0, -1) (1, -1 ) (2, -1) (2, 0) (2, 1) (-2, 1) (-2, 0) (-2, -1)
Aquí está mi solución (en Python):
def spiral(X, Y):
x = y = 0
dx = 0
dy = -1
for i in range(max(X, Y)**2):
if (-X/2 < x <= X/2) and (-Y/2 < y <= Y/2):
print (x, y)
# DO STUFF...
if x == y or (x < 0 and x == -y) or (x > 0 and x == 1-y):
dx, dy = -dy, dx
x, y = x+dx, y+dy
¿C++ alguien? Traducción rápida de Python, publicada para que esté completa
void Spiral( int X, int Y){
int x,y,dx,dy;
x = y = dx =0;
dy = -1;
int t = std::max(X,Y);
int maxI = t*t;
for(int i =0; i < maxI; i++){
if ((-X/2 <= x) && (x <= X/2) && (-Y/2 <= y) && (y <= Y/2)){
// DO STUFF...
}
if( (x == y) || ((x < 0) && (x == -y)) || ((x > 0) && (x == 1-y))){
t = dx;
dx = -dy;
dy = t;
}
x += dx;
y += dy;
}
}
let x = 0
let y = 0
let d = 1
let m = 1
while true
while 2 * x * d < m
print(x, y)
x = x + d
while 2 * y * d < m
print(x, y)
y = y + d
d = -1 * d
m = m + 1
Ha habido muchas soluciones propuestas para este problema escritas en varios lenguajes de programación, sin embargo, todas parecen surgir del mismo enfoque complicado. Voy a considerar el problema más general de calcular una espiral que se puede expresar de manera concisa mediante inducción.
Caso base: comience en (0, 0), avance 1 cuadrado, gire a la izquierda, avance 1 cuadrado, gire a la izquierda. Paso inductivo: avanzar n+1 casillas, girar a la izquierda, avanzar n+1 casillas, girar a la izquierda.
La elegancia matemática de expresar este problema sugiere fuertemente que debería haber un algoritmo simple para calcular la solución. Teniendo en cuenta la abstracción, he optado por no implementar el algoritmo en un lenguaje de programación específico sino más bien como pseudocódigo.
Primero, consideraré un algoritmo para calcular solo 2 iteraciones de la espiral usando 4 pares de bucles while. La estructura de cada par es similar, pero distinta por derecho propio. Esto puede parecer una locura al principio (algunos bucles solo se ejecutan una vez), pero paso a paso haré transformaciones hasta llegar a 4 pares de bucles que son idénticos y, por lo tanto, pueden reemplazarse con un solo par colocado dentro de otro bucle. Esto nos proporcionará una solución general para calcular n iteraciones sin utilizar ningún condicional.
let x = 0
let y = 0
//RIGHT, UP
while x < 1
print(x, y)
x = x + 1
while y < 1
print(x, y)
y = y + 1
//LEFT, LEFT, DOWN, DOWN
while x > -1
print(x, y)
x = x - 1
while y > -1
print(x, y)
y = y - 1
//RIGHT, RIGHT, RIGHT, UP, UP, UP
while x < 2
print(x, y)
x = x + 1
while y < 2
print(x, y)
y = y + 1
//LEFT, LEFT, LEFT, LEFT, DOWN, DOWN, DOWN, DOWN
while x > -2
print(x, y)
x = x - 1
while y > -2
print(x, y)
y = y - 1
La primera transformación que haremos es la introducción de una nueva variable d, para dirección, que tiene el valor +1 o -1. La dirección cambia después de cada par de bucles. Como conocemos el valor de d en todos los puntos, podemos multiplicar cada lado de cada desigualdad por él, ajustar la dirección de la desigualdad en consecuencia y simplificar cualquier multiplicación de d por una constante a otra constante. Esto nos deja con lo siguiente.
let x = 0
let y = 0
let d = 1
//RIGHT, UP
while x * d < 1
print(x, y)
x = x + d
while y * d < 1
print(x, y)
y = y + d
d = -1 * d
//LEFT, LEFT, DOWN, DOWN
while x * d < 1
print(x, y)
x = x + d
while y * d < 1
print(x, y)
y = y + d
d = -1 * d
//RIGHT, RIGHT, RIGHT, UP, UP, UP
while x * d < 2
print(x, y)
x = x + d
while y * d < 2
print(x, y)
y = y + d
d = -1 * d
//LEFT, LEFT, LEFT, LEFT, DOWN, DOWN, DOWN, DOWN
while x * d < 2
print(x, y)
x = x + d
while y * d < 2
print(x, y)
y = y + d
Ahora observamos que tanto x * d como el RHS son números enteros, por lo que podemos restar cualquier valor real entre 0 y 1 del RHS sin afectar el resultado de la desigualdad. Elegimos restar 0,5 de las desigualdades de cada otro par de bucles while para establecer más un patrón.
let x = 0
let y = 0
let d = 1
//RIGHT, UP
while x * d < 0.5
print(x, y)
x = x + d
while y * d < 0.5
print(x, y)
y = y + d
d = -1 * d
//LEFT, LEFT, DOWN, DOWN
while x * d < 1
print(x, y)
x = x + d
while y * d < 1
print(x, y)
y = y + d
d = -1 * d
//RIGHT, RIGHT, RIGHT, UP, UP, UP
while x * d < 1.5
print(x, y)
x = x + d
while y * d < 1.5
print(x, y)
y = y + d
d = -1 * d
//LEFT, LEFT, LEFT, LEFT, DOWN, DOWN, DOWN, DOWN
while x * d < 2
print(x, y)
x = x + d
while y * d < 2
print(x, y)
y = y + d
Ahora podemos introducir otra variable m para el número de pasos que damos en cada par de bucles while.
let x = 0
let y = 0
let d = 1
let m = 0.5
//RIGHT, UP
while x * d < m
print(x, y)
x = x + d
while y * d < m
print(x, y)
y = y + d
d = -1 * d
m = m + 0.5
//LEFT, LEFT, DOWN, DOWN
while x * d < m
print(x, y)
x = x + d
while y * d < m
print(x, y)
y = y + d
d = -1 * d
m = m + 0.5
//RIGHT, RIGHT, RIGHT, UP, UP, UP
while x * d < m
print(x, y)
x = x + d
while y * d < m
print(x, y)
y = y + d
d = -1 * d
m = m + 0.5
//LEFT, LEFT, LEFT, LEFT, DOWN, DOWN, DOWN, DOWN
while x * d < m
print(x, y)
x = x + d
while y * d < m
print(x, y)
y = y + d
Finalmente, vemos que la estructura de cada par de bucles while es idéntica y se puede reducir a un único bucle colocado dentro de otro bucle. Además, para evitar el uso de números con valores reales, he multiplicado el valor inicial de m; el valor m se incrementa en; y ambos lados de cada desigualdad por 2.
Esto lleva a la solución que se muestra al principio de esta respuesta.
EDITAR: Han pasado algunos años pero tuve un problema similar y escribí la siguiente solución en F# que quiero compartir. La palabra imprimir puede haber sido un nombre inapropiado en mi respuesta original, pero espero que esta versión sin pseudocódigo aborde cualquier punto planteado en los comentarios con respecto a la versatilidad y las condiciones de terminación. He agregado casos de uso de ejemplo para girar en espiral sobre un punto arbitrario y encontrar la solución correcta al problema original para iterar una matriz NxM.
let spiral =
let rec f (x, y) d m = seq {
let mutable x = x
let mutable y = y
while 2 * x * d < m do
yield x, y
x <- x + d
while 2 * y * d < m do
yield x, y
y <- y + d
yield! f (x, y) -d (m + 1)
}
f (0, 0) 1 1
spiral
|> Seq.take 5
|> List.ofSeq;;
// [(0, 0); (1, 0); (1, 1); (0, 1); (-1, 1)]
spiral
|> Seq.take 5
|> Seq.map (fun (x, y) -> x + 5, y + 5)
|> List.ofSeq;;
// [(5, 5); (6, 5); (6, 6); (5, 6); (4, 6)]
spiral
|> Seq.takeWhile (fun (x, y) -> x * x + y * y < 9)
|> Seq.filter (fun (x, y) -> -2 <= x && x <= 2 && -1 <= y && y <= 1)
|> List.ofSeq;;
// [(0, 0); (1, 0); (1, 1); (0, 1); (-1, 1); (-1, 0); (-1, -1); (0, -1); (1, -1); (2, -1); (2, 0); (2, 1); (-2, 1); (-2, 0); (-2, -1)]
Aquí hay una solución O(1) para encontrar la posición en una espiral cuadrada: Violín
function spiral(n) {
// given n an index in the squared spiral
// p the sum of point in inner square
// a the position on the current square
// n = p + a
var r = Math.floor((Math.sqrt(n + 1) - 1) / 2) + 1;
// compute radius : inverse arithmetic sum of 8+16+24+...=
var p = (8 * r * (r - 1)) / 2;
// compute total point on radius -1 : arithmetic sum of 8+16+24+...
var en = r * 2;
// points by face
var a = (1 + n - p) % (r * 8);
// compute de position and shift it so the first is (-r,-r) but (-r+1,-r)
// so square can connect
var pos = [0, 0, r];
switch (Math.floor(a / (r * 2))) {
// find the face : 0 top, 1 right, 2, bottom, 3 left
case 0:
{
pos[0] = a - r;
pos[1] = -r;
}
break;
case 1:
{
pos[0] = r;
pos[1] = (a % en) - r;
}
break;
case 2:
{
pos[0] = r - (a % en);
pos[1] = r;
}
break;
case 3:
{
pos[0] = -r;
pos[1] = r - (a % en);
}
break;
}
console.log("n : ", n, " r : ", r, " p : ", p, " a : ", a, " --> ", pos);
return pos;
}