Recently, while working on a problem from the CSES Problem Set known as #1071: “Number Spiral”, I accidentally misread the problem description and instead of finding for a given , solved the inverse by finding for a given . This problem turned out to have an interesting solution based on simple mathematical reasoning and as I couldn’t find any other solutions for this variant online, I thought I’d share my solution here.
I’ve named this the “Inverse Number Spiral” problem and here is the problem description:
Problem
#: Inverse Number Spiral
Time limit: 1.00 s Memory limit: 512 MBA number spiral is an infinite grid whose upper-left square has number 1. Here are the first five layers of the spiral:
Your task is to find out the row and column given a number .
Input
The first input line contains an integer : the number of tests.
After this, there are lines, each containing an integer .
Output
For each test, print the row and column .
Constraints
Example
Input:
38115Output:
2 31 14 2
Analysis
My initial thought was that I could solve this problem by simply iterating over the spiral and checking if the current number is equal to the given number . I imagined a robot starting at position , moving like a snake—right, down, left and up—while adjusting the number of steps in each direction. However, understanding the sequence of steps proved to be quite challenging. The pattern appeared complex and difficult to comprehend, making it difficult to implement (e.g. right 1, down 1, left 1, down 1, right 2, up 2, right 1, down 3, left 3, down 1, right 4, up 4, and so on).
The time complexity of such an algorithm is . However, the number of tests is also an input parameter, which makes the overall time complexity . Given that there can be up to tests and up to input numbers, a brute-force approach could potentially require up to iterations which may cause the program to exceed the time limit.
Given these challenges, I decided to look to see whether I could find an alternative approach.
As I contemplated the spiral grid, I recognised a familiar pattern at the end of each layer of the spiral:
It was the sequence of square numbers (e.g. ). This observation sparked my curiosity and led me to wonder if I could leverage this pattern to my advantage.
I realised that the maximum value in each layer of the spiral was equal to the square of the layer number . For example, the maximum value in layer 3 was , the maximum value in layer 4 was , and so on. This meant that I could use the square root function to determine the layer number for a given value . Only the square numbers of a layer would directly produce the layer number when taking a square root, however all values in the layer including its minimum produce a decimal value greater than the previous layer number and therefore as long as we round up this value to the nearest integer (e.g. Math.ceil
) our method produces the correct layer number.
typescript
functionlayer (N : number) {returnMath .ceil (Math .sqrt (N ));}constL1 =layer (1); // 1constL2 =layer (2); // 2constL3 =layer (3); // 2constL4 =layer (4); // 2constL5 =layer (5); // 3constL6 =layer (6); // 3constL7 =layer (7); // 3constL8 =layer (8); // 3constL9 =layer (9); // 3constL10 =layer (10); // 4
Once I had a layer number , I was able to use this to determine the range of values for that layer, as for a given layer the maximum value is and the minimum value is (if you add one to the maximum value of the previous layer you get the minimum of the layer that follows it).
typescript
functionlayerRange (L : number) {conststart =Math .pow (L - 1, 2) + 1;constend =Math .pow (L , 2);return [start ,end ] asconst ;}constR1 =layerRange (1); // [1, 1]constR2 =layerRange (2); // [2, 4]constR3 =layerRange (3); // [5, 9]constR4 =layerRange (4); // [10, 16]
The way I saw it at this point was that a layer range represented a sort of one-dimensional version of each layer of the spiral. In order to determine the coordinates for a given value , I would need to be able to determine the position of within this layer, and then be able to translate that position back into coordinates.
I found that I was able to get the position of within the one-dimensional layer range quite easily by subtracting the minimum value of the layer from . For example, the position of in layer 3 was . However, in order to convert the one-dimensional position into two-dimensional coordinates within the grid, there were two further properties of the spiral that I needed to use to my advantage:
- The spiral goes in a clockwise direction in even layers and an anti-clockwise direction in odd layers.
- Depending on the position of in relation to the mid-point of the layer range and the direction of the spiral, the or coordinates are either set to the layer number or calculated. For example, the spiral travels anti-clockwise in the 5th layer and therefore when the position of within the layer range is less than or equal to its mid-point, the coordinate of is the layer number , however, when the position of within the layer range is greater than its mid-point, the coordinate of is the layer number . The inverse is true for even layers.
Solution
With all of this in mind, I was finally able to determine the coordinates for a given value , like so:
typescript
functionlayer (N : number) {returnMath .ceil (Math .sqrt (N ));}functionlayerRange (L : number) {conststart =Math .pow (L - 1, 2) + 1;constend =Math .pow (L , 2);return [start ,end ] asconst ;}functiondirection (L : number,axis : "y" | "x") {switch (axis ) {case "y": {// Determine the direction for the "y" axis based on the even/odd nature// of the layer (L). If L is even, the direction is down (1); otherwise,// it is up (-1).returnL % 2 === 0 ? 1 : -1;}case "x": {// Determine the direction for the "x" axis based on the even/odd nature// of the layer (L). If L is even, the direction is left (-1); otherwise,// it is right (1).returnL % 2 === 0 ? -1 : 1;}default: {throw newError (`Invalid axis argument supplied: ${axis }`);}}}functioncoord (N : number,axis : "y" | "x") {constL =layer (N );const [start ,end ] =layerRange (L );// The `sequenceIndex` is a zero-indexed "position" of `N` within the layer// range.constsequenceIndex =N -start ;// The `midIndex` is the zero-indexed mid-point of the layer range.constmidIndex = (end -start ) / 2;// Depending on the direction of the spiral and the axis, the coordinate// can be either (1) the layer number `L`, (2) the position of `N` computed// by starting from the beginning of the layer range, (3) the position of// `N` computed by starting from the end of the layer range and counting// backwards towards the center.//// The value of `D` might be somewhat difficult to grasp at first as it// abstracts away the direction (clockwise/anti-clockwise) and the axis// into a single value, that determines whether the coordinate is// calculated by counting forwards towards the middle of the layer range// or counting backwards from the end towards the middle of the layer// range.constD =direction (L ,axis );switch (D ) {// If the direction is down or right.case 1: {if (sequenceIndex <=midIndex ) {// For the first half of the sequence, the coordinate is simply the// `sequenceindex` incremented by one to convert it from// zero-indexed to one-indexed.return 1 +sequenceIndex ;}// For the second half of the sequence, the coordinate is the layer `L`// itself.returnL ;}// If the direction is up or left.case -1: {if (sequenceIndex >midIndex ) {// For the second half of the sequence, the coordinate is calculated// by counting back from the maximum value of the outer layer `L`// towards the center. We do this by subtracting the difference// between the `sequenceindex` and the `midIndex` from the layer `L`.returnL - (sequenceIndex -midIndex );}// For the first half of the sequence, the coordinate is the layer `L`// itself.returnL ;}default: {throw newError (`Invalid direction generated: ${direction }`);}}}functiony (N : number) {returncoord (N , "y");}functionx (N : number) {returncoord (N , "x");}functionf (N : number) {return [y (N ),x (N )] asconst ;}constF1 =f (1); // [1, 1]constF4 =f (4); // [2, 1]constF7 =f (7); // [3, 3]constF9 =f (9); // [1, 3]constF11 =f (11); // [2, 4]constF13 =f (13); // [4, 4]constF14 =f (14); // [4, 3]constF17 =f (17); // [5, 1]constF18 =f (18); // [5, 2]constF24 =f (24); // [2, 5]constF25 =f (25); // [1, 5]
Conclusion
By identifying patterns in the number spiral and leveraging mathematical relationships, we were able to transform a seemingly complex problem into a solvable one. In the process, we developed an efficient algorithm that significantly reduced the time complexity compared to a brute-force approach.
Because we are able to calculate the coordinates for a given number using only constant-time mathematical operations, the time complexity of the solution is . When we execute f(N)
for each test case in the input, the overall time complexity becomes , a substantial improvement over the brute-force approach which would have a time complexity of .
Although further optimizations, such as memoizing f(N)
to minimize duplicate calculations, could still be made, the current solution is both efficient and elegant.
Now that you've seen our approach to solving this problem, we encourage you to try your hand at the original “Number Spiral” problem. Finally, if you're looking for more challenges, the entire CSES Problem Set is an excellent resource to explore, offering a wide range of problems to hone your coding and problem-solving skills.