Matrix Diagonal Traversal
Every cell on the same anti-diagonal shares the same row+col sum. That one insight turns a confusing 2D problem into a clean 1D sweep — and it's the key to diagonal sort, anti-diagonal grouping, and the Diagonal Traverse problem.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- Watch it happen, frame by frame
- Now, the code — line by line
- Why is it always correct?
- When should I reach for this? (the trigger list)
- The same trick in three disguises
- Disguise 1 — Sort the Matrix Diagonally (LC #1329)
- Disguise 2 — Anti-diagonal groups in DP
- Disguise 3 — N-Queens diagonal conflict check
- Traps that catch beginners
- Say it like a pro (interview one-liner)
- Check yourself
- Practice problems
Before we start
By the end of this page you will be able to:
- See why all cells on the same anti-diagonal share a single number (
row + col). - Explain out loud how to collect diagonals in order using that key.
- Recognise this pattern in diagonal sort, anti-diagonal grouping, and the classic Diagonal Traverse problem.
Stop at every Pause & Think box. The key insight here is a small but surprising mathematical observation — give it a moment to land.
Picture this first (no code yet)
A real-life story
Imagine a hillside with rain gutters cut diagonally — each gutter runs from upper-right to lower-left. A raindrop falls anywhere on the hill and slides straight down its gutter to the collection pipe at the bottom.
Here is the surprising fact: every gutter has a unique number painted at the pipe, and the number tells you exactly which gutter it is. You can calculate a raindrop's gutter number the moment it lands — without tracing the path — because the gutter number is simply the raindrop's column position plus its row position.
So raindrops at (row=0, col=2), (row=1, col=1), and (row=2, col=0) all land in gutter 2 because 0+2 = 1+1 = 2+0 = 2. They flow together.
That gutter number — row + col — is the diagonal key. Every anti-diagonal in a matrix is uniquely identified by it. The whole pattern is: group cells by their key, then read each group.
The actual problem
Given an m × n matrix, return all elements collected anti-diagonal by anti-diagonal, alternating direction (up-right, then down-left, then up-right…).
input:
[[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
output: [1, 2, 4, 7, 5, 3, 6, 8, 9]
Anti-diagonal 0: {(0,0)} → [1] (up → direction: single cell)
Anti-diagonal 1: {(0,1),(1,0)} → [2,4] (going down-left)
Anti-diagonal 2: {(0,2),(1,1),(2,0)} → [7,5,3] (going up-right — reversed)
Anti-diagonal 3: {(1,2),(2,1)} → [6,8] (going down-left)
Anti-diagonal 4: {(2,2)} → [9] (single cell)
Notice: even-numbered diagonals go one way, odd-numbered go the other.
First, the slow way (so you feel the pain)
Most beginners try to manually craft the traversal direction — moving up-right until hitting a wall, then stepping down or left, then moving down-left until hitting a wall, then stepping right or down, alternating forever. The wall-handling code has four separate cases, each slightly different, and getting all four right without off-by-one errors is genuinely hard.
# Direction-simulation — 4 wall cases, fragile
r, c = 0, 0
going_up = True
result = []
for _ in range(m * n):
result.append(matrix[r][c])
if going_up:
if c == n - 1: # hit right wall — step down
r += 1; going_up = False
elif r == 0: # hit top wall — step right
c += 1; going_up = False
else:
r -= 1; c += 1
else:
if r == m - 1: # hit bottom wall — step right
c += 1; going_up = True
elif c == 0: # hit left wall — step down
r += 1; going_up = True
else:
r += 1; c -= 1
This is O(m×n) in time but O(1) space — the cost is purely cognitive: four wall cases is a lot to hold in your head and get exactly right under interview pressure.
The grouping approach reduces those four cases to zero. The direction simply comes from whether the diagonal index is even or odd.
The turning point
The anti-diagonal key d = row + col is the insight. Let's stare at a small matrix to see it:
col=0 col=1 col=2
row=0 [1] [2] [3]
row=1 [4] [5] [6]
row=2 [7] [8] [9]
d = row + col:
col=0 col=1 col=2
row=0 0 1 2
row=1 1 2 3
row=2 2 3 4
Every cell where row + col = 2 is: (0,2), (1,1), (2,0). They form exactly one anti-diagonal.
Pause & think
For a 3×4 matrix (3 rows, 4 columns), how many distinct anti-diagonals are there? What is the maximum value that row + col can reach?
For an m × n matrix, row + col ranges from 0 (top-left corner) to (m-1) + (n-1) = m+n-2 (bottom-right corner). So there are exactly m + n - 1 anti-diagonals.
The one idea to remember
The entire pattern in one sentence
Every cell (row, col) on the same anti-diagonal shares the same key row + col — group cells by this key, then read each group in alternating order (even key → bottom to top, odd key → top to bottom, or whichever convention fits the problem).
Watch it happen, frame by frame
Step 1: Group all cells by d = row + col.
d=0: [(0,0)]
d=1: [(0,1), (1,0)]
d=2: [(0,2), (1,1), (2,0)]
d=3: [(1,2), (2,1)]
d=4: [(2,2)]
Step 2: For each group, decide the direction.
Even d (0, 2, 4): traverse upward (larger row → smaller row, i.e. bottom-left to top-right).
Odd d (1, 3): traverse downward (smaller row → larger row, i.e. top-right to bottom-left).
d=0 (even, up): [(0,0)] → values [1]
d=1 (odd, down): [(0,1),(1,0)] → values [2,4]
d=2 (even, up): [(2,0),(1,1),(0,2)] → values [7,5,3]
d=3 (odd, down): [(1,2),(2,1)] → values [6,8]
d=4 (even, up): [(2,2)] → values [9]
Result: [1, 2, 4, 7, 5, 3, 6, 8, 9] ✅
Pause & think
Cover the trace below. Apply this process to:
[[1, 2],
[3, 4],
[5, 6]]
How many diagonals are there? What does each group contain?
Check your trace
m=3, n=2 → diagonals d = 0 to 3.
d=0 (even, up): [(0,0)] → [1]
d=1 (odd, down): [(0,1),(1,0)] → [2,3]
d=2 (even, up): [(2,0),(1,1)] → [5,4]
d=3 (odd, down): [(2,1)] → [6]
Result: [1, 2, 3, 5, 4, 6]
Now, the code — line by line
Approach 1: Grouping (easiest to understand)
def findDiagonalOrder_grouped(matrix):
if not matrix or not matrix[0]:
return []
m, n = len(matrix), len(matrix[0])
diagonals = [[] for _ in range(m + n - 1)] # one list per anti-diagonal
for row in range(m):
for col in range(n):
diagonals[row + col].append(matrix[row][col]) # group by key
result = []
for d, group in enumerate(diagonals):
if d % 2 == 0:
result.extend(reversed(group)) # even: bottom-left to top-right
else:
result.extend(group) # odd: top-left to bottom-right
return result
diagonals[row + col].append(...)— every cell files itself into its gutter bucket.if d % 2 == 0: reversed(group)— even-indexed diagonals go upward, so reverse the bucket (which was filled top-to-bottom).- O(m×n) time, O(m×n) extra space for the buckets.
Approach 2: Direct walk (O(1) extra space)
For each diagonal index d, compute the starting cell and step direction directly:
def findDiagonalOrder(matrix):
if not matrix or not matrix[0]:
return []
m, n = len(matrix), len(matrix[0])
result = []
for d in range(m + n - 1):
if d % 2 == 0: # going up-right
r = min(d, m - 1) # start as low as possible
c = d - r # c = d - r
while r >= 0 and c < n:
result.append(matrix[r][c])
r -= 1; c += 1
else: # going down-left
c = min(d, n - 1) # start as far right as possible
r = d - c # r = d - c
while c >= 0 and r < m:
result.append(matrix[r][c])
r += 1; c -= 1
return result
Key observations:
r = min(d, m-1)— on a diagonald, the starting row isd(ifdis still inside the matrix) orm-1(if we've run out of rows). Same logic for columns.c = d - r— once we knowr, column isd - rbecauser + c = d.- The step
(r--, c++)or(r++, c--)follows the diagonal direction naturally.
Why is it always correct?
For every cell (r, c) in the matrix, r + c is a unique identifier for its anti-diagonal. By iterating d from 0 to m+n-2, we visit every anti-diagonal exactly once. Within each diagonal, we visit every cell exactly once (walking from one end to the other). Together, every cell is visited exactly once.
Time: O(m × n) — one constant-work step per cell. Space: O(m×n) for the grouping approach; O(1) for the direct-walk approach (output excluded).
When should I reach for this? (the trigger list)
Reach for the diagonal-key trick when:
- Asked to traverse or collect a matrix diagonally (anti-diagonal or main diagonal).
- Asked to sort diagonals of a matrix independently.
- Asked to group matrix elements where the grouping criterion is "same anti-diagonal" or "same diagonal."
- You see a 2D problem where
row + col(orrow - col) stays constant along the path of interest. - The problem involves LCS/DP where diagonals in a 2D DP table represent equal-length subproblems.
Whenever you spot a constraint of the form row + col = constant or row - col = constant, the diagonal key is at work.
The same trick in three disguises
Disguise 1 — Sort the Matrix Diagonally (LC #1329)
For each main diagonal (where row - col = constant), sort the elements independently and put them back.
def diagonalSort(mat):
from collections import defaultdict
import heapq
m, n = len(mat), len(mat[0])
diags = defaultdict(list)
for r in range(m):
for c in range(n):
diags[r - c].append(mat[r][c]) # key = row - col for main diagonals
for key in diags:
diags[key].sort(reverse=True) # sort descending (we'll pop from end)
for r in range(m):
for c in range(n):
mat[r][c] = diags[r - c].pop() # place sorted values back
return mat
The key here is row - col (constant along main diagonals) instead of row + col (constant along anti-diagonals). Same concept, different direction.
Disguise 2 — Anti-diagonal groups in DP
In dynamic-programming problems on a 2D table (like Longest Common Subsequence), cells on the same anti-diagonal (i + j = constant) are independent and can be computed in parallel (useful for parallelism) or filled in diagonal order to ensure dependencies are met.
Disguise 3 — N-Queens diagonal conflict check
Two queens at (r1, c1) and (r2, c2) are on the same anti-diagonal if r1 + c1 == r2 + c2, and on the same main diagonal if r1 - c1 == r2 - c2. Using sets of these two values instantly tells you if a new queen conflicts diagonally — O(1) per check.
anti_diagonals = set() # stores r + c for each placed queen
main_diagonals = set() # stores r - c for each placed queen
def can_place(r, c):
return (r + c) not in anti_diagonals and (r - c) not in main_diagonals
Level up — Diagonal Traverse II (variable-length rows)
In LC #1424, rows have different lengths. The anti-diagonal key still works — but when computing starting cells, you must clamp to valid row/column ranges carefully.
The grouping approach (bucket per row + col) handles this elegantly: just skip out-of-bounds cells during grouping, and the buckets naturally adjust.
Traps that catch beginners
Watch out for these
- Confusing anti-diagonal (
row + col) with main diagonal (row - col). Anti-diagonals go upper-right to lower-left; main diagonals go upper-left to lower-right. Sketch a tiny 3×3 matrix and label both to check which the problem wants. - Wrong starting cell computation. For diagonal
dgoing upward:r = min(d, m-1), thenc = d - r. If you start withr = dandd ≥ m, you'll be outside the matrix. - Forgetting to alternate direction. The "diagonal traverse" problem alternates direction per diagonal. If you always go the same direction, every group is right but concatenated in the wrong order for odd diagonals.
- Using row - col for anti-diagonals (wrong key).
row - colis the main diagonal key.row + colis the anti-diagonal key. Getting these swapped produces completely wrong groupings.
| Bug | Fix |
|---|---|
| Confused anti vs main diagonal | Sketch: anti-diagonal cells have row+col constant; main diagonal cells have row-col constant |
| Starting cell out of bounds | Use r = min(d, m-1) and c = d - r, clamping to valid range |
| Not alternating direction | Even d → one direction; odd d → the other |
Say it like a pro (interview one-liner)
"The key insight is that every cell on the same anti-diagonal shares the same
row + colvalue. I group all cells by this key — giving exactlym + n - 1groups — then traverse each group in alternating direction (even key upward, odd key downward). This visits every cell exactly once in O(m×n) time."
Remember this forever
Matrix Diagonal Traversal
Anti-diagonal key: row + col (constant along each anti-diagonal).
Main diagonal key: row - col (constant along each main diagonal).
Group cells by their key. Read each group, alternating direction if needed.
Number of anti-diagonals: m + n - 1
Starting cell of diagonal d (going up): r = min(d, m-1), c = d - r
Cost: O(m×n) time · Trigger: traverse/sort/group along diagonals · diagonal conflict checks (N-Queens)
Check yourself
What is the anti-diagonal key, and why does it work?
The key is row + col. For any two cells on the same anti-diagonal, moving one step down-left changes row by +1 and col by -1 — keeping row + col unchanged. So every cell on a given anti-diagonal produces the same sum, making it a perfect group identifier.
How many anti-diagonals does an m × n matrix have?
The key row + col ranges from 0 (at the top-left corner) to (m-1) + (n-1) = m + n - 2 (at the bottom-right corner). That's m + n - 1 distinct values — one per anti-diagonal.
What is the main diagonal key, and when would you use it instead?
The main diagonal key is row - col. Along a main diagonal (upper-left to lower-right), moving one step down-right adds 1 to row and 1 to col, keeping row - col unchanged. Use this key when the problem asks about main diagonals (e.g., Sort Matrix Diagonally, N-Queens diagonal conflict with \ direction).
In the direct-walk approach, how do you find the starting cell of diagonal d going upward?
r = min(d, m-1) — the starting row is d itself if d is still within the matrix, or the last valid row m-1 if d has grown large. Then c = d - r follows directly from the key equation r + c = d. This gives the bottom-most cell of the diagonal, which is the starting point for an upward walk.
Practice problems
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Diagonal Traverse | Medium | Alternate direction; row+col grouping | LC #498 |
| Sort the Matrix Diagonally | Medium | Main diagonal: row-col key; sort each independently | LC #1329 |
| Diagonal Traverse II | Medium | Variable-length rows; same grouping, careful bounds | LC #1424 |
| Valid Sudoku | Medium | Main + anti diagonals used for N-Queens style conflict checking | LC #36 |
When you can explain the row + col key in one sentence and code the grouped approach from memory, you've learned this pattern.
Next up: Matrix Search — Sorted Matrix — where we exploit a sorted 2D grid using the staircase search trick you first met in Two Pointers.