用于从原点迭代离散2D网格上的向外螺旋的算法

时间:2010-09-14 04:58:37

标签: algorithm language-agnostic recursion iteration

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

          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]等等。返回网格中的位置为空(或者没有被数据点占用)的第一个。

网格大小没有上限。

12 个答案:

答案 0 :(得分:21)

直接的“ad-hoc”解决方案没有任何问题。它也足够干净。
请注意,螺旋是由段构建的。你可以从当前的一个旋转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的原始值。要将旋转切换为顺时针,请查看这些值的修改方式。

答案 1 :(得分:14)

这是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;
        }
    }
};

应该尽可能高效。

答案 2 :(得分:10)

这是基于答案的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/

答案 3 :(得分:7)

通过分析如何改变螺旋角的坐标可以最好地理解这个问题。考虑这个前8个螺旋角(不包括原点)的表:

 x,y   |  dx,dy  | k-th corner | N | Sign |
___________________________________________
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)
Sign = | +1 when N is Odd
       | -1 when N is Even
[dx,dy] = | [N*Sign,0]  when k is Odd
          | [0,N*Sign]  when k is Even
[X(k),Y(k)] = [X(k-1)+dx,Y(k-1)+dy]

现在,当您知道k和k + 1螺旋角的坐标时,您可以通过简单地将1或-1添加到最后一个点的x或y来获得k和k + 1之间的所有数据点。 多数民众赞成。

祝你好运。

答案 4 :(得分: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

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

高尔夫球版:

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,接下来两段({2分)为1,接下来两段为2(= 4分),等等。细分和每次细分的部分数量增加。这就是j的用途。

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

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

答案 5 :(得分:0)

尝试搜索参数或极坐标方程。两者都适合绘制螺旋形物。 Here's a page有很多例子,有图片(和方程式)。它应该给你更多的想法。

答案 6 :(得分:0)

我做的训练非常相同,输出和螺旋方向存在一些差异,并且需要额外的要求,即空间复杂度必须为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

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

答案 7 :(得分:0)

我有一个类似的问题,但我不想每次循环整个螺旋以找到下一个新坐标。要求是你知道你的最后一个坐标。

以下是我提出的关于其他解决方案的大量内容:

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语句。一些限制是需要一些修改来改变螺旋方向,不考虑非方形螺旋并且不能围绕固定坐标螺旋。

答案 8 :(得分:0)

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

// 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)。我基本上扩展括号,很多。它使您可以更轻松地将其粘贴到记事本中,并将x参数/变量名称的'sx'和y参数/变量名称的'sy'更改。

`// 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但我希望它能帮助未来的访客。

答案 9 :(得分:0)

我在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;
}

答案 10 :(得分:0)

可以使用递归以一种非常直接的方式完成。我们只需要一些基本的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);
    }
  });

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

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

outward spiral

您还可以否定旋转角度以朝另一个方向移动,或者使用变换和腿长来获得更复杂的形状。

例如,相同的技术也适用于向内螺旋。这只是一个稍有不同的变换,而用于更改腿长的方案也略有不同:

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

inward spiral

如果想玩的话,这里有完整的实时片段:

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

答案 11 :(得分:0)

这是一个基于@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