En bucle en espiral

Resuelto Can Berk Güder asked hace 16 años • 35 respuestas

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 )

matriz 3x3

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)

matriz 5x3

Can Berk Güder avatar Dec 30 '08 01:12 Can Berk Güder
Aceptado

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
Can Berk Güder avatar Dec 29 '2008 18:12 Can Berk Güder

¿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;
    }
}
Tom J Nowell avatar Oct 12 '2009 15:10 Tom J Nowell
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)]
Mike avatar Nov 10 '2015 21:11 Mike

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;
}
davidonet avatar Oct 10 '2013 05:10 davidonet