【问题标题】:Algorithm for iterating over an outward spiral on a discrete 2D grid from the origin从原点开始在离散二维网格上迭代向外螺旋的算法
【发布时间】:2011-04-11 23:55:25
【问题描述】:

例如,这里是预期螺旋的形状(以及迭代的每一步)

          y
          |
          |
   16 15 14 13 12
   17  4  3  2 11
-- 18  5  0  1 10 --- x
   19  6  7  8  9
   20 21 22 23 24
          |
          |

线条是 x 和 y 轴。

以下是算法在每次迭代中“返回”的实际值(点的坐标):

[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,2], [1,2], [0,2], [-1,2], [-2,2], [-2,1], [-2,0]..

等等

我尝试过搜索,但我不确定到底要搜索什么,而且我尝试过的搜索出现了死胡同。

我什至不知道从哪里开始,除了一些杂乱、不雅和临时的东西,比如为每一层创建/编码一个新的螺旋。

谁能帮我开始?

另外,有没有一种方法可以轻松地在顺时针和逆时针(方向)之间切换,以及从哪个方向“开始”螺旋? (旋转)

另外,有没有办法递归地做到这一点?


我的应用程序

我有一个填充了数据点的稀疏网格,我想在网格中添加一个新数据点,并让它“尽可能接近”给定的其他点。

为此,我将调用grid.find_closest_available_point_to(point),它将遍历上面给出的螺旋并返回第一个空且可用的位置。

首先,它会检查point+[0,0](为了完整起见)。然后它会检查point+[1,0]。然后它会检查point+[1,1]。然后point+[0,1] 等。并返回第一个在网格中的位置为空(或尚未被数据点占用)的位置。

网格大小没有上限。

【问题讨论】:

  • 我已经这样做了,但是看不懂你给出的输出示例
  • 听起来像是一个代码高尔夫问题...
  • @alcuadrado 首先,它返回原点。然后它返回点 [1,0]。然后它逆时针“旋转”并返回点 [1,1]。我会尽量让它更清楚
  • 这个问题是否与 ProjectEuler 问题有关?
  • @Arak 不是;我将在帖子中阐明我的目的。

标签: algorithm language-agnostic recursion iteration


【解决方案1】:

直接的“临时”解决方案没有任何问题。它也可以足够干净。
请注意,螺旋是由段构成的。你可以从当前的一个片段中得到下一个片段,将它旋转 90 度。并且每旋转两次,段的长度增加1。

编辑插图,这些段编号

   ... 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;
            }
        }
    }

要更改原始方向,请查看 didj 的原始值。要将旋转切换为顺时针,请查看这些值是如何修改的。

【讨论】:

  • 只是因为个人原因我不喜欢 i,j,k :) ... i=x, j=y, k=n, 其中 n 是坐标数。
  • 我喜欢这种方法 =) 它很容易实现,也是我目前正在使用的方法。不过,出于好奇,我会等着看是否有其他人出现,然后再选择此作为最佳答案。
【解决方案2】:

这是 C++ 中的一个尝试,一个有状态的迭代器。

class SpiralOut{
protected:
    unsigned layer;
    unsigned leg;
public:
    int x, y; //read these as output from next, do not modify.
    SpiralOut():layer(1),leg(0),x(0),y(0){}
    void goNext(){
        switch(leg){
        case 0: ++x; if(x  == layer)  ++leg;                break;
        case 1: ++y; if(y  == layer)  ++leg;                break;
        case 2: --x; if(-x == layer)  ++leg;                break;
        case 3: --y; if(-y == layer){ leg = 0; ++layer; }   break;
        }
    }
};

应该尽可能高效。

【讨论】:

【解决方案3】:

这是基于答案的javascript解决方案 Looping in a spiral

var x = 0,
    y = 0,
    delta = [0, -1],
    // spiral width
    width = 6,
    // spiral height
    height = 6;


for (i = Math.pow(Math.max(width, height), 2); i>0; i--) {
    if ((-width/2 < x && x <= width/2) 
            && (-height/2 < y && y <= height/2)) {
        console.debug('POINT', x, y);
    }

    if (x === y 
            || (x < 0 && x === -y) 
            || (x > 0 && x === 1-y)){
        // change direction
        delta = [-delta[1], delta[0]]            
    }

    x += delta[0];
    y += delta[1];        
}

小提琴:http://jsfiddle.net/N9gEC/18/

