2392. Build a Matrix With Conditions
Problem Description
You need to build a k x k
matrix that places numbers from 1
to k
exactly once, with all other cells containing 0
.
The placement must follow ordering constraints:
- Row constraints (
rowConditions
): Each constraint[above_i, below_i]
means numberabove_i
must appear in a row strictly above the row containing numberbelow_i
- Column constraints (
colConditions
): Each constraint[left_i, right_i]
means numberleft_i
must appear in a column strictly to the left of the column containing numberright_i
The solution uses topological sorting to determine valid orderings:
-
Function
f(cond)
performs topological sort on the constraints:- Builds a directed graph where edge
a β b
meansa
must come beforeb
- Tracks in-degrees (number of incoming edges) for each node
- Uses BFS starting from nodes with no dependencies (in-degree = 0)
- Returns the sorted order if valid, or
None
if a cycle exists
- Builds a directed graph where edge
-
Applies topological sort to both row and column constraints separately:
row
gives the top-to-bottom ordering of numberscol
gives the left-to-right ordering of numbers
-
Constructs the final matrix:
- Creates a mapping
m
from each number to its column position - Places each number at position
[row_index][column_index]
- Number
v
at positioni
in row ordering goes to rowi
, columnm[v]
- Creates a mapping
Returns an empty list if either ordering is invalid (contains a cycle), otherwise returns the constructed matrix.
Intuition
The key insight is that we can decompose this 2D placement problem into two independent 1D ordering problems.
Think about it this way: if we need to place numbers 1
to k
in a matrix with certain "above/below" and "left/right" relationships, we essentially need to:
- Determine which row each number should go in (respecting the row constraints)
- Determine which column each number should go in (respecting the column constraints)
These two orderings are independent - the row position of a number doesn't affect its column position and vice versa. This means we can solve them separately.
For each set of constraints, we're essentially asking: "Given that some numbers must come before others, what's a valid ordering?" This is the classic topological sorting problem. If A
must be above B
, then A
comes earlier in the row ordering. If C
must be left of D
, then C
comes earlier in the column ordering.
The beauty of this approach is that topological sort also naturally detects impossibilities - if there's a cycle (like A
must be above B
, B
must be above C
, and C
must be above A
), the topological sort will fail to include all numbers, telling us no valid arrangement exists.
Once we have both orderings:
- The row ordering tells us: "number at position 0 goes in row 0, number at position 1 goes in row 1, etc."
- The column ordering tells us: "number at position 0 goes in column 0, number at position 1 goes in column 1, etc."
By combining these two pieces of information, we know exactly where each number should be placed in the matrix. The remaining cells are simply filled with 0
.
Learn more about Graph and Topological Sort patterns.
Solution Approach
The implementation uses Kahn's algorithm for topological sorting to find valid orderings for both rows and columns.
Step 1: Topological Sort Function f(cond)
For each set of constraints, we build a directed graph and perform topological sort:
-
Build the graph:
- Create adjacency list
g
whereg[a]
contains all nodes that must come aftera
- Track in-degrees in array
indeg
whereindeg[b]
counts how many nodes must come beforeb
- For each constraint
[a, b]
, add edgea β b
and incrementindeg[b]
- Create adjacency list
-
Initialize BFS queue:
- Find all nodes with
indeg = 0
(no prerequisites) - These nodes can be placed first in the ordering
- Find all nodes with
-
Process nodes level by level:
- Remove a node from queue and add to result
- For each neighbor, decrement its in-degree
- If neighbor's in-degree becomes 0, add it to queue
- This ensures we only process nodes after all their prerequisites
-
Validate the result:
- If
len(res) != k
, there's a cycle (impossible constraints) - Return
None
for invalid case, otherwise return the ordering
- If
Step 2: Get Row and Column Orderings
row = f(rowConditions) # Get top-to-bottom ordering col = f(colConditions) # Get left-to-right ordering
If either returns None
, no valid matrix exists, so return empty list.
Step 3: Build the Matrix
-
Create a
k Γ k
matrix initialized with zeros:ans = [[0] * k for _ in range(k)]
-
Create position mapping for columns:
m = [0] * (k + 1) # m[number] = column_index for i, v in enumerate(col): m[v] = i
This maps each number to its column position based on the column ordering.
-
Place numbers in the matrix:
for i, v in enumerate(row): ans[i][m[v]] = v
i
is the row index (position in row ordering)v
is the number to placem[v]
is the column index wherev
should go- Place number
v
at position[i][m[v]]
Time Complexity: O(k + n + m)
where n
and m
are the number of row and column conditions respectively. The topological sort runs in linear time relative to nodes and edges.
Space Complexity: O(kΒ²)
for the output matrix, plus O(k)
for the graph structures and orderings.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with k = 3
, rowConditions = [[1,2],[3,2]]
, and colConditions = [[2,1],[3,1]]
.
Step 1: Process Row Conditions
Row conditions [[1,2],[3,2]]
mean:
- Number 1 must be in a row above number 2
- Number 3 must be in a row above number 2
Building the graph:
- Edges:
1β2
,3β2
- In-degrees:
[0, 0, 2, 0]
(index 0 unused, numbers 1 and 3 have no incoming edges, number 2 has 2 incoming edges)
Topological sort:
- Start with nodes having in-degree 0: queue =
[1, 3]
- Process node 1: Add to result, decrement in-degree of 2 (now 1)
- Process node 3: Add to result, decrement in-degree of 2 (now 0)
- Node 2 now has in-degree 0, add to queue
- Process node 2: Add to result
Row ordering: [1, 3, 2]
(top to bottom)
Step 2: Process Column Conditions
Column conditions [[2,1],[3,1]]
mean:
- Number 2 must be in a column left of number 1
- Number 3 must be in a column left of number 1
Building the graph:
- Edges:
2β1
,3β1
- In-degrees:
[0, 2, 0, 0]
(number 1 has 2 incoming edges)
Topological sort:
- Start with nodes having in-degree 0: queue =
[2, 3]
- Process node 2: Add to result, decrement in-degree of 1 (now 1)
- Process node 3: Add to result, decrement in-degree of 1 (now 0)
- Node 1 now has in-degree 0, add to queue
- Process node 1: Add to result
Column ordering: [2, 3, 1]
(left to right)
Step 3: Build the Matrix
Create column position mapping from col = [2, 3, 1]
:
m[2] = 0
(number 2 goes in column 0)m[3] = 1
(number 3 goes in column 1)m[1] = 2
(number 1 goes in column 2)
Place numbers using row = [1, 3, 2]
:
- Row 0: Place number 1 at column
m[1] = 2
βans[0][2] = 1
- Row 1: Place number 3 at column
m[3] = 1
βans[1][1] = 3
- Row 2: Place number 2 at column
m[2] = 0
βans[2][0] = 2
Final matrix:
[[0, 0, 1], [0, 3, 0], [2, 0, 0]]
Verification:
- Row constraints satisfied: 1 is in row 0, 3 is in row 1, both above 2 in row 2 β
- Column constraints satisfied: 2 is in column 0, 3 is in column 1, both left of 1 in column 2 β
Solution Implementation
1class Solution:
2 def buildMatrix(
3 self, k: int, rowConditions: List[List[int]], colConditions: List[List[int]]
4 ) -> List[List[int]]:
5 def topological_sort(conditions):
6 """
7 Perform topological sort on the given conditions.
8 Returns sorted order if valid, None if cycle detected.
9 """
10 # Build adjacency list and track in-degrees
11 adjacency_list = defaultdict(list)
12 in_degree = [0] * (k + 1) # Index 0 unused, 1 to k for actual values
13
14 # Process each condition: value_a should come before value_b
15 for value_a, value_b in conditions:
16 adjacency_list[value_a].append(value_b)
17 in_degree[value_b] += 1
18
19 # Initialize queue with all nodes having zero in-degree
20 # Skip index 0, enumerate from 1 to k
21 queue = deque([i for i, degree in enumerate(in_degree[1:], 1) if degree == 0])
22 sorted_order = []
23
24 # Process nodes in topological order
25 while queue:
26 # Process all nodes at current level
27 for _ in range(len(queue)):
28 current_node = queue.popleft()
29 sorted_order.append(current_node)
30
31 # Reduce in-degree for all neighbors
32 for neighbor in adjacency_list[current_node]:
33 in_degree[neighbor] -= 1
34 if in_degree[neighbor] == 0:
35 queue.append(neighbor)
36
37 # Check if all k nodes were processed (no cycle)
38 return None if len(sorted_order) != k else sorted_order
39
40 # Get row and column orderings via topological sort
41 row_order = topological_sort(rowConditions)
42 col_order = topological_sort(colConditions)
43
44 # Return empty matrix if either ordering is invalid (contains cycle)
45 if row_order is None or col_order is None:
46 return []
47
48 # Initialize k x k matrix with zeros
49 result_matrix = [[0] * k for _ in range(k)]
50
51 # Create mapping from value to column index
52 value_to_col_index = [0] * (k + 1) # Index 0 unused
53 for col_idx, value in enumerate(col_order):
54 value_to_col_index[value] = col_idx
55
56 # Place each value in the matrix according to row and column orderings
57 for row_idx, value in enumerate(row_order):
58 col_idx = value_to_col_index[value]
59 result_matrix[row_idx][col_idx] = value
60
61 return result_matrix
62
1class Solution {
2 private int k;
3
4 public int[][] buildMatrix(int k, int[][] rowConditions, int[][] colConditions) {
5 this.k = k;
6
7 // Get topological order for rows and columns
8 List<Integer> rowOrder = topologicalSort(rowConditions);
9 List<Integer> colOrder = topologicalSort(colConditions);
10
11 // If either ordering is invalid (has cycle), return empty matrix
12 if (rowOrder == null || colOrder == null) {
13 return new int[0][0];
14 }
15
16 // Initialize result matrix
17 int[][] resultMatrix = new int[k][k];
18
19 // Create mapping from value to column position
20 int[] valueToColIndex = new int[k + 1];
21 for (int i = 0; i < k; i++) {
22 valueToColIndex[colOrder.get(i)] = i;
23 }
24
25 // Place each value in the matrix based on row and column orders
26 for (int rowIndex = 0; rowIndex < k; rowIndex++) {
27 int value = rowOrder.get(rowIndex);
28 int colIndex = valueToColIndex[value];
29 resultMatrix[rowIndex][colIndex] = value;
30 }
31
32 return resultMatrix;
33 }
34
35 private List<Integer> topologicalSort(int[][] conditions) {
36 // Initialize adjacency list for directed graph
37 List<Integer>[] adjacencyList = new List[k + 1];
38 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
39
40 // Track in-degree for each node (1 to k)
41 int[] inDegree = new int[k + 1];
42
43 // Build graph from conditions
44 for (int[] condition : conditions) {
45 int from = condition[0];
46 int to = condition[1];
47 adjacencyList[from].add(to);
48 inDegree[to]++;
49 }
50
51 // Initialize queue with nodes having zero in-degree
52 Deque<Integer> queue = new ArrayDeque<>();
53 for (int node = 1; node <= k; node++) {
54 if (inDegree[node] == 0) {
55 queue.offer(node);
56 }
57 }
58
59 // Perform topological sort using Kahn's algorithm
60 List<Integer> sortedOrder = new ArrayList<>();
61 while (!queue.isEmpty()) {
62 // Process all nodes at current level
63 int levelSize = queue.size();
64 for (int i = 0; i < levelSize; i++) {
65 int currentNode = queue.pollFirst();
66 sortedOrder.add(currentNode);
67
68 // Decrease in-degree of neighbors and add to queue if becomes zero
69 for (int neighbor : adjacencyList[currentNode]) {
70 inDegree[neighbor]--;
71 if (inDegree[neighbor] == 0) {
72 queue.offer(neighbor);
73 }
74 }
75 }
76 }
77
78 // Return sorted order if all nodes are included, otherwise null (cycle exists)
79 return sortedOrder.size() == k ? sortedOrder : null;
80 }
81}
82
1class Solution {
2public:
3 int k;
4
5 vector<vector<int>> buildMatrix(int k, vector<vector<int>>& rowConditions, vector<vector<int>>& colConditions) {
6 this->k = k;
7
8 // Get topological order for row and column positions
9 vector<int> rowOrder = topologicalSort(rowConditions);
10 vector<int> colOrder = topologicalSort(colConditions);
11
12 // If either ordering is invalid (cycle detected), return empty matrix
13 if (rowOrder.empty() || colOrder.empty()) {
14 return {};
15 }
16
17 // Initialize result matrix with all zeros
18 vector<vector<int>> resultMatrix(k, vector<int>(k, 0));
19
20 // Create a mapping from value to its column position
21 vector<int> valueToColIndex(k + 1);
22 for (int i = 0; i < k; ++i) {
23 valueToColIndex[colOrder[i]] = i;
24 }
25
26 // Place each value in the matrix based on row and column orders
27 for (int rowIdx = 0; rowIdx < k; ++rowIdx) {
28 int value = rowOrder[rowIdx];
29 int colIdx = valueToColIndex[value];
30 resultMatrix[rowIdx][colIdx] = value;
31 }
32
33 return resultMatrix;
34 }
35
36private:
37 vector<int> topologicalSort(vector<vector<int>>& conditions) {
38 // Build adjacency list and calculate in-degrees
39 vector<vector<int>> adjacencyList(k + 1);
40 vector<int> inDegree(k + 1, 0);
41
42 // Process each condition to build the directed graph
43 for (const auto& condition : conditions) {
44 int from = condition[0];
45 int to = condition[1];
46 adjacencyList[from].push_back(to);
47 ++inDegree[to];
48 }
49
50 // Initialize queue with nodes having zero in-degree
51 queue<int> zeroInDegreeQueue;
52 for (int node = 1; node <= k; ++node) {
53 if (inDegree[node] == 0) {
54 zeroInDegreeQueue.push(node);
55 }
56 }
57
58 // Perform topological sort using Kahn's algorithm
59 vector<int> topologicalOrder;
60 while (!zeroInDegreeQueue.empty()) {
61 int currentNode = zeroInDegreeQueue.front();
62 zeroInDegreeQueue.pop();
63 topologicalOrder.push_back(currentNode);
64
65 // Process all neighbors of current node
66 for (int neighbor : adjacencyList[currentNode]) {
67 --inDegree[neighbor];
68 if (inDegree[neighbor] == 0) {
69 zeroInDegreeQueue.push(neighbor);
70 }
71 }
72 }
73
74 // Return the result only if all nodes are processed (no cycle)
75 return topologicalOrder.size() == k ? topologicalOrder : vector<int>();
76 }
77};
78
1/**
2 * Builds a k x k matrix based on row and column ordering conditions
3 * @param k - The size of the matrix and the range of values (1 to k)
4 * @param rowConditions - Array of [above, below] pairs indicating row ordering constraints
5 * @param colConditions - Array of [left, right] pairs indicating column ordering constraints
6 * @returns The constructed matrix, or empty array if no valid arrangement exists
7 */
8function buildMatrix(k: number, rowConditions: number[][], colConditions: number[][]): number[][] {
9 /**
10 * Performs topological sort on the given conditions to find a valid ordering
11 * @param conditions - Array of [before, after] pairs representing ordering constraints
12 * @returns Array containing the topologically sorted sequence, or empty if cycle exists
13 */
14 function topologicalSort(conditions: number[][]): number[] {
15 // Build adjacency list for the directed graph
16 const adjacencyList: number[][] = Array.from({ length: k + 1 }, () => []);
17 // Track in-degree for each node (1 to k)
18 const inDegree: number[] = new Array(k + 1).fill(0);
19
20 // Construct the graph from conditions
21 for (const [beforeNode, afterNode] of conditions) {
22 adjacencyList[beforeNode].push(afterNode);
23 inDegree[afterNode]++;
24 }
25
26 // Initialize queue with nodes having zero in-degree
27 const queue: number[] = [];
28 for (let node = 1; node <= k; node++) {
29 if (inDegree[node] === 0) {
30 queue.push(node);
31 }
32 }
33
34 // Perform BFS-based topological sort
35 const sortedResult: number[] = [];
36 while (queue.length > 0) {
37 // Process all nodes at current level
38 const levelSize = queue.length;
39 for (let i = 0; i < levelSize; i++) {
40 const currentNode = queue.shift()!;
41 sortedResult.push(currentNode);
42
43 // Reduce in-degree for neighboring nodes
44 for (const neighbor of adjacencyList[currentNode]) {
45 inDegree[neighbor]--;
46 if (inDegree[neighbor] === 0) {
47 queue.push(neighbor);
48 }
49 }
50 }
51 }
52
53 // Return sorted result only if all k nodes are included (no cycle)
54 return sortedResult.length === k ? sortedResult : [];
55 }
56
57 // Get topologically sorted sequences for rows and columns
58 const rowOrder: number[] = topologicalSort(rowConditions);
59 const columnOrder: number[] = topologicalSort(colConditions);
60
61 // If either ordering is invalid (has cycle), return empty matrix
62 if (rowOrder.length === 0 || columnOrder.length === 0) {
63 return [];
64 }
65
66 // Initialize k x k matrix with zeros
67 const resultMatrix: number[][] = Array.from({ length: k }, () => new Array(k).fill(0));
68
69 // Create mapping from value to column index
70 const valueToColumnIndex: number[] = new Array(k + 1).fill(0);
71 for (let colIdx = 0; colIdx < k; colIdx++) {
72 valueToColumnIndex[columnOrder[colIdx]] = colIdx;
73 }
74
75 // Place each value in the matrix according to row and column orderings
76 for (let rowIdx = 0; rowIdx < k; rowIdx++) {
77 const value = rowOrder[rowIdx];
78 const colIdx = valueToColumnIndex[value];
79 resultMatrix[rowIdx][colIdx] = value;
80 }
81
82 return resultMatrix;
83}
84
Time and Space Complexity
Time Complexity: O(k^2 + R + C)
where k
is the matrix dimension, R
is the length of rowConditions, and C
is the length of colConditions.
The analysis breaks down as follows:
- The topological sort function
f()
is called twice (once for rows, once for columns) - For each call to
f()
:- Building the graph takes
O(len(conditions))
time to iterate through all conditions - Initializing indegree array takes
O(k)
time - The BFS/topological sort processes each node once and each edge once, taking
O(k + len(conditions))
time
- Building the graph takes
- After getting both orderings:
- Creating the mapping array
m
takesO(k)
time - Filling the answer matrix takes
O(k)
time to placek
elements - Initializing the answer matrix takes
O(k^2)
time
- Creating the mapping array
Total: O(k + R) + O(k + C) + O(k) + O(k) + O(k^2)
= O(k^2 + R + C)
Space Complexity: O(k^2 + R + C)
The space usage includes:
- Graph representation
g
stores all edges:O(R)
for row graph andO(C)
for column graph - Indegree array:
O(k)
for each call tof()
- Queue for BFS:
O(k)
in worst case - Result arrays
row
andcol
:O(k)
each - Mapping array
m
:O(k)
- Answer matrix:
O(k^2)
Total: O(k^2) + O(R) + O(C) + O(k)
= O(k^2 + R + C)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Cycle Detection in Topological Sort
Pitfall: A common mistake is assuming that if the queue becomes empty, we've successfully completed the topological sort. However, the queue can become empty even when there's a cycle in the graph - this happens when all remaining unprocessed nodes are part of a cycle (all have non-zero in-degrees).
Wrong Implementation:
def topological_sort(conditions):
# ... build graph and in-degrees ...
queue = deque([i for i in range(1, k+1) if in_degree[i] == 0])
sorted_order = []
while queue:
current = queue.popleft()
sorted_order.append(current)
for neighbor in adjacency_list[current]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
# WRONG: Just returning the result without checking completeness
return sorted_order # This might return partial ordering!
Solution: Always verify that the topological sort processed exactly k
nodes. If fewer nodes were processed, it means there's a cycle:
# Correct validation
return None if len(sorted_order) != k else sorted_order
2. Missing Values in the Ordering
Pitfall: Not all values from 1 to k might appear in the constraints. If a value doesn't appear in any constraint, it won't have any edges in the graph but should still be included in the ordering.
Wrong Approach:
# Only considering values that appear in constraints
visited = set()
for a, b in conditions:
visited.add(a)
visited.add(b)
# This might miss values that don't appear in any constraint!
Solution: Initialize the queue with ALL values from 1 to k that have zero in-degree, not just those appearing in constraints:
# Correct initialization - checks all values from 1 to k
queue = deque([i for i in range(1, k+1) if in_degree[i] == 0])
3. Index Out of Bounds When Building the Matrix
Pitfall: Using 1-indexed values directly as array indices when placing elements in the matrix, or incorrectly handling the mapping between values and positions.
Wrong Implementation:
# WRONG: Using values directly as indices
for i, v in enumerate(row_order):
result_matrix[v][col_order[i]] = v # Values are 1-indexed, arrays are 0-indexed!
Solution: Maintain proper mappings between values (1 to k) and array indices (0 to k-1):
# Create proper value-to-column-index mapping
value_to_col_index = [0] * (k + 1) # Index 0 unused, indices 1-k for values 1-k
for col_idx, value in enumerate(col_order):
value_to_col_index[value] = col_idx # col_idx is 0-indexed
# Use enumeration index for row, mapped index for column
for row_idx, value in enumerate(row_order):
col_idx = value_to_col_index[value]
result_matrix[row_idx][col_idx] = value
4. Handling Self-Loops and Duplicate Constraints
Pitfall: The constraints might contain self-loops (e.g., [2, 2]
) or duplicate edges (e.g., multiple [1, 3]
constraints), which can cause incorrect in-degree counting or infinite loops.
Solution: Add validation to skip self-loops and handle duplicates:
def topological_sort(conditions):
adjacency_list = defaultdict(set) # Use set to avoid duplicate edges
in_degree = [0] * (k + 1)
for value_a, value_b in conditions:
if value_a == value_b: # Skip self-loops
continue
if value_b not in adjacency_list[value_a]: # Avoid counting duplicates
adjacency_list[value_a].add(value_b)
in_degree[value_b] += 1
# ... rest of the implementation
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Topological Sort Topological Order Prereq Breadth First Search Review problems graph_bfs Directed Graph Before we get started on topological order let's talk about directed graphs first A graph is directed when its edges have directions these edges are also called arcs Suppose that v w are vertices in a directed
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
Want a Structured Path to Master System Design Too? Donβt Miss This!