Concentric Square Ring Pattern: Peeling the Onion on Two Ways I Accidentally Solved It
There's this pattern problem that looks harmless until you actually sit down and try to generate it:
5 5 5 5 5 5 5 5 5
5 4 4 4 4 4 4 4 5
5 4 3 3 3 3 3 4 5
5 4 3 2 2 2 3 4 5
5 4 3 2 1 2 3 4 5
5 4 3 2 2 2 3 4 5
5 4 3 3 3 3 3 4 5
5 4 4 4 4 4 4 4 5
5 5 5 5 5 5 5 5 5
Concentric squares. A 1 locked in the center, growing outward ring by ring to n at the border. Simple to look at. Deceptively slippery to generate.
I want to walk through this the honest way — not the clean, confident version you write after you already know the answer, but the actual sequence of wrong turns, half-formed guesses, and the two genuinely different mental models I ended up building before it clicked. Think of it as peeling an onion: each layer looked like the final answer until I dug one level deeper and found another shape underneath.
Layer One: "It's Just Rows"
My first foothold wasn't the grid. It was a single row.
I looked at the exact middle row — the one slicing straight through the center — and read it like a sentence:
5 4 3 2 1 2 3 4 5
A lonely 1 in the center. Then 2 on both sides of it. Then 3. Then 4. Growing outward, symmetrically, until it caps out at n on both ends.
Okay, I thought, that's just a palindrome that counts down to the middle and back up. Nothing scary about a single row.
So I moved up one row:
5 4 3 2 2 2 3 4 5
Same shape — but now the smallest number, 2, isn't alone anymore. It's repeated three times in the middle. One row further up:
5 4 3 3 3 3 3 4 5
Now 3 is repeated five times.
That's when the pattern behind the pattern showed up: every row is a number, repeated some number of times, sandwiched on both sides by every number bigger than it, counting up to n.
| Row (top → center) | Center value | Repeated | Sandwich |
|---|---|---|---|
| edge row | 5 |
9 times | nothing bigger — it is the ceiling |
| next in | 4 |
7 times | 5 |
| next in | 3 |
5 times | 4, 5 |
| next in | 2 |
3 times | 3, 4, 5 |
| center row | 1 |
1 time | 2, 3, 4, 5 |
I had a rule I could actually build. For the i-th row down from the top (0-indexed), the center value is n - i, and it needs to be sandwiched by everything from n-i+1 up to n. So the row is: count down to the center value, place the center value repeated enough times to fill the middle, count back up.
row = list(range(n, n-i, -1)) + [n-i]*(2*(n-i)-1) + list(range(n-i+1, n+1))
-
range(n, n-i, -1)→ the left sandwich: counting down fromnto just above the center value -
[n-i]*(2*(n-i)-1)→ the center value, repeated exactly enough to fill the row's width at that depth -
range(n-i+1, n+1)→ the right sandwich: counting back up ton
Build that for every row from the top down to the center, and you've got the top half of the grid:
def pattern(n):
N = 2 * n - 1
upper = []
for i in range(n):
row = (list(range(n, n-i, -1))
+ [n-i] * (2*(n-i)-1)
+ list(range(n-i+1, n+1)))
upper.append(row)
for i in range(n):
print(*upper[i])
for i in range(1, n):
print(*upper[n-i-1])
And here's the part that felt like a small gift: the bottom half of the grid is the same rows, in reverse. I don't need to derive it separately — I already built it once going down. So the second loop just walks back up through upper, skipping the center row so it isn't printed twice.
At this point I was satisfied. I had working code. I understood every line. Case closed... except it wasn't, because a nagging question kept surfacing: I built this entirely in terms of rows and horizontal sandwiches — but this is supposed to be a square. Where did the "squareness" actually go into the logic? The answer was: nowhere, directly. It fell out as a side effect of the row math. That bugged me enough to keep going.
Layer Two: "Wait, It's Actually Rings"
The row-sandwich model works, but it treats the grid as a stack of independent horizontal problems. To see the actual shape underneath it, I have to back up to where this really started — because before I ever wrote a sandwich formula, my first instinct was something else entirely.
My first instinct was to think of it as a circular pattern — a ring radiating out from the center. That felt intuitive: each cell's value should just be its distance from the center.
So I tried to apply that. For any cell, I know its (x, y) position and I know where the center is — easy, right? But then it broke down immediately: distance to the center isn't uniform across a square. A cell on the edge, straight out from the center, is at one distance. A cell on the diagonal, same "ring," is at √2 times that distance. The shape I wanted was a square ring, not a circle, so "distance from center" was the wrong metric from the start — it doesn't produce flat, even rings, it produces actual circles.
That's when I reframed it: forget distance from the center — think of it as ring numbers, counting outward. The innermost ring — just the single center cell — is ring 1. The ring just outside it is ring 2, and so on, growing outward until the very last ring, ring n, is the outer boundary of the whole square.
To actually convince myself of this, I sketched it out:
I drew the outer square, and then looked at the space just inside it — the next ring in. And there it was: a corner cell of that ring and an edge-mid cell of that same ring — one sitting on the diagonal, one not — were exactly the same distance from the boundary. Not wider at the corner, not narrower along the edge. One consistent gap, wrapping the whole boundary evenly, whether you measured it from a diagonal position or not.
That consistent gap was the click. If that space is always uniform, then I don't need to think about rings as some abstract counting exercise — I can just measure it. Given the size of the whole grid, finding how far in a cell sits from the boundary is a simple calculation. And since that distance is measured from the outside in, while ring numbers grow from the inside out, the two are just mirror images of each other: the ring number is that distance, reversed — n minus however far the cell is from the edge.
That was the real unlock: the value of a cell isn't about its distance to a point, it's about its distance to a boundary — and once I could measure that distance, the whole formula fell out of it.
For a cell at (i, j) in an N × N grid, its distance from the boundary is simply the smallest of its distances to all four sides:
distance_from_left = j
distance_from_top = i
distance_from_right = N - 1 - j
distance_from_bottom = N - 1 - i
distance_from_edge = min(distance_from_left, distance_from_top,
distance_from_right, distance_from_bottom)
A corner cell is 0 distance from two edges simultaneously — no √2 involved, because we're not measuring toward a single point anymore, just toward whichever side is nearest. That single change is what makes the rings come out square instead of circular.
The last step was just relabeling that distance as a ring value. Distance-from-edge counts inward — 0 at the boundary, growing as you approach the center. Ring numbers, at least the way I'd been drawing them, count the opposite way — 1 at the center, growing as you go outward. They're mirror images of the same measurement, so:
ring_value = n - distance_from_edge
def pattern(n):
N = 2 * n - 1
for i in range(N):
row = []
for j in range(N):
distance_from_edge = min(i, j, N-1-i, N-1-j)
row.append(n - distance_from_edge)
print(*row)
No rows-as-sandwiches, no upper/lower split, no reversed lists. Every cell computes its own value, independently, from one idea: how close am I to getting out of this square?
The Two Layers, Side by Side
| Row-Sandwich Model | Ring-Distance Model | |
|---|---|---|
| Unit of thought | one row at a time | one cell at a time |
| Core question | "what number, repeated how many times, sandwiched by what?" | "how far am I from the nearest edge?" |
| Where symmetry lives | reuse rows top-to-bottom | built-in — every direction is checked equally |
| What broke my first guess | — (this was the first working model) | distance-to-center makes circles, not squares |
| Feels like | counting outward from the middle of a line | shrinking inward from the walls of a box |
Neither one is "more correct" than the other — they're two honest descriptions of the same object, discovered in a different order, through different mistakes. The row model came from staring at a line. The ring model came from staring at my own bad drawing and noticing a gap that refused to change width. Both times, the wrong first guess — a lonely row with no shape, a distance measured to a point instead of a wall — was doing useful work. It's what made the eventual correction mean something instead of just being a formula I copied from somewhere.
That, more than the code itself, is the part worth remembering: the formula is three lines. Getting to the point where it feels obvious takes a wrong turn or two on purpose.

Top comments (0)