从原点开始在离散二维网格上迭代向外螺旋的算法

Posted

技术标签:

【中文标题】从原点开始在离散二维网格上迭代向外螺旋的算法【英文标题】: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 不是;我将在帖子中阐明我的目的。 【参考方案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;
        
    
;

应该尽可能高效。

【讨论】:

这是此算法在 Godot GDScript 中的一个版本:gist.github.com/dmlo/0662bbafb125e4174fc28c60273f56d7【参考方案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);elsenewNumberRight=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

【讨论】:

以上是关于从原点开始在离散二维网格上迭代向外螺旋的算法的主要内容,如果未能解决你的问题,请参考以下文章

项目管理-软件开发模式对比(瀑布迭代螺旋敏捷)

迭代模型和敏捷模型的区别/迭代模型和螺旋模型的区别

如何迭代栅格网格中的环?

迭代原型螺旋敏捷模型之间的区别

顺时针和逆时针螺旋打印二维数组(行列式)

python 学习第四天