1536. Minimum Swaps to Arrange a Binary Grid
Problem Description
You are given an n x n
binary grid (containing only 0s and 1s). In one step, you can choose two adjacent rows and swap them.
A grid is considered valid when all cells above the main diagonal contain zeros. The main diagonal runs from the top-left corner (1, 1)
to the bottom-right corner (n, n)
.
Your task is to find the minimum number of steps (row swaps) needed to make the grid valid. If it's impossible to make the grid valid through row swaps, return -1
.
For example, in a 3x3 grid, the cells above the main diagonal are positions (0,1)
, (0,2)
, and (1,2)
. All these positions must contain zeros for the grid to be valid.
The key insight is that for row i
to be valid in the final configuration, all cells from position (i, i+1)
to (i, n-1)
must be zeros. This means each row needs to have its last 1
appearing at or before column i
to be placed as the i
-th row in a valid grid.
The solution works by:
- First calculating for each row the position of its rightmost
1
- For each target row position
i
, finding the nearest row below that has its last1
at positionβ€ i
- Bubbling that row up to position
i
through adjacent swaps - Counting the total number of swaps needed
If at any point no suitable row can be found for a position, the grid cannot be made valid and -1
is returned.
Intuition
To understand this problem, let's first think about what makes a grid valid. For a grid to be valid, all cells above the main diagonal must be zeros. This means:
- Row 0 needs zeros from column 1 to n-1 (requires n-1 trailing zeros)
- Row 1 needs zeros from column 2 to n-1 (requires n-2 trailing zeros)
- Row 2 needs zeros from column 3 to n-1 (requires n-3 trailing zeros)
- And so on...
The key observation is that we can only swap adjacent rows, not modify the content of any row. So each row has a fixed number of trailing zeros that won't change no matter how we rearrange the rows.
This leads us to think: what if we track the rightmost position of 1
in each row? If a row has its last 1
at position j
, then it has at least n-1-j
trailing zeros. For this row to be placed at position i
in the final valid grid, we need j β€ i
.
Now the problem becomes a matching problem: we need to assign rows to positions such that each row i
gets a row whose last 1
is at position β€ i
.
Since we can only swap adjacent rows, we should use a greedy approach: for each position i
starting from the top, find the closest row below that satisfies the condition (last 1
at position β€ i
). Why the closest? Because moving a row from position j
to position i
requires exactly j - i
swaps, so choosing the closest minimizes the number of swaps for this step.
If at any position i
we can't find a suitable row in positions [i, n)
, then it's impossible to make the grid valid, and we return -1
.
This greedy strategy works because once we place a suitable row at position i
, we never need to move it again - it will remain valid for all subsequent operations.
Learn more about Greedy patterns.
Solution Approach
The implementation follows a greedy approach where we process each row position from top to bottom.
Step 1: Calculate the rightmost 1
position for each row
First, we create an array pos
where pos[i]
stores the column index of the rightmost 1
in row i
. If a row contains no 1
s, we store -1
.
pos = [-1] * n
for i in range(n):
for j in range(n - 1, -1, -1):
if grid[i][j] == 1:
pos[i] = j
break
We iterate from right to left in each row to find the last occurrence of 1
.
Step 2: Greedily assign rows to positions
For each target position i
(from 0
to n-1
), we need to find a row that can be placed there. A row can be placed at position i
if its rightmost 1
is at column β€ i
.
for i in range(n):
k = -1
for j in range(i, n):
if pos[j] <= i:
ans += j - i
k = j
break
if k == -1:
return -1
We search from position i
downwards to find the first row k
that satisfies pos[k] <= i
. The number of swaps needed to move row k
to position i
is exactly k - i
.
Step 3: Perform the swaps
Once we find a suitable row at position k
, we bubble it up to position i
through adjacent swaps:
while k > i: pos[k], pos[k - 1] = pos[k - 1], pos[k] k -= 1
This simulates swapping row k
with row k-1
, then row k-1
with row k-2
, and so on, until the row reaches position i
. We only need to update the pos
array since we don't need the actual grid configuration.
Time Complexity: O(nΒ²)
- For each of the n
positions, we may need to search through O(n)
rows and perform O(n)
swaps.
Space Complexity: O(n)
- We only need the pos
array to track the rightmost 1
positions.
The algorithm returns the total number of swaps ans
if successful, or -1
if it's impossible to make the grid valid.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate the solution approach.
Consider this 3Γ3 grid:
grid = [[0,1,1], [0,0,1], [0,0,0]]
Step 1: Calculate rightmost 1 position for each row
We examine each row from right to left to find the last occurrence of 1
:
- Row 0:
[0,1,1]
β rightmost1
at column 2 - Row 1:
[0,0,1]
β rightmost1
at column 2 - Row 2:
[0,0,0]
β no1
s, so position is -1
Therefore, pos = [2, 2, -1]
Step 2: Process each target position
For a valid grid, we need:
- Row 0: all cells from (0,1) to (0,2) must be 0 β rightmost
1
must be at column β€ 0 - Row 1: all cells from (1,2) to (1,2) must be 0 β rightmost
1
must be at column β€ 1 - Row 2: no restrictions β rightmost
1
can be anywhere
Position i = 0:
- Need a row with rightmost
1
at column β€ 0 - Check rows starting from position 0:
- Row 0:
pos[0] = 2
(2 > 0, doesn't work) - Row 1:
pos[1] = 2
(2 > 0, doesn't work) - Row 2:
pos[2] = -1
(-1 β€ 0, works!)
- Row 0:
- Found row 2 at position k = 2
- Swaps needed: 2 - 0 = 2
- Bubble row 2 up to position 0:
- Swap rows 2 and 1:
pos = [2, -1, 2]
- Swap rows 1 and 0:
pos = [-1, 2, 2]
- Swap rows 2 and 1:
- Total swaps so far: 2
Position i = 1:
- Need a row with rightmost
1
at column β€ 1 - Check rows starting from position 1:
- Row 1 (current):
pos[1] = 2
(2 > 1, doesn't work) - Row 2:
pos[2] = 2
(2 > 1, doesn't work)
- Row 1 (current):
- No suitable row found!
- Return -1
The grid cannot be made valid because both remaining rows have their rightmost 1
at column 2, which is too far right to be placed at position 1 (where we need the rightmost 1
to be at column β€ 1).
Let's try a different example where the transformation is possible:
grid = [[0,0,1], [1,1,0], [1,0,0]]
Step 1: Calculate positions
- Row 0: rightmost
1
at column 2 βpos[0] = 2
- Row 1: rightmost
1
at column 1 βpos[1] = 1
- Row 2: rightmost
1
at column 0 βpos[2] = 0
So pos = [2, 1, 0]
Position i = 0:
- Need rightmost
1
at column β€ 0 - Check: Row 0 (pos=2, no), Row 1 (pos=1, no), Row 2 (pos=0, yes!)
- Move row 2 to position 0: 2 swaps
- After bubbling:
pos = [0, 2, 1]
- Total swaps: 2
Position i = 1:
- Need rightmost
1
at column β€ 1 - Check: Row 1 (current, pos=2, no), Row 2 (pos=1, yes!)
- Move row 2 to position 1: 1 swap
- After bubbling:
pos = [0, 1, 2]
- Total swaps: 2 + 1 = 3
Position i = 2:
- Need rightmost
1
at column β€ 2 - Row 2 (current, pos=2, yes!)
- No swaps needed
- Total swaps: 3
The algorithm successfully returns 3 as the minimum number of swaps needed.
Solution Implementation
1class Solution:
2 def minSwaps(self, grid: List[List[int]]) -> int:
3 """
4 Find minimum number of row swaps to make the grid upper triangular.
5 An upper triangular matrix has all elements below the main diagonal as 0.
6
7 Args:
8 grid: n x n binary matrix
9
10 Returns:
11 Minimum number of swaps needed, or -1 if impossible
12 """
13 n = len(grid)
14
15 # Find the rightmost 1 position in each row
16 # This tells us how many trailing zeros each row has
17 rightmost_one_positions = [-1] * n
18
19 for row_idx in range(n):
20 for col_idx in range(n - 1, -1, -1):
21 if grid[row_idx][col_idx] == 1:
22 rightmost_one_positions[row_idx] = col_idx
23 break
24
25 total_swaps = 0
26
27 # For each row position, find a suitable row that can be placed there
28 for target_row in range(n):
29 # Find the first row from current position onwards that can fit
30 # A row can fit at position i if its rightmost 1 is at position <= i
31 suitable_row_idx = -1
32
33 for candidate_row in range(target_row, n):
34 if rightmost_one_positions[candidate_row] <= target_row:
35 # Found a suitable row
36 total_swaps += candidate_row - target_row
37 suitable_row_idx = candidate_row
38 break
39
40 # If no suitable row found, it's impossible to form upper triangular matrix
41 if suitable_row_idx == -1:
42 return -1
43
44 # Bubble the suitable row up to the target position
45 # This simulates the actual row swaps
46 while suitable_row_idx > target_row:
47 rightmost_one_positions[suitable_row_idx], rightmost_one_positions[suitable_row_idx - 1] = \
48 rightmost_one_positions[suitable_row_idx - 1], rightmost_one_positions[suitable_row_idx]
49 suitable_row_idx -= 1
50
51 return total_swaps
52
1class Solution {
2 public int minSwaps(int[][] grid) {
3 int n = grid.length;
4
5 // Store the rightmost position of 1 in each row
6 // If a row has all zeros, it will remain -1
7 int[] rightmostOnePosition = new int[n];
8 Arrays.fill(rightmostOnePosition, -1);
9
10 // Find the rightmost 1 in each row
11 for (int row = 0; row < n; ++row) {
12 for (int col = n - 1; col >= 0; --col) {
13 if (grid[row][col] == 1) {
14 rightmostOnePosition[row] = col;
15 break;
16 }
17 }
18 }
19
20 int totalSwaps = 0;
21
22 // For each row position, find a suitable row that can be placed there
23 for (int targetRow = 0; targetRow < n; ++targetRow) {
24 int foundRowIndex = -1;
25
26 // Search for a row that can be placed at targetRow position
27 // A row can be placed at position i if its rightmost 1 is at position <= i
28 for (int candidateRow = targetRow; candidateRow < n; ++candidateRow) {
29 if (rightmostOnePosition[candidateRow] <= targetRow) {
30 // Found a suitable row, count the swaps needed
31 totalSwaps += candidateRow - targetRow;
32 foundRowIndex = candidateRow;
33 break;
34 }
35 }
36
37 // If no suitable row found, it's impossible to form upper triangular matrix
38 if (foundRowIndex == -1) {
39 return -1;
40 }
41
42 // Bubble the found row up to its target position
43 // by swapping it with each row above it
44 for (int currentPos = foundRowIndex; currentPos > targetRow; --currentPos) {
45 int temp = rightmostOnePosition[currentPos];
46 rightmostOnePosition[currentPos] = rightmostOnePosition[currentPos - 1];
47 rightmostOnePosition[currentPos - 1] = temp;
48 }
49 }
50
51 return totalSwaps;
52 }
53}
54
1class Solution {
2public:
3 int minSwaps(vector<vector<int>>& grid) {
4 int n = grid.size();
5
6 // Store the rightmost position of 1 in each row
7 // If a row has all 0s, the position will be -1
8 vector<int> rightmostOnePosition(n, -1);
9
10 // Find the rightmost 1 in each row
11 for (int row = 0; row < n; ++row) {
12 for (int col = n - 1; col >= 0; --col) {
13 if (grid[row][col] == 1) {
14 rightmostOnePosition[row] = col;
15 break;
16 }
17 }
18 }
19
20 int totalSwaps = 0;
21
22 // For each row position, find a suitable row that can be placed here
23 for (int targetRow = 0; targetRow < n; ++targetRow) {
24 int foundRow = -1;
25
26 // Search for the first row that can be placed at targetRow position
27 // A row can be placed at position i if its rightmost 1 is at position <= i
28 for (int candidateRow = targetRow; candidateRow < n; ++candidateRow) {
29 if (rightmostOnePosition[candidateRow] <= targetRow) {
30 totalSwaps += candidateRow - targetRow;
31 foundRow = candidateRow;
32 break;
33 }
34 }
35
36 // If no suitable row is found, it's impossible to form valid matrix
37 if (foundRow == -1) {
38 return -1;
39 }
40
41 // Bubble the found row up to its target position
42 // by swapping it with previous rows
43 for (int currentPos = foundRow; currentPos > targetRow; --currentPos) {
44 swap(rightmostOnePosition[currentPos], rightmostOnePosition[currentPos - 1]);
45 }
46 }
47
48 return totalSwaps;
49 }
50};
51
1/**
2 * Calculates minimum number of swaps needed to make grid valid
3 * A valid grid has all 1s in upper triangle (above main diagonal)
4 * @param grid - n x n binary matrix
5 * @returns minimum swaps needed, or -1 if impossible
6 */
7function minSwaps(grid: number[][]): number {
8 const n: number = grid.length;
9
10 // Store the rightmost position of 1 in each row
11 // If no 1 exists in a row, position remains -1
12 const rightmostOnePosition: number[] = Array(n).fill(-1);
13
14 // Find the rightmost 1 in each row
15 for (let row = 0; row < n; ++row) {
16 for (let col = n - 1; col >= 0; --col) {
17 if (grid[row][col] === 1) {
18 rightmostOnePosition[row] = col;
19 break;
20 }
21 }
22 }
23
24 let totalSwaps: number = 0;
25
26 // For each row position, find a suitable row that can be placed there
27 for (let targetRow = 0; targetRow < n; ++targetRow) {
28 let foundRowIndex: number = -1;
29
30 // Search for a row that can be placed at targetRow position
31 // A row can be placed at position i if its rightmost 1 is at position <= i
32 for (let currentRow = targetRow; currentRow < n; ++currentRow) {
33 if (rightmostOnePosition[currentRow] <= targetRow) {
34 // Found a suitable row, calculate swaps needed
35 totalSwaps += currentRow - targetRow;
36 foundRowIndex = currentRow;
37 break;
38 }
39 }
40
41 // If no suitable row found, grid cannot be made valid
42 if (foundRowIndex === -1) {
43 return -1;
44 }
45
46 // Bubble the found row up to its target position by swapping
47 for (let swapIndex = foundRowIndex; swapIndex > targetRow; --swapIndex) {
48 // Swap adjacent elements
49 [rightmostOnePosition[swapIndex], rightmostOnePosition[swapIndex - 1]] =
50 [rightmostOnePosition[swapIndex - 1], rightmostOnePosition[swapIndex]];
51 }
52 }
53
54 return totalSwaps;
55}
56
Time and Space Complexity
Time Complexity: O(nΒ²)
The algorithm consists of three main parts:
- First loop: Iterates through each row (
n
iterations) and for each row, iterates through columns to find the rightmost1
(up ton
iterations per row). This givesO(nΒ²)
. - Second nested loop structure:
- Outer loop runs
n
times - Inner loop searches for a valid row starting from position
i
, which in worst case examinesO(n)
rows - The while loop performs bubble-up operations, moving element from position
k
to positioni
, which takesO(n)
swaps in worst case - Overall for this part:
O(n) Γ O(n) = O(nΒ²)
- Outer loop runs
Since both parts are O(nΒ²)
and execute sequentially, the total time complexity is O(nΒ²)
.
Space Complexity: O(n)
The algorithm uses a single auxiliary array pos
of size n
to store the position of the rightmost 1
in each row. All other variables (i
, j
, k
, ans
) use constant space. Therefore, the space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Confusing Upper Triangular with Lower Triangular Matrix
A frequent mistake is misunderstanding what "valid grid" means. The problem states cells above the main diagonal should be zeros, which means we want an upper triangular matrix. However, many developers incorrectly interpret this as needing zeros below the diagonal.
Wrong interpretation:
# Incorrectly checking for lower triangular
for i in range(n):
for j in range(i): # Wrong: checking positions below diagonal
if grid[i][j] == 1:
# This is checking the wrong cells
Correct approach:
# For row i to be valid at position i, all cells from (i, i+1) to (i, n-1) must be 0 # This means the rightmost 1 in row i must be at position <= i
Pitfall 2: Not Understanding the Greedy Strategy's Correctness
Some developers try to optimize by finding the "best" row for each position rather than the first suitable one, thinking this might reduce total swaps. This overcomplication is unnecessary and can lead to incorrect solutions.
Incorrect approach:
# Trying to find the "optimal" row with minimum swaps
best_row = -1
min_swaps = float('inf')
for j in range(i, n):
if pos[j] <= i and j - i < min_swaps:
min_swaps = j - i
best_row = j
Why the greedy approach works: The first suitable row we find is always optimal because:
- We must fill position
i
with some valid row - Any valid row currently below position
i
will require swaps to reach positioni
- Using the nearest valid row minimizes swaps for this position
- Delaying this choice doesn't improve future positions
Pitfall 3: Forgetting to Update the Position Array After Swaps
A critical bug occurs when developers count the swaps but forget to actually simulate them by updating the rightmost_one_positions
array.
Bug-prone code:
for i in range(n):
for j in range(i, n):
if rightmost_one_positions[j] <= i:
total_swaps += j - i
# BUG: Not updating the array after "swapping"
break
# The array is now inconsistent with the simulated swaps
Correct implementation:
# Must bubble the row up and update positions while suitable_row_idx > target_row: rightmost_one_positions[suitable_row_idx], rightmost_one_positions[suitable_row_idx - 1] = \ rightmost_one_positions[suitable_row_idx - 1], rightmost_one_positions[suitable_row_idx] suitable_row_idx -= 1
Pitfall 4: Off-by-One Errors in Rightmost One Position
Developers sometimes make errors when calculating what position the rightmost 1
needs to be at for each row.
Common mistake:
# Wrong: thinking row i needs rightmost 1 at position < i if rightmost_one_positions[candidate_row] < target_row: # Off by one!
Correct logic:
- Row 0 can have its rightmost
1
at position 0 (needs n-1 trailing zeros) - Row 1 can have its rightmost
1
at position 1 (needs n-2 trailing zeros) - Row i can have its rightmost
1
at position i (needs n-i-1 trailing zeros)
The condition should be <=
not <
.
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!