frenchtoast has asked for the wisdom of the Perl Monks concerning the following question:

Is there a good method for traversing the elements of a given grid in a inward-bound spiral path? let's say i have a 7x3 grid of characters:
  a b c d e f g
  h i j k l m n
  o p q r s t u
i'm storing the grid in a single array, though a list of lists would work equally well. so my input is ('a'..'u') and i want my output to be 'abcdefgnutsrqpohijklm', where the traversal starts at (0,0) and spirals inward clockwise.

this is supposedly used frequently in simple ciphers techniques, but i can't find any code for it. the only algorithm i can find is for the spiralling integer problem like that in node 487200, though i can't seem to adapt it.

the only method i can think of (that i'm algorithmically capable of it seems) is very ugly and probably very inefficient- remove the first row, then rotate the array counter-clockwise, and repeat.

Replies are listed 'Best First'.
Re: spiral path traversal for a grid
by mrborisguy (Hermit) on Dec 31, 2005 at 18:55 UTC

    Here's what I came up with, using an iterator. I like the iterator because once it's written, you can kindof just forget about it. You could even throw the iterator in a different file, or module if need be. It's not the most elegant code, but it works, but there is no error checking. The iterator returns undef when it's done, but will start over if you call it again.

    #!/usr/bin/perl my @box1 = ( [ qw/a b c d e f g/ ], [ qw/h i j k l m n/ ], [ qw/o p q r s t u/ ], ); my @box2 = ( [ qw/01 02 03 04 05 06 07/ ], [ qw/20 21 22 23 24 25 08/ ], [ qw/19 32 33 34 35 26 09/ ], [ qw/18 31 30 29 28 27 10/ ], [ qw/17 16 15 14 13 12 11/ ], ); my $iter1 = iterator( \@box1 ); while ( my $x = $iter1->() ) { print "$x "; } print "\n"; $iter2 = iterator( \@box2 ); while ( my $x = $iter2->() ) { print "$x "; } print "\n";
    And now the iterator.
    sub iterator { my $box = shift; # direction: # 0 => right # 1 => down # 2 => left # 3 => up my $direction = 0; my @pos = (0,0); my $up = 0; my $left = 0; my $down = ( scalar @$box ) - 1; my $right = ( scalar @{ $box->[0] } ) - 1; return sub { my $return_element = $box->[ $pos[1] ]->[ $pos[0] ]; if ( $up > $down or $left > $right ) { # reset, so if we call it after undef, it will start over. $up = 0; $left = 0; $down = ( scalar @$box ) - 1; $right = ( scalar @{ $box->[0] } ) - 1; @pos = ( 0, 0 ); $direction = 0; # return that we are done. return undef; } LABEL: { if ( $direction == 0 ) { if ( $pos[0] == $right ) { # we're traveling right and have hit the right edge. # change the direction to down. $direction++; # move our top edge (because we just got finished with # our uppermost remaining row) $up++; redo LABEL; } } if ( $direction == 1 ) { if ( $pos[1] == $down ) { # we're traveling down and have hit the bottom edge. # change the direction to left. $direction++; # move our right edge (because we just got finished wit +h # our rightmost remaining row) $right--; redo LABEL; } } if ( $direction == 2 ) { if ( $pos[0] == $left ) { # we're traveling left and have hit the leftmost edge. # change the direction to up. $direction++; # move our bottom edge (because we just got finished wi +th # our bottommost remaining row) $down--; redo LABEL; } } if ( $direction == 3 ) { if ( $pos[1] == $up ) { # we're traveling up and have hit the top edge. # change the direction to right. $direction=0; # move our left edge (because we just got finished with # our leftmost remaining row) $left++; redo LABEL; } } }; # move our position. if ( $direction == 0 ) { $pos[ 0 ]++; } elsif ( $direction == 1 ) { $pos[ 1 ]++; } elsif ( $direction == 2 ) { $pos[ 0 ]--; } elsif ( $direction == 3 ) { $pos[ 1 ]--; } return $return_element; } }

        -Bryan

Re: spiral path traversal for a grid
by negation (Beadle) on Dec 31, 2005 at 10:52 UTC

    One possible solution would be to traverse the first row until you need to stop, then change the direction and traverse downwards until you need to stop and then traverse to left and eventually up. At that point you would be one row below the starting point. Then just repeat the process until done.

    I guess that could be the way how I would implement it if I was trying to solve the problem.

    I hope that helps some
    - negation
      Very literally and verbosely so you can see what it's doing:
      #!/usr/bin/perl # # Matrix Spiral # use strict; use warnings; # # Size of input matrix in columns and rows # my $cols = 7; my $rows = 3; # # Decrement rows & columns because we're using them as array indices # (i.e. counting from 0 not 1) # $cols--; $rows--; # # Populate a list of elements to feed the matrix from # my @elements; while (<DATA>) { # # Display data to screen so we can compare results to it # print; push @elements, (split(' ', $_)); } # # Keep the size of elements for after we've shifted it empty # my $count = @elements; # # Populate a matrix of the elements # which pacman can then walk around & eat pill by pill... # my @matrix; for my $row (0 .. $rows ) { for my $col (0 .. $cols ) { $matrix[$col][$row] = shift(@elements); } } my @output; my $x = 0; # X position in matrix my $y = 0; # Y position in matrix my $v = 0; # Vertical direction my $h = 1; # Horizontal direction my $row_s = 0; # Current Row start my $row_e = $rows; # Current Row end my $col_s = 0; # Current Column start my $col_e = $cols; # Current Column end for my $place ( 0 .. $count-1 ) { # # Show X and Y coordinate in bounds "min<coord<max" # and the value at that coordinate in [ ] so we can see it work # print "$place x[$col_s<$x<$col_e] y[$row_s<$y<$row_e]", " [$matrix[$x][$y]]\n"; # # Push the element at that matrix coordinate into the output list # $output[$place] = $matrix[$x][$y]; if ( $h == 1 && $v == 0 && $x == $col_e && $y == $row_s ) { print "> reached last col. go v\n"; $h = 0; # Stop horizontal movement (was right) $v = 1; # Go down # Finished highest remaining row # so remove it from matrix bounds $row_s++; } elsif ( $h == 0 && $v == 1 && $x == $col_e && $y == $row_e ) { print "v reached last row. go <\n"; $h = -1; # Go left $v = 0; # Stop vertical movement (was down) # Finished rightmost remaining column # so remove it from matrix bounds $col_e--; } elsif ( $h == -1 && $v == 0 && $x == $col_s && $y == $row_e ) { print "< reached first col. go ^\n"; $h = 0; # Stop horizontal movement (was left) $v = -1; # Go up # Finished lowest remaining row # so remove it from matrix bounds $row_e--; } elsif ( $h == 0 && $v == -1 && $x == $col_s && $y == $row_s ) { print "^ reached first row. go >\n"; $h = 1; # Go right $v = 0; # Stop vertical movement (was up) # Finished leftmost remaining column # so remove it from matrix bounds $col_s++; } # # Increment (or decrement) matrix position pointer # $x += $h; $y += $v; } print join($/, @output), $/; __DATA__ a b c d e f g h i j k l m n o p q r s t u
      Try it with:
      my $cols = 26; my $rows = 26;
      at the top and
      __DATA__ aa ab ac ad ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw a +x ay az ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp bq br bs bt bu bv bw b +x by bz ca cb cc cd ce cf cg ch ci cj ck cl cm cn co cp cq cr cs ct cu cv cw c +x cy cz da db dc dd de df dg dh di dj dk dl dm dn do dp dq dr ds dt du dv dw d +x dy dz ea eb ec ed ee ef eg eh ei ej ek el em en eo ep eq er es et eu ev ew e +x ey ez fa fb fc fd fe ff fg fh fi fj fk fl fm fn fo fp fq fr fs ft fu fv fw f +x fy fz ga gb gc gd ge gf gg gh gi gj gk gl gm gn go gp gq gr gs gt gu gv gw g +x gy gz ha hb hc hd he hf hg hh hi hj hk hl hm hn ho hp hq hr hs ht hu hv hw h +x hy hz ia ib ic id ie if ig ih ii ij ik il im in io ip iq ir is it iu iv iw i +x iy iz ja jb jc jd je jf jg jh ji jj jk jl jm jn jo jp jq jr js jt ju jv jw j +x jy jz ka kb kc kd ke kf kg kh ki kj kk kl km kn ko kp kq kr ks kt ku kv kw k +x ky kz la lb lc ld le lf lg lh li lj lk ll lm ln lo lp lq lr ls lt lu lv lw l +x ly lz ma mb mc md me mf mg mh mi mj mk ml mm mn mo mp mq mr ms mt mu mv mw m +x my mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt nu nv nw n +x ny nz oa ob oc od oe of og oh oi oj ok ol om on oo op oq or os ot ou ov ow o +x oy oz pa pb pc pd pe pf pg ph pi pj pk pl pm pn po pp pq pr ps pt pu pv pw p +x py pz qa qb qc qd qe qf qg qh qi qj qk ql qm qn qo qp qq qr qs qt qu qv qw q +x qy qz ra rb rc rd re rf rg rh ri rj rk rl rm rn ro rp rq rr rs rt ru rv rw r +x ry rz sa sb sc sd se sf sg sh si sj sk sl sm sn so sp sq sr ss st su sv sw s +x sy sz ta tb tc td te tf tg th ti tj tk tl tm tn to tp tq tr ts tt tu tv tw t +x ty tz ua ub uc ud ue uf ug uh ui uj uk ul um un uo up uq ur us ut uu uv uw u +x uy uz va vb vc vd ve vf vg vh vi vj vk vl vm vn vo vp vq vr vs vt vu vv vw v +x vy vz wa wb wc wd we wf wg wh wi wj wk wl wm wn wo wp wq wr ws wt wu wv ww w +x wy wz xa xb xc xd xe xf xg xh xi xj xk xl xm xn xo xp xq xr xs xt xu xv xw x +x xy xz ya yb yc yd ye yf yg yh yi yj yk yl ym yn yo yp yq yr ys yt yu yv yw y +x yy yz za zb zc zd ze zf zg zh zi zj zk zl zm zn zo zp zq zr zs zt zu zv zw z +x zy zz
      at the bottom, that way it's easier to see what it's doing because the letters correspond to columns and rows.

      Also with such a large list you might want to format the output with:

      my $place; for my $row (0 .. $rows) { for my $col (0 .. $cols) { print $output[$place++], " "; } print $/; }
      instead of:
      print join($/, @output), $/;
Re: spiral path traversal for a grid
by dragonchild (Archbishop) on Dec 31, 2005 at 21:51 UTC
    use strict; use warnings; use Test::More no_plan => 1; my @input = ( [ 'a' .. 'f' ], [ 'g' .. 'l' ], [ 'm' .. 'r' ], [ 's' .. 'x' ], ); my @expected = split '', 'abcdeflrxwvutsmghijkqpon'; my @try; while (@input) { push @try, @{shift @input}; push @try, pop @$_ for @input; @input = reverse @input; @$_ = reverse @$_ for @input; } print "@expected\n"; print "@try\n"; is_deeply( \@try, \@expected );
    Someone else explain it.

    My criteria for good software:
    1. Does it work?
    2. Can someone else come in, make a change, and be reasonably certain no bugs were introduced?
Re: spiral path traversal for a grid
by jdporter (Paladin) on Dec 31, 2005 at 17:49 UTC

    The thread at Spiraling integers provides a number of techniques that could probably be used, even though the problem being addressed is slightly differently formulated.

    We're building the house of the future together.
Re: spiral path traversal for a grid
by TedPride (Priest) on Dec 31, 2005 at 18:56 UTC
    use strict; use warnings; my (@grid, $top, $bottom, $left, $right); while (<DATA>) { chomp; push @grid, [split / /]; } $top = 0; $bottom = $#grid; $left = 0; $right = $#{$grid[0]}; while (1) { print $_ for @{$grid[$top]}[$left..$right]; last if ++$top > $bottom; print $grid[$_][$right] for $top..$bottom; last if --$right < $left; print $_ for @{$grid[$bottom]}[reverse $left..$right]; last if --$bottom < $top; print $grid[$_][$left] for reverse $top..$bottom; last if ++$left > $right; } __DATA__ a b c d e f g h i j k l m n o p q r s t u
    EDIT: Whose doesn't work for 4 rows, ikegami? I tested mine with a number of different configurations, including several with 4 rows, and it seemed to work just fine.
      That doesn't work for 4 rows.