【讨论】:

    【解决方案4】:

    这个问题最好通过分析螺旋角坐标的变化来理解。考虑这张前 8 个螺旋角的表格(不包括原点):

    x,y | dx,dy |第 k 个角 | N |登录 | ___________________________________________ 1,0 | 1,0 | 1 | 1 | + 1,1 | 0,1 | 2 | 1 | + -1,1 | -2,0 | 3 | 2 | - -1,-1 | 0,-2 | 4 | 2 | - 2,-1 | 3,0 | 5 | 3 | + 2,2 | 0,3 | 6 | 3 | + -2,2 | -4,0 | 7 | 4 | - -2,-2 | 0,-4 | 8 | 4 | -

    通过查看此表,我们可以计算给定 (k-1) 个角的 X,Y 的第 k 个角的 X,Y:

    N = INT((1+k)/2) 标志 = |当 N 为奇数时 +1 | -1 当 N 为偶数时 [dx,dy] = | [N*Sign,0] 当 k 为奇数时 | [0,N*Sign] 当 k 为偶数时 [X(k),Y(k)] = [X(k-1)+dx,Y(k-1)+dy]

    现在,当您知道 k 和 k+1 螺旋角的坐标时,您只需将 1 或 -1 添加到最后一个点的 x 或 y 即可获得 k 和 k+1 之间的所有数据点。 就是这样。

    祝你好运。

    【讨论】:

      【解决方案5】:

      我会用一些数学来解决它。这是 Ruby 代码(带输入和输出):

      (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
      

      例如

      $ 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
      

      打高尔夫球的版本:

      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)}
      

      编辑

      首先尝试从功能上解决问题。在每个步骤中,您需要了解什么才能进入下一步?

      关注平面的第一条对角线x = yk 告诉你在触摸它之前必须走多少步:负值意味着你必须垂直移动abs(k) 步,而正值意味着你必须水平移动k 步。

      现在关注您当前所在的线段的长度(螺旋的顶点 - 当线段的倾斜度发生变化时 - 被视为“下一个”线段的一部分)。第一次是0,然后是1 用于接下来的两个段(= 2 分),然后是2 用于接下来的两个段(= 4 分),依此类推。它每两个段更改一次,并且每次数字该段的点部分增加。这就是j 的用途。

      无意中,这可用于获取另一位信息:(-1)**j 只是“1 如果您正在减少某个坐标以到达此步骤;-1 如果您正在增加”的简写(请注意,每一步仅更改一个坐标)。同样适用于j%2,在这种情况下只需将1 替换为0 并将-1 替换为1。这意味着它们在两个值之间交换:一个用于“向上或向右”的片段,另一个用于向下或向左的片段。

      如果您习惯于函数式编程,这是一个熟悉的推理:其余的只是一些简单的数学运算。

      【讨论】:

      • 非常好的答案,唯一一个不使用循环的。好工作!但为什么是“高尔夫版”?这应该是“禁止的”(除非在 IOCCC :)。像这样的代码是一个很好的例子,它需要比实际代码多 2 倍的 cmets 和 doc 行。
      • 干得好伙计,我刚刚完成了螺旋图案设计,但你的代码比我的要小得多...感谢..1+
      【解决方案6】:

      可以使用递归以相当简单的方式完成。我们只需要一些基本的 2D 矢量数学和工具来生成和映射(可能是无限的)序列:

      // 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);
          }
        });
      

      现在我们可以通过生成一条扁平线,加上一个旋转的(flat line,加上一个旋转的(flat line,加上一个旋转的...))来递归地表达一个螺旋:

      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));
        });
      };
      

      这会生成您感兴趣的坐标的无限列表。以下是内容示例:

      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]]
      

      您还可以取消旋转角度以向另一个方向移动,或者调整变换和腿长以获得更复杂的形状。

      例如,同样的技术也适用于内向螺旋。这只是一个稍微不同的变换,以及改变腿长的一个稍微不同的方案:

      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]));
              })
            );
      };
      

      哪个产生(这个螺旋是有限的,所以我们不需要take某个任意数字):

      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]]
      

      如果你想玩的话,这就是一个现场 sn-p 的全部内容:

      // 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));
      }

      【讨论】:

        【解决方案7】:

        尝试搜索参数或极坐标方程。两者都适合绘制螺旋状的东西。 Here's a page 有很多例子,有图片(和方程式)。它应该给你一些关于寻找什么的更多想法。

        【讨论】:

          【解决方案8】:

          我已经完成了与训练练习几乎相同的操作,只是在输出和螺旋方向方面存在一些差异,并且还有一个额外要求,即函数的空间复杂度必须为 O(1)。

          经过一段时间的思考,我想到了通过知道螺旋从哪里开始以及我计算值的位置,我可以通过减去螺旋的所有完整“圆”来简化问题,然后只需计算一个更简单的值。

          这是我在 ruby​​ 中对该算法的实现:

          def print_spiral(n)
            (0...n).each do |y|
              (0...n).each do |x|
                printf("%02d ", get_value(x, y, n))
              end
              print "\n"
            end
          end
          
          
          def distance_to_border(x, y, n)
            [x, y, n - 1 - x, n - 1 - y].min
          end
          
          def get_value(x, y, n)
            dist = distance_to_border(x, y, n)
            initial = n * n - 1
          
            (0...dist).each do |i|
              initial -= 2 * (n - 2 * i) + 2 * (n - 2 * i - 2)
            end        
          
            x -= dist
            y -= dist
            n -= dist * 2
          
            if y == 0 then
              initial - x # If we are in the upper row
            elsif y == n - 1 then
              initial - n - (n - 2) - ((n - 1) - x) # If we are in the lower row
            elsif x == n - 1 then
              initial - n - y + 1# If we are in the right column
            else
              initial - 2 * n - (n - 2) - ((n - 1) - y - 1) # If we are in the left column
            end
          end
          
          print_spiral 5
          

          这不完全是你要求的,但我相信它会帮助你思考你的问题

          【讨论】:

            【解决方案9】:

            我也遇到过类似的问题,但我不想每次都遍历整个螺旋线来寻找下一个新坐标。要求是你知道你的最后一个坐标。

            这是我通过大量阅读其他解决方案得出的结论:

            function getNextCoord(coord) {
            
                // required info
                var x     = coord.x,
                    y     = coord.y,
                    level = Math.max(Math.abs(x), Math.abs(y));
                    delta = {x:0, y:0};
            
                // calculate current direction (start up)
                if (-x === level)
                    delta.y = 1;    // going up
                else if (y === level)
                    delta.x = 1;    // going right
                else if (x === level)        
                    delta.y = -1;    // going down
                else if (-y === level)
                    delta.x = -1;    // going left
            
                // check if we need to turn down or left
                if (x > 0 && (x === y || x === -y)) {
                    // change direction (clockwise)
                    delta = {x: delta.y, 
                             y: -delta.x};
                }
            
                // move to next coordinate
                x += delta.x;
                y += delta.y;
            
                return {x: x,
                        y: y};
            }
            
            coord = {x: 0, y: 0}
            for (i = 0; i < 40; i++) {
                console.log('['+ coord.x +', ' + coord.y + ']');
                coord = getNextCoord(coord);  
            
            }
            

            仍然不确定这是否是最优雅的解决方案。也许一些优雅的数学可以删除一些 if 语句。一些限制是需要一些修改来改变螺旋方向,不考虑非方形螺旋并且不能围绕固定坐标螺旋。

            【讨论】:

              【解决方案10】:

              我在 java 中有一个算法,它输出与你相似的输出,只是它优先考虑右边的数字,然后是左边的数字。

                public static String[] rationals(int amount){
                 String[] numberList=new String[amount];
                 int currentNumberLeft=0;
                 int newNumberLeft=0;
                 int currentNumberRight=0;
                 int newNumberRight=0;
                 int state=1;
                 numberList[0]="("+newNumberLeft+","+newNumberRight+")";
                 boolean direction=false;
               for(int count=1;count<amount;count++){
                 if(direction==true&&newNumberLeft==state){direction=false;state=(state<=0?(-state)+1:-state);}
                 else if(direction==false&&newNumberRight==state){direction=true;}
                 if(direction){newNumberLeft=currentNumberLeft+sign(state);}else{newNumberRight=currentNumberRight+sign(state);}
                 currentNumberLeft=newNumberLeft;
                 currentNumberRight=newNumberRight;
                 numberList[count]="("+newNumberLeft+","+newNumberRight+")";
               }
               return numberList;
              }
              

              【讨论】:

                【解决方案11】:

                这是算法。它顺时针旋转,但可以很容易地逆时针旋转,只需进行一些改动。我在不到一个小时内就完成了。

                // 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);`
                

                它将处理任何 x / y 值(无限)。

                它是用 GML(游戏制作语言)编写的,但实际逻辑在任何编程语言中都是合理的。

                单行算法对于 x 和 y 输入只有 2 个变量(sx 和 sy)。我基本上扩大了括号,很多。它使您可以更轻松地将其粘贴到记事本中,并将“sx”更改为 x 参数/变量名,将“sy”更改为 y 参数/变量名。

                `// 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);`
                

                我知道回复太晚了 :D 但我希望它对未来的访客有所帮助。

                【讨论】:

                • 您可以在您的个人资料中添加您的个人详细信息。头像已在此帖子中显示您的名字。
                【解决方案12】:

                这是基于@mako 回答的 Python 实现。

                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
                

                运行此代码:

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

                产量:

                0 0
                1 0
                1 1
                0 1
                -1 1
                -1 0
                -1 -1
                0 -1
                1 -1
                2 -1
                2 0
                

                【讨论】:

                  猜你喜欢
                  • 2016-12-17
                  • 2018-05-18
                  • 1970-01-01
                  • 2015-06-10
                  • 1970-01-01
                  • 2012-02-17
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多