Algorithm for iterating over an outward spiral on a discrete 2D grid from the origin

前端 未结 12 1236
独厮守ぢ
独厮守ぢ 2020-12-05 06:54

For example, here is the shape of intended spiral (and each step of the iteration)

          y
          |
          |
   16 15 14 13 12
   17  4  3  2 11
--         


        
相关标签:
12条回答
  • 2020-12-05 07:30

    It can be done in a fairly straightforward way using recursion. We just need some basic 2D vector math and tools for generating and mapping over (possibly infinite) sequences:

    // 2D vectors
    const add = ([x0, y0]) => ([x1, y1]) => [x0 + x1, y0 + y1];
    const rotate = θ => ([x, y]) => [
      Math.round(x * Math.cos(θ) - y * Math.sin(θ)),
      Math.round(x * Math.sin(θ) + y * Math.cos(θ))
    ];
    // Iterables
    const fromGen = g => ({ [Symbol.iterator]: g });
    const range = n => [...Array(n).keys()];
    const map = f => it =>
      fromGen(function*() {
        for (const v of it) {
          yield f(v);
        }
      });
    

    And now we can express a spiral recursively by generating a flat line, plus a rotated (flat line, plus a rotated (flat line, plus a rotated ...)):

    const spiralOut = i => {
      const n = Math.floor(i / 2) + 1;
      const leg = range(n).map(x => [x, 0]);
      const transform = p => add([n, 0])(rotate(Math.PI / 2)(p));
    
      return fromGen(function*() {
        yield* leg;
        yield* map(transform)(spiralOut(i + 1));
      });
    };
    

    Which produces an infinite list of the coordinates you're interested in. Here's a sample of the contents:

    const take = n => it =>
      fromGen(function*() {
        for (let v of it) {
          if (--n < 0) break;
          yield v;
        }
      });
    const points = [...take(5)(spiralOut(0))];
    console.log(points);
    // => [[0,0],[1,0],[1,1],[0,1],[-1,1]]
    

    You can also negate the rotation angle to go in the other direction, or play around with the transform and leg length to get more complex shapes.

    For example, the same technique works for inward spirals as well. It's just a slightly different transform, and a slightly different scheme for changing the length of the leg:

    const empty = [];
    const append = it1 => it2 =>
      fromGen(function*() {
        yield* it1;
        yield* it2;
      });
    const spiralIn = ([w, h]) => {
      const leg = range(w).map(x => [x, 0]);
      const transform = p => add([w - 1, 1])(rotate(Math.PI / 2)(p));
    
      return w * h === 0
        ? empty
        : append(leg)(
            fromGen(function*() {
              yield* map(transform)(spiralIn([h - 1, w]));
            })
          );
    };
    

    Which produces (this spiral is finite, so we don't need to take some arbitrary number):

    const points = [...spiralIn([3, 3])];
    console.log(points);
    // => [[0,0],[1,0],[2,0],[2,1],[2,2],[1,2],[0,2],[0,1],[1,1]]
    

    Here's the whole thing together as a live snippet if you want play around with it:

    // 2D vectors
    const add = ([x0, y0]) => ([x1, y1]) => [x0 + x1, y0 + y1];
    const rotate = θ => ([x, y]) => [
      Math.round(x * Math.cos(θ) - y * Math.sin(θ)),
      Math.round(x * Math.sin(θ) + y * Math.cos(θ))
    ];
    
    // Iterables
    const fromGen = g => ({ [Symbol.iterator]: g });
    const range = n => [...Array(n).keys()];
    const map = f => it =>
      fromGen(function*() {
        for (const v of it) {
          yield f(v);
        }
      });
    const take = n => it =>
      fromGen(function*() {
        for (let v of it) {
          if (--n < 0) break;
          yield v;
        }
      });
    const empty = [];
    const append = it1 => it2 =>
      fromGen(function*() {
        yield* it1;
        yield* it2;
      });
    
    // Outward spiral
    const spiralOut = i => {
      const n = Math.floor(i / 2) + 1;
      const leg = range(n).map(x => [x, 0]);
      const transform = p => add([n, 0])(rotate(Math.PI / 2)(p));
    
      return fromGen(function*() {
        yield* leg;
        yield* map(transform)(spiralOut(i + 1));
      });
    };
    
    // Test
    {
      const points = [...take(5)(spiralOut(0))];
      console.log(JSON.stringify(points));
    }
    
    // Inward spiral
    const spiralIn = ([w, h]) => {
      const leg = range(w).map(x => [x, 0]);
      const transform = p => add([w - 1, 1])(rotate(Math.PI / 2)(p));
    
      return w * h === 0
        ? empty
        : append(leg)(
            fromGen(function*() {
              yield* map(transform)(spiralIn([h - 1, w]));
            })
          );
    };
    
    // Test
    {
      const points = [...spiralIn([3, 3])];
      console.log(JSON.stringify(points));
    }

    0 讨论(0)
  • 2020-12-05 07:30

    Here's the algorithm. It rotates clockwise, but could easily rotate anticlockwise, with a few alterations. I made it in just under an hour.

    // spiral_get_value(x,y);
    sx = argument0;
    sy = argument1;
    a = max(sqrt(sqr(sx)),sqrt(sqr(sy)));
    c = -b;
    d = (b*2)+1;
    us = (sy==c and sx !=c);
    rs = (sx==b and sy !=c);
    bs = (sy==b and sx !=b);
    ls = (sx==c and sy !=b);
    ra = rs*((b)*2);
    ba = bs*((b)*4);
    la = ls*((b)*6);
    ax = (us*sx)+(bs*-sx);
    ay = (rs*sy)+(ls*-sy);
    add = ra+ba+la+ax+ay;
    value = add+sqr(d-2)+b;
    return(value);`
    

    It will handle any x / y values (infinite).

    It's written in GML (Game Maker Language), but the actual logic is sound in any programming language.

    The single line algorithm only has 2 variables (sx and sy) for the x and y inputs. I basically expanded brackets, a lot. It makes it easier for you to paste it into notepad and change 'sx' for your x argument / variable name and 'sy' to your y argument / variable name.

    `// spiral_get_value(x,y);
    
    sx = argument0;  
    sy = argument1;
    
    value = ((((sx==max(sqrt(sqr(sx)),sqrt(sqr(sy))) and sy !=(-1*max(sqrt(sqr(sx)),sqrt(sqr(sy))))))*((max(sqrt(sqr(sx)),sqrt(sqr(sy))))*2))+(((sy==max(sqrt(sqr(sx)),sqrt(sqr(sy))) and sx !=max(sqrt(sqr(sx)),sqrt(sqr(sy)))))*((max(sqrt(sqr(sx)),sqrt(sqr(sy))))*4))+(((sx==(-1*max(sqrt(sqr(sx)),sqrt(sqr(sy)))) and sy !=max(sqrt(sqr(sx)),sqrt(sqr(sy)))))*((max(sqrt(sqr(sx)),sqrt(sqr(sy))))*6))+((((sy==(-1*max(sqrt(sqr(sx)),sqrt(sqr(sy)))) and sx !=(-1*max(sqrt(sqr(sx)),sqrt(sqr(sy))))))*sx)+(((sy==max(sqrt(sqr(sx)),sqrt(sqr(sy))) and sx !=max(sqrt(sqr(sx)),sqrt(sqr(sy)))))*-sx))+(((sx==max(sqrt(sqr(sx)),sqrt(sqr(sy))) and sy !=(-1*max(sqrt(sqr(sx)),sqrt(sqr(sy))))))*sy)+(((sx==(-1*max(sqrt(sqr(sx)),sqrt(sqr(sy)))) and sy !=max(sqrt(sqr(sx)),sqrt(sqr(sy)))))*-sy))+sqr(((max(sqrt(sqr(sx)),sqrt(sqr(sy)))*2)+1)-2)+max(sqrt(sqr(sx)),sqrt(sqr(sy)));
    
    return(value);`
    

    I know the reply is awfully late :D but i hope it helps future visitors.

    0 讨论(0)
  • 2020-12-05 07:32

    There's nothing wrong with direct, "ad-hoc" solution. It can be clean enough too.
    Just notice that spiral is built from segments. And you can get next segment from current one rotating it by 90 degrees. And each two rotations, length of segment grows by 1.

    edit Illustration, those segments numbered

       ... 11 10
    7 7 7 7 6 10
    8 3 3 2 6 10
    8 4 . 1 6 10
    8 4 5 5 5 10
    8 9 9 9 9  9
    

    .

        // (di, dj) is a vector - direction in which we move right now
        int di = 1;
        int dj = 0;
        // length of current segment
        int segment_length = 1;
    
        // current position (i, j) and how much of current segment we passed
        int i = 0;
        int j = 0;
        int segment_passed = 0;
        for (int k = 0; k < NUMBER_OF_POINTS; ++k) {
            // make a step, add 'direction' vector (di, dj) to current position (i, j)
            i += di;
            j += dj;
            ++segment_passed;
            System.out.println(i + " " + j);
    
            if (segment_passed == segment_length) {
                // done with current segment
                segment_passed = 0;
    
                // 'rotate' directions
                int buffer = di;
                di = -dj;
                dj = buffer;
    
                // increase segment length if necessary
                if (dj == 0) {
                    ++segment_length;
                }
            }
        }
    

    To change original direction, look at original values of di and dj. To switch rotation to clockwise, see how those values are modified.

    0 讨论(0)
  • 2020-12-05 07:35

    Here is a Python implementation based on the answer by @mako.

    def spiral_iterator(iteration_limit=999):
        x = 0
        y = 0
        layer = 1
        leg = 0
        iteration = 0
    
        yield 0, 0
    
        while iteration < iteration_limit:
            iteration += 1
    
            if leg == 0:
                x += 1
                if (x == layer):
                    leg += 1
            elif leg == 1:
                y += 1
                if (y == layer):
                    leg += 1
            elif leg == 2:
                x -= 1
                if -x == layer:
                    leg += 1
            elif leg == 3:
                y -= 1
                if -y == layer:
                    leg = 0
                    layer += 1
    
            yield x, y
    

    Running this code:

    for x, y in spiral_iterator(10):
           print(x, y)
    

    Yields:

    0 0
    1 0
    1 1
    0 1
    -1 1
    -1 0
    -1 -1
    0 -1
    1 -1
    2 -1
    2 0
    
    0 讨论(0)
  • 2020-12-05 07:40

    I would solve it using some math. Here is Ruby code (with input and output):

    (0..($*.pop.to_i)).each do |i|
        j = Math.sqrt(i).round
        k = (j ** 2 - i).abs - j
        p = [k, -k].map {|l| (l + j ** 2 - i - (j % 2)) * 0.5 * (-1) ** j}.map(&:to_i)
        puts "p => #{p[0]}, #{p[1]}"
    end
    

    E.g.

    $ ruby spiral.rb 10
    p => 0, 0
    p => 1, 0
    p => 1, 1
    p => 0, 1
    p => -1, 1
    p => -1, 0
    p => -1, -1
    p => 0, -1
    p => 1, -1
    p => 2, -1
    p => 2, 0
    

    And golfed version:

    p (0..$*.pop.to_i).map{|i|j=Math.sqrt(i).round;k=(j**2-i).abs-j;[k,-k].map{|l|(l+j**2-i-j%2)*0.5*(-1)**j}.map(&:to_i)}
    

    Edit

    First try to approach the problem functionally. What do you need to know, at each step, to get to the next step?

    Focus on plane's first diagonal x = y. k tells you how many steps you must take before touching it: negative values mean you have to move abs(k) steps vertically, while positive mean you have to move k steps horizontally.

    Now focus on the length of the segment you're currently in (spiral's vertices - when the inclination of segments change - are considered as part of the "next" segment). It's 0 the first time, then 1 for the next two segments (= 2 points), then 2 for the next two segments (= 4 points), etc. It changes every two segments and each time the number of points part of that segments increase. That's what j is used for.

    Accidentally, this can be used for getting another bit of information: (-1)**j is just a shorthand to "1 if you're decreasing some coordinate to get to this step; -1 if you're increasing" (Note that only one coordinate is changed at each step). Same holds for j%2, just replace 1 with 0 and -1 with 1 in this case. This mean they swap between two values: one for segments "heading" up or right and one for those going down or left.

    This is a familiar reasoning, if you're used to functional programming: the rest is just a little bit of simple math.

    0 讨论(0)
  • Try searching for either parametric or polar equations. Both are suitable to plotting spirally things. Here's a page that has plenty of examples, with pictures (and equations). It should give you some more ideas of what to look for.

    0 讨论(0)
提交回复
热议问题