Forum Discussion
Formula Challenge: The most efficient way to generate a Number Spiral (Ulam spiral) ?
The goal:
The trick is creating a function capable of producing the largest matrix of numbers possible (this may rule out the recursive approach).
The approach I took:
The approach I took was creating 'frames' and working inward with REDUCE essentially performing repeated addition:
REDUCE works its way through an array of odd numbers in descending order (1 is omitted) with 3 different situations:
1. The odd number is the largest so the matrix does not require padding.
2. The odd number is 3, the anomaly, so a 'padded core' is created.
3. The odd number is not the greatest nor 3 so a padded matrix is generated
Spiral Lambda:
=LET(
s, ODD(INT(SQRT(n))),
arr, UNIQUE(ODD(SEQUENCE(s - 1, , s, -1))),
arrCore, {5, 4, 3; 6, 1, 2; 7, 8, 9},
IFERROR(
REDUCE(
0,
arr,
LAMBDA(a, v,
LET(
TopBottomPadding, EXPAND(0, (s - v) / 2, s, 0),
SidePadding, EXPAND(0, v, (s - v) / 2, 0),
top, SEQUENCE(, v, (v - 1) ^ 2 + 1, -1),
bottom, SEQUENCE(, v, (v - 1) ^ 2 + v, 1),
left_frame, EXPAND(SEQUENCE(v - 2, , (v - 1) ^ 2 + 2), , v - 1, 0),
right_frame, SEQUENCE(v - 2, , (v - 1) ^ 2 - (v - 1), -1),
core_stuffing, EXPAND(0, v, (s - v) / 2, 0),
core, VSTACK(
TopBottomPadding,
HSTACK(core_stuffing, arrCore, core_stuffing),
TopBottomPadding
),
center, HSTACK(left_frame, right_frame),
nopad, VSTACK(top, center, bottom),
pad, VSTACK(
TopBottomPadding,
HSTACK(SidePadding, VSTACK(top, center, bottom), SidePadding),
TopBottomPadding
),
a + IF(v = s, nopad, IF(v = 3, core, pad))
)
)
),
"Please enter a number 17 or greater"
)
)The accuracy checks
The highest number I've been able to get away with for n is 300,001. I'm interesting in any suggested improvements or different approaches to this task!
19 Replies
- Patrick2788Silver Contributor
I thought it would be interesting to use the Ulam Spiral to demonstrate 2D element-wise rotation within rings.
I was curious what this solution would look like in Excel. The most practical function used within is simple ring extraction from a 2D array. I can't think of any other use cases for the rotational parts. It was something I had visualized and had to complete it regardless of practicality.
// Author: Patrick H. // Date: 2/26/2026 // Title: Rotate2Dλ Demonstration /* This demonstration illustrates how Excel’s formula language is expressive enough to implement a full 2D rotational engine - a small example of Excel’s computational completeness. The larger task can be broken into smaller, solvable tasks: Extract ring by layer from a 2D grid - Ringλ | | Rotate each ring with recursion (Clockwise or Reverse) RingRotateλ, RingRotateRevλ | | Iterate through all rings in a 2D array and rotate - Rotate2Dλ ** Rotate2Dλ calls RingRotateλ or RingRotateRevλ, which call Ringλ ** Examples: A ring is a perimeter layer of the grid: the outermost cells form ring 1, the next layer inward is ring 2, and so on. Letters is comprised of 3 rings A B C D E F G H I J K L M N O P Q R S T U V W X Y Clockwise rotation by ring =Rotate2Dλ(Letters,1) F A B C D K L G H E P Q M I J U R S N O V W X Y T Reverse rotation by ring =Rotate2Dλ(Letters,1,1) B C D E J A H I N O F G M S T K L Q R Y P U V W X */ //----------------------------------------------------------------------------------- // Rotate2Dλ //----------------------------------------------------------------------------------- // Author: Patrick H. // Date: 2/18/2026 // Version: 1.0 // // Description: // Rotates all layers (rings) of a 2D grid by a specified number of rotations, // either clockwise or counterclockwise. Rotation counts are optimized based on // the perimeter of the outer ring so that no more rotation steps are performed // than mathematically necessary. // // Example: // For a 10×10 grid with 500 requested rotations: // // MOD(500, (10*2 + 10*2) - 4) = 32 // // Only 32 effective rotations are required to produce the final state. // // Parameters: // grid – Required 2D array representing the full grid. // [rotations] – Optional number of rotations. Defaults to 1 if omitted. // Negative or non-numeric values are treated as invalid. // [reverse] – Optional Boolean. If TRUE, rotates each ring in the reverse // (counterclockwise) direction. If omitted, rotation is clockwise. // // Notes: // • This wrapper applies rotation optimization globally. Each ring receives the // same effective rotation count. // • Literal rotation behavior is handled by RingRotateλ and RingRotateRevλ. // • Degenerate rings (1×N, N×1, 2×N, etc.) are safely handled within the ring // rotation functions via Ringλ and Squeezeλ. // // Lambdas called: // RingRotateλ, RingRotateRevλ // (Ringλ and Squeezeλ are called internally by those functions.) Rotate2Dλ= LAMBDA( grid, [rotations], [reverse], LET( //Dimensions h, ROWS(grid), w, COLUMNS(grid), //Set rotation count rotations, IF(ISOMITTED(rotations),1,rotations), //No-rotation scenarios IsScalar?, AND(h = 1,w = 1), IsID?, XOR(h = 1, w = 1), //Halting scenario InvalidRot?, OR(ISTEXT(rotations),rotations <= 0), //Logic gate IF(IsScalar?,grid, IF(IsID?,grid, IF(InvalidRot?,"#ROTATION!", LET( //How many rotations are needed? passes, MOD(rotations,(h*2 + w*2) - 4), //Set rotation function IsClockwise?, OR(ISOMITTED(reverse),reverse = FALSE), Rotateλ, IF(IsClockwise?,RingRotateλ,RingRotateRevλ), //Ring count layers layers, (MIN(h,w) + 1) / 2, k, SEQUENCE(layers), //Rotate each layer of 2D array Iterateλ, LAMBDA(acc,each_layer,Rotateλ(acc,each_layer,passes)), rotated, REDUCE(grid,k,Iterateλ), rotated )))))); //----------------------------------------------------------------------------------- // RingRotateRevλ //----------------------------------------------------------------------------------- // Author: Patrick H. // Date: 2/18/2026 // Version: 1.0 // // Description: // Rotates a single ring (layer) of a 2D grid in the reverse (counterclockwise) // direction while preserving the grid’s shape. The function accepts a source // grid, a target layer index, and an optional rotation count. The returned grid // matches the dimensions of the input grid. // // Parameters: // grid – Required 2D array representing the full grid. // layer – Integer ≥ 1 indicating which ring to rotate. // For an N×M grid, layer 1 is the outermost ring. // Layers exceeding the innermost ring return the grid unchanged. // [rotations] – Optional number of counterclockwise rotations. // Defaults to 1 if omitted. Negative or non-numeric values // are treated as invalid. // // Notes: // • This function performs literal rotation steps; it does not optimize rotation // counts. Optimization is handled by the Rotate2Dλ wrapper. // • Ring extraction is performed via Ringλ, which ensures correct handling of // degenerate rings (1×N, N×1, 2×N, etc.). // • Clockwise rotation is implemented in the companion function RingRotateλ. // // Lambdas called: // Ringλ, Squeezeλ RingRotateRevλ= LAMBDA( grid, layer, [rotations], LET( //Set rotations and validate rotations, IF(ISOMITTED(rotations),1,rotations), InvalidRot?, OR(ISTEXT(rotations),rotations < 0), //Mesh grid i, MGridλ(grid), j, MGridλ(grid,1), //Max dimensions h, ROWS(grid), w, COLUMNS(grid), //Ring extracted ring, Ringλ(grid,layer,""), //Ring without padding to detect 1D layers squeezed, Squeezeλ(ring,""), //No rotation scenarios Is1D?, OR(ROWS(squeezed) = 1,COLUMNS(squeezed) = 1), Two_x_Two, AND(ROWS(ring) = 2, COLUMNS(ring) = 2), traversed, IF(Two_x_Two,Traverseλ(ring,3),FALSE), IF(InvalidRot?,"#ROTATIONS!", IF(Is1D?,grid, IF(Two_x_Two,traversed, LET( //Identify ring parts for i/j offsets occupied?, LEN(ring) <> 0, //Clockwise masks top, occupied? * (i = layer) * (j <= w - layer), left, occupied? * (j = layer) * (i > layer) * (i <= h - layer + 1), right, occupied? * (j = w - layer + 1) * (i >= layer) * (i < h - layer + 1), bottom, occupied? * (i = h - layer + 1) * (j > layer) * (j <= w - layer + 1), //Local navigation function ↑↓← Decideλ, LAMBDA( logic1,true1, logic2,true2, logic3,true3, logic4,true4, fallback, // 0 for non-ring elements IF(logic1,true1,IF(logic2,true2,IF(logic3,true3,IF(logic4,true4,fallback))))), //Indices r, i + Decideλ(top,0, bottom,0, left,-1, right,1,0), c, j + Decideλ(top,1, bottom,-1, left,0, right,0,0), rotated, INDEX(grid,r,c), IF(rotations = 0,grid, RingRotateRevλ(rotated,layer,rotations - 1) ))))))); //----------------------------------------------------------------------------------- // RingRotateλ //----------------------------------------------------------------------------------- // Author: Patrick H. // Date: 2/18/2026 // Version: 1.0 // // Description: // Rotates a single ring (layer) of a 2D grid while preserving the grid’s shape. // The function accepts a source grid, a target layer index, and an optional // rotation count. // // Parameters: // grid – Required 2D array // layer – Integer ≥ 1 indicating which ring to rotate. // For an N×M grid, layer 1 is the outermost ring. // Layers exceeding the innermost ring return the grid unchanged. // [rotations] – Optional number of clockwise rotations. Defaults to 1 if omitted. // Negative or non-numeric values are treated as invalid. // // Notes: // • This function performs literal rotation steps; it does not optimize rotation // counts. Optimization is handled by the Rotate2Dλ wrapper. // • Ring extraction is performed via Ringλ, which ensures correct handling of // degenerate rings (1×N, N×1, 2×N, etc.). // • Reverse rotation is implemented in the companion function RingRotateRevλ. // // Lambdas called: // Ringλ, Squeezeλ RingRotateλ= LAMBDA( grid, layer, [rotations], LET( //Set rotations and validate rotations, IF(ISOMITTED(rotations),1,rotations), InvalidRot?, OR(ISTEXT(rotations),rotations < 0), //Mesh grid i, MGridλ(grid), j, MGridλ(grid,1), //Max dimensions h, ROWS(grid), w, COLUMNS(grid), //Ring extracted ring, Ringλ(grid,layer,""), //Ring without padding to detect 1D layers squeezed, Squeezeλ(ring,""), //No rotation scenarios Is1D?, OR(ROWS(squeezed) = 1,COLUMNS(squeezed) = 1), Two_x_Two, AND(ROWS(ring) = 2, COLUMNS(ring) = 2), traversed, IF(Two_x_Two,Traverseλ(ring,3),FALSE), IF(InvalidRot?,"#ROTATIONS!", IF(Is1D?,grid, IF(Two_x_Two,traversed, LET( //Identify ring parts for i/j offsets occupied?, LEN(ring) <> 0, //Clockwise masks top, occupied? * (i = layer) * (j > layer) * (j <= w - layer + 1), left, occupied? * (j = layer) * (i >= layer) * (i < h - layer + 1), right, occupied? * (j = w - layer + 1) * (i >= layer + 1) * (i <= h - layer + 1), bottom, occupied? * (i = h - layer + 1) * (j >= layer) * (j < w - layer + 1), //Local navigation function ↑↓← Decideλ, LAMBDA( logic1,true1, logic2,true2, logic3,true3, logic4,true4, fallback, // 0 for non-ring elements IF(logic1,true1,IF(logic2,true2,IF(logic3,true3,IF(logic4,true4,fallback))))), //Indices r, i + Decideλ(top,0, bottom,0, left,1, right,-1,0), c, j + Decideλ(top,-1, bottom,1, left,0, right,0,0), rotated, INDEX(grid,r,c), IF(rotations = 0,grid,RingRotateλ(rotated,layer,rotations - 1)) )))))); //----------------------------------------------------------------------------------- // Ringλ //----------------------------------------------------------------------------------- // Author: Patrick H. // Date: 2/17/2026 // Version: 1.0 // // Description: // Extracts a single ring (layer) from a 2D grid while preserving the overall // shape of the grid. The ring is identified by its layer index, where layer 1 // corresponds to the outermost perimeter and higher layers move inward. The // returned array matches the dimensions of the input grid, with all non-ring // cells filled using the specified fill value. // // Parameters: // grid – Required 2D array representing the full grid. // layer – Integer ≥ 1 specifying which ring to extract. // For an N×M grid, layer 1 is the outermost ring. // Layers exceeding the innermost ring return an empty array. // [fill_value] – Optional scalar used to fill all non-ring cells in the output. // Defaults to 0 if omitted. // // Notes: // • This function performs no rotation; it only extracts the ring mask. // • Degenerate rings (1×N, N×1, 2×N, etc.) are preserved exactly as extracted. // • RingRotateλ and RingRotateRevλ rely on Ringλ to correctly identify ring // boundaries before performing rotation. // • The output always matches the dimensions of the input grid. // // Used by: // RingRotateλ, RingRotateRevλ Ringλ= LAMBDA( grid, layer, [fill_value], LET( //Dimensions h, ROWS(grid), w, COLUMNS(grid), //Validation check IsScalar?, AND(h = 1, w = 1), Is1D?, OR(h = 1, w = 1), InvalidLayer?, OR(ISTEXT(layer),layer <= 0), MaxLayer, INT((MIN(h,w) + 1) / 2), //Logic gate IF(IsScalar?,grid, IF(Is1D?,"#1D-ARRAY!", IF(InvalidLayer?,"#LAYER!", IF(Layer > MaxLayer,"#MAX-LAYER: "&MaxLayer, LET( //Optional fill value fill_value, IF(ISOMITTED(fill_value),0,fill_value), //Mesh grid i, MGridλ(grid), j, MGridλ(grid,1), //Edges of ring top, ((i = layer) + (i = 1 + h - layer)) * ((j > layer - 1) * (j <= 1 + w - layer)) , side, ((j = layer) + (j = 1 + w - layer)) * ((i > layer - 1) * (i <= 1 + h - layer)), // 1/0 ring mask mask, SIGN(top + side), final, IF(mask,grid,fill_value), final ))))))); //---Utilities--- //----------------------------------------------------------------------------------- // MGridλ //----------------------------------------------------------------------------------- // Author: Patrick H. // Date: 2026‑02‑09 // Version: 1.0 // // Description: // Generates row‑index or column‑index grids matching the shape of a supplied // array. This is the Excel equivalent of MATLAB’s meshgrid or NumPy’s indices. // By default, MGridλ returns row indices. When the optional parameter is TRUE, // it returns column indices instead. // // Parameters: // grid – A 1D or 2D array whose dimensions determine the output size. // [col_index] – If TRUE, return column indices. If omitted, return row indices. // // Example: // Input grid: // {1,2,3; // 4,5,6; // 7,8,9} // // =MGridλ(grid) // {1,1,1; // 2,2,2; // 3,3,3} // // =MGridλ(grid, TRUE) // {1,2,3; // 1,2,3; // 1,2,3} // // Notes: // • Uses Resizeλ to broadcast SEQUENCE vectors across the grid dimensions. // • Works for both 1D and 2D arrays. // • Output always matches the geometry of the input grid. //----------------------------------------------------------------------------------- MGridλ= LAMBDA( grid,[col_index], LET( //Return mode RowIdx?, ISOMITTED(col_index), //Grid dimensions h, ROWS(grid), w, COLUMNS(grid), //Deferred index grids i, LAMBDA(Resizeλ(SEQUENCE(h),,w)), j, LAMBDA(Resizeλ(SEQUENCE(,w),h)), //Select row or column index grid idx, IF(RowIdx?,i(),j()), idx )); //----------------------------------------------------------------------------------- // Resizeλ //----------------------------------------------------------------------------------- // Author: Patrick H. // Date: 2/18/2026 // Version: 1.0 // // Description: // Repeats a 2D array vertically and/or horizontally by integer repeat counts. // Users may specify height, width, or both. If either parameter is omitted, // the omitted dimension defaults to 1. Geometry is preserved; no flattening // occurs. // // Example: // Input: // {1,2; // 3,4} // // =Resizeλ(array, 2, 3) // // Output: // {1,2,1,2,1,2; // 3,4,3,4,3,4; // 1,2,1,2,1,2; // 3,4,3,4,3,4} // // Parameters: // array – Required 2D array to be repeated. // [height] – Optional vertical repeat count. Defaults to 1. // [width] – Optional horizontal repeat count. Defaults to 1. // // Notes: // • Repetition is performed via index remapping, not concatenation. // • Output geometry is (h × height) by (w × width). //----------------------------------------------------------------------------------- Resizeλ= LAMBDA( array,[height],[width], LET( //Dimensions h, ROWS(array), w, COLUMNS(array), //Omission checks HeightOmitted?, ISOMITTED(height), WidthOmitted?, ISOMITTED(width), NoHWInput?, HeightOmitted? + WidthOmitted? = 2, //Default repeat counts height, IF(OR(NoHWInput?,HeightOmitted?),1,height), width, IF(OR(NoHWInput?,WidthOmitted?),1,width), //Scenarios NoRepeat?, AND(height= 1,width = 1), IsScalar?, AND(h = 1, w = 1), ExpandScalar, EXPAND(array,height,width,array), InvalidRepeat?, OR(ISTEXT(height),ISTEXT(width), OR(AND(ISNUMBER(height),height <= 0), AND(ISNUMBER(width),width <= 0))), //Logic gate IF(NoRepeat?,array, IF(IsScalar?,ExpandScalar, IF(InvalidRepeat?,array, //Proceed LET( //Expanded dimensions k, h * height, k₂, w * width, //Indices - deferred i, LAMBDA(1 + MOD(SEQUENCE(k,,0,),h) * SEQUENCE(,k₂,1,0)), j, LAMBDA(1 + MOD(SEQUENCE(,k₂,0),w) * SEQUENCE(k,,1,0)), //Construct repeated array new_arr, INDEX(array,i(),j()), new_arr )))))); //Author: Patrick H. //Date: 10/27/2025 //Version: 1.0 //Repo: Please see The Diagonal Suite for a full suite of functions. //------------------------------------------------------------------------------------------- //Traverseλ - counter_clockwise Axis Remapper //------------------------------------------------------------------------------------------- //The selected axis is remapped to the top-left traversal order. //Accepted counter_clockwises: // "NE" or 1 → Northeast (↗) // "SE" or 2 → Southeast (↘) // "SW" or 3 → Southwest (↙) //Parameters: //array → 2D input array (scalars not accepted) //new_axis → Axis counter_clockwise ("NE", "SE", "SW" or 1–3) Traverseλ = LAMBDA( array, new_axis, //Input validation IF(OR(ROWS(array)=1,COLUMNS(array)=1), "#2D-ARRAY!", IF(AND(ISNUMBER(new_axis),OR(new_axis<=0,new_axis>3)),"#AXIS!", LET( //Dimensions i, ROWS(array), j, COLUMNS(array), //Axis traversal indices (deferred) x_NE, LAMBDA(SEQUENCE(j,,1,0)*SEQUENCE(,i)), y_NE, LAMBDA(SEQUENCE(j,,j,-1)*SEQUENCE(,i,1,0)), x_SE, LAMBDA(SEQUENCE(i,,i,-1)*SEQUENCE(,j,1,0)), y_SE, LAMBDA(SEQUENCE(i,,j,0)+SEQUENCE(,j,0,-1)), x_SW, LAMBDA(SEQUENCE(j,,i,0)+SEQUENCE(,i,0,-1)), y_SW, LAMBDA(SEQUENCE(j,,1)*SEQUENCE(,i,1,0)), //Axis mode selection mode, IF(ISNUMBER(new_axis),new_axis, SWITCH(new_axis,"NE",1,"SE",2,"SW",3,1)), //Index selection x, CHOOSE(mode,x_NE,x_SE,x_SW), y, CHOOSE(mode,y_NE,y_SE,y_SW), //Unwrap indices and get results result, INDEX(array,x(),y()), result )))); //----------------------------------------------------------------------------------- // Squeezeλ //----------------------------------------------------------------------------------- // Description: // Removes null and straight zero vertical or horizontal vectors from a 2D array. // Supports optional custom criteria for removal. //----------------------------------------------------------------------------------- // Parameters: // array - array (1D or 2D) where rows or columns are removed. // Optional parameters: // custom_criteria - scalar criteria for removal of rows or columns. // suppress_zeros - Set to TRUE will replace 0s with empty "" in final result. //----------------------------------------------------------------------------------- Squeezeλ= LAMBDA( array, [custom_criteria], [suppress_zeros], LET( HideZero?, NOT(ISOMITTED(suppress_zeros)), //Shape i, SEQUENCE(ROWS(array)), j, SEQUENCE(,COLUMNS(array)), //Retain non-blank rows, columns. Straight zero vectors are purged. Keepλ, LAMBDA(v,NOT(AND(TOCOL(v,2)=0))), //Custom criteria Customλ, LAMBDA(v,NOT(AND(TOCOL(v,2)=custom_criteria))), //Function selectino fn, IF(ISOMITTED(custom_criteria),Keepλ,Customλ), a, TOCOL(IF(BYROW(array,fn),i,NA()),2), b, TOROW(IF(BYCOL(array,fn),j,NA()),2), IsEmpty?, AND(ISERROR(a),ISERROR(b)), //Reset shape x, ROWS(a), y, COLUMNS(b), //Mesh grid r, a * SEQUENCE(,y,1,0), c, b * SEQUENCE(x,,1,0), //Get results, suppress 0s result, INDEX(array,r,c), result_b, IF(result=0,"",result), deliver, IF(IsEmpty?,"#EMPTY-ARRAY!",IF(HideZero?,result_b,result)), deliver ));Attaching a link in case the attachment is zapped: Excel-Lambda-Playground/Rotate2Dλ Demo.xlsx at main · Patrick2788/Excel-Lambda-Playground
- Samir2025Copper Contributor
Hi sorry to disturb , but how to post a question in the forum ? thanks
- Patrick2788Silver Contributor
One way to speed up the spiral is to use thunks/lazy evaluation all the way down:
Spiralλ = LAMBDA(n, [start_at], LET( start_at, IF(ISOMITTED(start_at), 1, start_at), i, LAMBDA(SEQUENCE(n * 2 + 1, , n, -1)), j, LAMBDA(SEQUENCE(, n * 2 + 1, -n, 1)), x, LAMBDA(j() * SEQUENCE(n * 2 + 1, , 1, 0)), y, LAMBDA(i() * SEQUENCE(, n * 2 + 1, 1, 0)), a_, LAMBDA((ABS(x()) > ABS(y())) * (x() > 0)), b_, LAMBDA((ABS(x()) > ABS(y())) * (x() <= 0)), c_, LAMBDA((ABS(x()) <= ABS(y())) * (y() > 0)), d_, LAMBDA((ABS(x()) <= ABS(y())) * (y() <= 0)), a, LAMBDA(4 * x() ^ 2 - 3 * x() + y()), b, LAMBDA(4 * x() ^ 2 - x() - y()), c, LAMBDA(4 * y() ^ 2 - y() - x()), d, LAMBDA(4 * y() ^ 2 - 3 * y() + x()), result, start_at + IF(a_(), a(), IF(b_(), b(), IF(c_(), c(), d()))), result ) );This generates a spiral with 51,854,401 elements in about 78 seconds.
- Patrick2788Silver Contributor
This is my rendition for 2024 with no Lambda helper functions.
//Patrick2788 11/21/2024 //Create a Ulam Spiral by supplying cartesian coordinates. //Parameters: //rings - Create a spiral with this many rings from the center starting value //[start_at] - Specify a starting number. Defaults to 1 if omitted. Spiralλ = LAMBDA(rings,[start_at], LET( start_at,IF(ISOMITTED(start_at),1,start_at), //Create cartesian coordindates i, SEQUENCE(rings * 2 + 1, , rings, -1), j, SEQUENCE(, rings * 2 + 1, -rings, 1), //Broadcast cartesian coordinates to matrices x, j * SEQUENCE(rings * 2 + 1, , 1, 0), y, i * SEQUENCE(, rings * 2 + 1, 1, 0), //Four scenarios for Four quadrants: //a_ - right b_ - left c_ - top d_ - bottom a_, (ABS(x) > ABS(y)) * (x > 0), b_, (ABS(x) > ABS(y)) * (x <= 0), c_, (ABS(x) <= ABS(y)) * (y > 0), d_, (ABS(x) <= ABS(y)) * (y <= 0), //Calculations for each quadrant: a, 4 * x ^ 2 - 3 * x + y, b, 4 * x ^ 2 - x - y, c, 4 * y ^ 2 - y - x, d, 4 * y ^ 2 - 3 * y + x, //Check cartesian coordinates, apply calculations result, start_at+IF(a_, a, IF(b_, b, IF(c_,c, d))), result ) ); - djclementsSilver Contributor
Patrick2788 Interesting challenge. I took a slightly different approach, starting from the center of the spiral and working my way outwards, offsetting the row or column number by either 1 or -1 at each step. I used TOCOL / IF / SEQUENCE as the primary method of array manipulation, then SORTBY to sort the numeric sequence by row and column number, and WRAPROWS to output the final results. Due to the row limitations of the SEQUENCE function, the maximum output size is 1023 rows by 1023 columns (1,046,529 numbers), but it only takes 3 seconds to process.
The complete definition of the LAMBDA function is:
ULAMSPIRAL: =LAMBDA(rows, LET( n, rows + ISEVEN(rows), adj, DROP(TOCOL(IF({1,1}, SEQUENCE(n))), -1), row, DROP(TOCOL(IF(SEQUENCE(n), {0,1})), -1), off, DROP(DROP(TOCOL(IF(SEQUENCE(n), {1,1,-1,-1})), 1), -2), rId, DROP(TOCOL(IFS(adj >= SEQUENCE(, MAX(adj)), SEQUENCE(ROWS(adj))), 2), -1), r, INDEX(row, rId), k, INDEX(off, rId), s, ROUNDUP(n/2, 0), POS, LAMBDA(int,arr, VSTACK(int, SCAN(int, arr, LAMBDA(a,v, a+v)))), spiral, WRAPROWS(SORTBY(SEQUENCE(n^2), POS(s, r*k), 1, POS(s, NOT(r)*k), 1), n), DROP(spiral, rows-n, n-rows) ) )It's set up a little different, in that you enter the total number of rows to be generated (n=rows^2). Entering an odd number of rows will generate a perfect spiral, whereas an even number of rows will be truncated (the last row and first column will be dropped)...
- djclementsSilver Contributor
Patrick2788 Sorry, I wasn't happy with the row limitations of my first try, so I came up with another version using the MAKEARRAY function:
ULAMSPIRAL: =LAMBDA(rows, LET( n, rows + ISEVEN(rows), s, ROUNDUP(n / 2, 0), spiral, MAKEARRAY(n, n, LAMBDA(r,c, LET( off_r, r - s, off_c, c - s, lvl, MAX(ABS(off_r), ABS(off_c)) + 1, lvl_n, lvl * 2 - 1, lvl_r, lvl + off_r, lvl_c, lvl + off_c, IFS( lvl_r = lvl_n, lvl_n ^ 2 - lvl_n + lvl_c, lvl_c = 1, lvl_n ^ 2 - lvl_n * 2 + lvl_r + 1, lvl_r = 1, lvl_n ^ 2 - lvl_n * 2 - lvl_c + 3, TRUE, lvl_n ^ 2 - lvl_n * 3 - lvl_r + 4)))), IF(ISODD(rows), spiral, DROP(spiral, -1, 1)) ) )Basically, the value of each cell is determined by its row and column number in relation to the center of the spiral. Surprisingly, this version was more efficient that my first, returning a 1023x1023 spiral in only 2 seconds. The maximum size I was able to achieve was a 7327x7327 spiral (53,684,929 numbers) in 2 minutes and 7 seconds. Attempting 7328 rows and above returned the "Excel ran out of resources" error message.
- ShayCapehartCopper Contributor
Very impressive. Building the list in one pass based on the four diagonal quadrants is the way to go. Did you happen to make any comparisons with the formula that I submitted? Sheets doesn't allow more than 10 million cells, plus I think making comparisons using the same platform would be nice.
- ShayCapehartCopper Contributor
I found it best to avoid any iteration in order to not reach the calculation limit so quickly. This formula can return an 1120x1120 in less than 30 seconds. I must admit, I'm using Google Sheets, but I believe there's no compatibility issues with this formula. I'm not really sure what it's limit is in Sheets or Excel yet.
=let(n,1120,i,sequence(n),j,sequence(1,n), index(n^2- if(i<j+1, if(j<=n-i+1, 4*n*i-4*i^2+7*i-4*n+j-4, 4*n*j-4*j^2+3*j+i-2*n-2), if(j<n-i+1, 4*n*j-4*j^2+j-i, 4*n*i-4*i^2-2*n-j+5*i-2))))- Patrick2788Silver Contributor
This is terrific! May I ask how much time you've invested in studying the Ulam spiral?Any time I post one of these out of the box problem-solving discussions I don't expect a lot of replies (It gives me hope that there may be an audience for the fractals Lambda I made months ago.).
I hadn't looked at this task since I posted it in July but would brainstorm alternatives solutions every so often.
For example:
-Create a sparse array and have SCAN finish the matrix by filling in the missing values
-Create an array of R/C coordinates in relation to the origin (1). There's a pattern to the coordinates but it's a slog.
-Recursive wrapping/flattening. I think this is theoretically possible but it's not terribly efficient.It's funny I was messing with MAKEARRAY on/off this morning but being interrupted too much to finish it. I like this solution. MAKEARRAY gets a bad rap for being 'slow' but it's not a bad option when a matrix needs to be created where a target array doesn't exist. Also, MAKEARRAY doesn't need to stack.
- ShayCapehartCopper Contributor
Thanks for responding. About a month ago, someone needed some help creating the reverse spiral, or Pyramid Ascent as I like to call it. As you can imagine, that quickly turned into seeing who could come up with the shortest/fastest/most robust formula. Sheets kept reaching a calculation limit, and I knew that was highly affected by the use of LAMBDA helper functions.
After an all-nighter, I came up with this direct approach.
=lambda(n, makearray(n,n,lambda(i,j, let(a,min(n-i,j-1,i-1,n-j), b,if(i>j,4*n-j-i-6*a,j+i-2*a), 4*a*(n-a)+b-1))))It was still hitting the calc limit at 219 rows, so last week I was determined to:
- Get rid of MIN, since it's generating all four values at each coordinate.
- Remove the final LAMBDA helper function, MAKEARRAY.
That led to a formula similar to the one I subitted here. I thought that I could easily convert the Pyramid Ascent into the Ulam Spiral by simply adding n^1-1, but I now recognize that the Ulam Spiral is set by the spiral direction starting at 1 vs the Pyramid Ascent, which always begins with 1 in the upper left corner. I'm currently working on that fix.
As for any future improvements to efficiency, here's my thought. If the final matrix is generated row by row, would it be more efficient if at each row, stack together three arrays of numbers, which would avoid a bunch of conditional tests to determine which quadrant each coordinates is in. I've tried visualizing this below:
I've made a Named Function called SPIRAL that outputs the spiral (fix still pending from above) given the side length and type of spiral. This https://docs.google.com/spreadsheets/d/1hU4ykuXKvhKuvH5WRV0K-z25f4DjHEmNyfWljiHY_UI/edit?usp=sharing also includes some nice extras to visualize the prime numbers and demonstrate that it may not be the primes that have a pattern, but rather the non-primes. And when you remove a pattern from a canvas, what remains can appear to be a pattern.
- mtarlerSilver Contributor
interesting. I already did something like this for that Fibonacci sequence challenge. I will have to pull out that file and see how I did it then
Ok so I used an approach inside out and here it is modified for this solution:
=IF(n > 1, LET(turns, 2 * INT(SQRT(n)) - 1, s, SEQUENCE(turns), all, REDUCE({1, 2},s,LAMBDA(p, q, CHOOSE( MOD(q, 4) + 1, HSTACK(p, SEQUENCE(ROWS(p), 1, MAX(p) + ROWS(p) + 1, -1)), VSTACK(SEQUENCE(1, COLUMNS(p), COLUMNS(p) + MAX(p), -1), p), HSTACK(SEQUENCE(ROWS(p), 1, MAX(p) + 1), p), VSTACK(p, SEQUENCE(1, COLUMNS(p), MAX(p) + 1)) ) ) ), final, IF(all > n, "", all), final ), "Parameter 'n' must be >=1" )it is far from optimized as excel has to find the MAX each loop when I could potentially add some parameters to the accumulator to pass the max and # rows/columns and even the next position
regardless it works relatively well and can do significant numbers (just how long you want to wait).
- Patrick2788Silver ContributorIt's close. It looks like 10 is being skipped. I played with adjusting the 2nd VSTACK within the CHOOSE to remove the +1, this adds 10 back to the matrix but then causes a duplication. It's a good start!
- mtarlerSilver Contributor
Patrick2788 dang I thought I fixed that. the problem was on the first hstack. When you add ROWS() you don't need the extra +1. i fixed it on the vstack that uses COLUMNS() and thought I fixed it on that hstack but either forgot or lost the change when I did something else. Check the attached