1203. Sort Items by Groups Respecting Dependencies
Problem Description
You are given n
items numbered from 0 to n-1. Each item can belong to at most one group out of m
groups (numbered 0 to m-1). The group membership is specified by the array group
, where:
group[i]
represents the group that itemi
belongs togroup[i] = -1
means itemi
doesn't belong to any group
Additionally, there are dependency constraints between items specified by beforeItems
, where beforeItems[i]
is a list of items that must appear before item i
in the final sorted order.
Your task is to return a sorted list of all items that satisfies two conditions:
- Group Constraint: Items belonging to the same group must be placed consecutively (next to each other) in the sorted list
- Dependency Constraint: For each item
i
, all items inbeforeItems[i]
must appear before itemi
in the sorted list
If multiple valid orderings exist, return any one of them. If no valid ordering exists (due to conflicting constraints), return an empty list.
For example, if items 0, 2, and 4 belong to group 1, they must appear consecutively in the result (like ...0,2,4... or ...4,0,2...), but their internal order depends on the dependency constraints. Meanwhile, if item 3 has beforeItems[3] = [0, 2]
, then both items 0 and 2 must appear before item 3 in the final ordering.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: This problem involves items (nodes) with dependency relationships (edges). The
beforeItems
array defines directed edges between items, where if itemj
is inbeforeItems[i]
, there's an edge fromj
toi
. Additionally, we have group-level dependencies that form another graph.
Is it a tree?
- No: The dependency relationships can form multiple components and cycles. Items can have multiple predecessors (multiple items in
beforeItems[i]
), and there's no single root node.
Is the problem related to directed acyclic graphs?
- Yes: The problem requires ordering items based on dependencies, which is only possible if the dependency graph is acyclic. If there's a cycle in the dependencies, no valid ordering exists. The problem is essentially about finding a linear ordering that respects both item-level and group-level dependencies.
Topological Sort
- Yes: This is the algorithm we need. Topological sorting is designed to find a linear ordering of vertices in a directed acyclic graph where for every directed edge
u β v
, vertexu
comes beforev
in the ordering.
Conclusion: The flowchart correctly leads us to Topological Sort, which is implemented using DFS (or BFS with Kahn's algorithm). In this problem, we need to perform topological sorting at two levels:
- Group-level topological sort to determine the order of groups
- Item-level topological sort within each group to determine the order of items
The solution uses Kahn's algorithm (BFS-based topological sort) with in-degree tracking to detect cycles and produce the valid ordering.
Intuition
The key insight is recognizing that we have two levels of dependencies to handle: dependencies between individual items and implicit dependencies between groups.
When items belong to the same group, they must appear consecutively in the final ordering. This means if item A
from group 1 must come before item B
from group 2, then all items in group 1 must come before all items in group 2. This creates a group-level dependency.
Think of it like organizing tasks in a project where tasks are grouped by teams. If Task A (from Team 1) must be done before Task B (from Team 2), then Team 1 must complete all their tasks before Team 2 starts any of theirs.
This leads us to a two-layer graph structure:
- Group Graph: Where nodes are groups and edges represent dependencies between groups
- Item Graph: Where nodes are items and edges represent dependencies between items within the same group
For example, if item i
(in group G1
) must come after item j
(in group G2
), we need:
- An edge from
G2
toG1
in the group graph (groupG2
must be processed before groupG1
) - If both items are in the same group, we add an edge from
j
toi
in the item graph
The solution approach becomes clear:
- First, determine the order of groups using topological sort on the group graph
- Then, for each group in that order, determine the internal ordering of its items using topological sort
Why assign new groups to items with group[i] = -1
? This simplifies our logic - instead of handling ungrouped items as special cases, we treat each as its own single-item group. This uniform treatment makes the algorithm cleaner.
The beauty of this approach is that it decomposes a complex constraint problem into two simpler topological sorting problems, each handling one type of dependency.
Learn more about Depth-First Search, Breadth-First Search, Graph and Topological Sort patterns.
Solution Approach
The implementation follows our two-layer topological sorting strategy. Let's walk through the key steps:
Step 1: Assign Groups to Ungrouped Items
First, we handle items with group[i] = -1
by assigning each a unique group ID starting from m
:
idx = m
for i, g in enumerate(group):
if g == -1:
group[i] = idx
idx += 1
This ensures every item belongs to exactly one group, simplifying our logic.
Step 2: Build Group Membership Mapping
We create group_items
to track which items belong to each group:
group_items = [[] for _ in range(n + m)]
for i, g in enumerate(group):
group_items[group[i]].append(i)
Note we allocate n + m
slots since we might create up to n
new groups for ungrouped items.
Step 3: Build Two Graphs with In-Degree Tracking
For each dependency in beforeItems[i]
, we determine whether it creates an item-level or group-level edge:
for i, gi in enumerate(group):
for j in beforeItems[i]:
gj = group[j]
if gi == gj: # Same group - item-level dependency
item_degree[i] += 1
item_graph[j].append(i)
else: # Different groups - group-level dependency
group_degree[gi] += 1
group_graph[gj].append(gi)
- Same group (
gi == gj
): Add edgej β i
in item graph - Different groups: Add edge
gj β gi
in group graph (group ofj
must come before group ofi
)
Step 4: Topological Sort Using Kahn's Algorithm
The helper function topo_sort
implements Kahn's algorithm:
def topo_sort(degree, [graph](/problems/graph_intro), items):
q = deque(i for i in items if degree[i] == 0)
res = []
while q:
i = q.popleft()
res.append(i)
for j in graph[i]:
degree[j] -= 1
if degree[j] == 0:
q.append(j)
return res if len(res) == len(items) else []
This BFS-based approach:
- Starts with all nodes having in-degree 0 (no prerequisites)
- Processes each node and reduces in-degrees of its neighbors
- Returns empty list if cycle detected (when
len(res) != len(items)
)
Step 5: Two-Level Sorting
First, sort groups:
group_order = topo_sort(group_degree, group_graph, range(n + m))
if not group_order:
return [] # Cycle in group dependencies
Then, for each group in order, sort its items:
for gi in group_order:
items = group_items[gi]
item_order = topo_sort(item_degree, item_graph, items)
if len(items) != len(item_order):
return [] # Cycle in item dependencies within group
ans.extend(item_order)
The algorithm ensures:
- Groups are processed in valid dependency order
- Within each group, items are arranged respecting their dependencies
- Cycles at any level result in returning an empty list
Time complexity: O(n + m + E)
where E
is the total number of dependencies.
Space complexity: O(n + m + E)
for storing graphs and in-degree arrays.
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 to illustrate the solution approach.
Given:
n = 5
items (0, 1, 2, 3, 4)m = 2
groups (0, 1)group = [1, -1, 0, 1, -1]
- Items 0, 3 belong to group 1
- Item 2 belongs to group 0
- Items 1, 4 have no group (-1)
beforeItems = [[], [0], [], [2], [1, 3]]
- Item 1 must come after item 0
- Item 3 must come after item 2
- Item 4 must come after items 1 and 3
Step 1: Assign groups to ungrouped items
- Item 1 gets group 2 (first available after m=2)
- Item 4 gets group 3
- Updated
group = [1, 2, 0, 1, 3]
Step 2: Build group membership
group_items: - Group 0: [2] - Group 1: [0, 3] - Group 2: [1] - Group 3: [4]
Step 3: Build graphs
For each item's dependencies:
-
Item 1 depends on item 0:
- Item 0 (group 1) β Item 1 (group 2): Different groups
- Add edge: group 1 β group 2 in group graph
-
Item 3 depends on item 2:
- Item 2 (group 0) β Item 3 (group 1): Different groups
- Add edge: group 0 β group 1 in group graph
-
Item 4 depends on items 1 and 3:
- Item 1 (group 2) β Item 4 (group 3): Different groups
- Add edge: group 2 β group 3 in group graph
- Item 3 (group 1) β Item 4 (group 3): Different groups
- Add edge: group 1 β group 3 in group graph
Resulting graphs:
Group graph: - Group 0 β Group 1 - Group 1 β Group 2 - Group 1 β Group 3 - Group 2 β Group 3 Group in-degrees: [0, 1, 1, 2] Item graph (only same-group edges): Empty in this example Item in-degrees: [0, 0, 0, 0, 0]
Step 4: Topological sort of groups
- Start with group 0 (in-degree 0)
- Process group 0: reduces group 1's in-degree to 0
- Process group 1: reduces group 2's in-degree to 0, group 3's to 1
- Process group 2: reduces group 3's in-degree to 0
- Process group 3
Group order: [0, 1, 2, 3]
Step 5: Process each group and sort its items
- Group 0: items [2], no internal dependencies β output:
[2]
- Group 1: items [0, 3], no internal dependencies β output:
[0, 3]
- Group 2: items [1], no internal dependencies β output:
[1]
- Group 3: items [4], no internal dependencies β output:
[4]
Final Result: [2, 0, 3, 1, 4]
Let's verify:
- β Items in same group are consecutive: Group 1 items (0, 3) are together
- β Dependencies satisfied:
- Item 0 before item 1: 0 appears at index 1, 1 at index 3 β
- Item 2 before item 3: 2 appears at index 0, 3 at index 2 β
- Items 1, 3 before item 4: 1 at index 3, 3 at index 2, 4 at index 4 β
The algorithm successfully found a valid ordering by first determining the group order (0β1β2β3) and then placing each group's items consecutively in that order.
Solution Implementation
1class Solution:
2 def sortItems(
3 self, n: int, m: int, group: List[int], beforeItems: List[List[int]]
4 ) -> List[int]:
5 """
6 Sort items respecting both group constraints and dependency constraints.
7
8 Args:
9 n: Number of items
10 m: Number of groups (excluding items with no group)
11 group: List where group[i] is the group of item i (-1 means no group)
12 beforeItems: List where beforeItems[i] contains items that must come before item i
13
14 Returns:
15 A valid ordering of items, or empty list if impossible
16 """
17
18 def topological_sort(in_degree: List[int], adjacency_list: List[List[int]], nodes: List[int]) -> List[int]:
19 """
20 Perform topological sort on given nodes using Kahn's algorithm.
21
22 Args:
23 in_degree: In-degree count for each node
24 adjacency_list: Adjacency list representation of the graph
25 nodes: List of nodes to sort
26
27 Returns:
28 Sorted order of nodes, or empty list if cycle detected
29 """
30 # Initialize queue with nodes having zero in-degree
31 queue = deque(node for node in nodes if in_degree[node] == 0)
32 sorted_result = []
33
34 while queue:
35 current_node = queue.popleft()
36 sorted_result.append(current_node)
37
38 # Process all neighbors of current node
39 for neighbor in adjacency_list[current_node]:
40 in_degree[neighbor] -= 1
41 if in_degree[neighbor] == 0:
42 queue.append(neighbor)
43
44 # Return result only if all nodes were processed (no cycle)
45 return sorted_result if len(sorted_result) == len(nodes) else []
46
47 # Assign unique group IDs to items without a group (-1)
48 next_group_id = m
49 groups_to_items = [[] for _ in range(n + m)] # Map group ID to its items
50
51 for item_id, group_id in enumerate(group):
52 if group_id == -1:
53 # Assign new group ID for ungrouped item
54 group[item_id] = next_group_id
55 next_group_id += 1
56 groups_to_items[group[item_id]].append(item_id)
57
58 # Initialize degree and graph structures for both items and groups
59 item_in_degree = [0] * n
60 group_in_degree = [0] * (n + m)
61 item_adjacency = [[] for _ in range(n)]
62 group_adjacency = [[] for _ in range(n + m)]
63
64 # Build dependency graphs
65 for current_item, current_group in enumerate(group):
66 for prerequisite_item in beforeItems[current_item]:
67 prerequisite_group = group[prerequisite_item]
68
69 if current_group == prerequisite_group:
70 # Same group: add item-level dependency
71 item_in_degree[current_item] += 1
72 item_adjacency[prerequisite_item].append(current_item)
73 else:
74 # Different groups: add group-level dependency
75 group_in_degree[current_group] += 1
76 group_adjacency[prerequisite_group].append(current_group)
77
78 # Sort groups topologically
79 sorted_groups = topological_sort(group_in_degree, group_adjacency, list(range(n + m)))
80 if not sorted_groups:
81 return [] # Cycle detected in group dependencies
82
83 # Process each group and sort items within it
84 final_result = []
85 for group_id in sorted_groups:
86 items_in_group = groups_to_items[group_id]
87 sorted_items = topological_sort(item_in_degree, item_adjacency, items_in_group)
88
89 # Check if all items in the group were successfully sorted
90 if len(items_in_group) != len(sorted_items):
91 return [] # Cycle detected in item dependencies
92
93 final_result.extend(sorted_items)
94
95 return final_result
96
1class Solution {
2 public int[] sortItems(int n, int m, int[] group, List<List<Integer>> beforeItems) {
3 // Assign unique group IDs to items without a group (group == -1)
4 int nextGroupId = m;
5
6 // Initialize data structures for group and item management
7 List<Integer>[] groupItems = new List[n + m]; // Items belonging to each group
8 int[] itemInDegree = new int[n]; // In-degree for each item
9 int[] groupInDegree = new int[n + m]; // In-degree for each group
10 List<Integer>[] itemGraph = new List[n]; // Dependency graph for items
11 List<Integer>[] groupGraph = new List[n + m]; // Dependency graph for groups
12
13 // Initialize all lists
14 Arrays.setAll(groupItems, k -> new ArrayList<>());
15 Arrays.setAll(itemGraph, k -> new ArrayList<>());
16 Arrays.setAll(groupGraph, k -> new ArrayList<>());
17
18 // Assign unique group IDs to ungrouped items and populate groupItems
19 for (int item = 0; item < n; item++) {
20 if (group[item] == -1) {
21 group[item] = nextGroupId++;
22 }
23 groupItems[group[item]].add(item);
24 }
25
26 // Build dependency graphs based on beforeItems constraints
27 for (int currentItem = 0; currentItem < n; currentItem++) {
28 for (int prerequisiteItem : beforeItems.get(currentItem)) {
29 if (group[currentItem] == group[prerequisiteItem]) {
30 // Same group: add item-level dependency
31 itemInDegree[currentItem]++;
32 itemGraph[prerequisiteItem].add(currentItem);
33 } else {
34 // Different groups: add group-level dependency
35 groupInDegree[group[currentItem]]++;
36 groupGraph[group[prerequisiteItem]].add(group[currentItem]);
37 }
38 }
39 }
40
41 // Create list of all group IDs for topological sort
42 List<Integer> allGroups = new ArrayList<>();
43 for (int groupId = 0; groupId < n + m; groupId++) {
44 allGroups.add(groupId);
45 }
46
47 // Perform topological sort on groups
48 List<Integer> groupOrder = topoSort(groupInDegree, groupGraph, allGroups);
49 if (groupOrder.isEmpty()) {
50 return new int[0]; // Cycle detected in group dependencies
51 }
52
53 // Process each group in topological order
54 List<Integer> result = new ArrayList<>();
55 for (int groupId : groupOrder) {
56 List<Integer> itemsInGroup = groupItems[groupId];
57
58 // Perform topological sort on items within the group
59 List<Integer> itemOrder = topoSort(itemInDegree, itemGraph, itemsInGroup);
60 if (itemOrder.size() != itemsInGroup.size()) {
61 return new int[0]; // Cycle detected in item dependencies
62 }
63
64 result.addAll(itemOrder);
65 }
66
67 // Convert result list to array
68 return result.stream().mapToInt(Integer::intValue).toArray();
69 }
70
71 /**
72 * Performs topological sort using Kahn's algorithm
73 * @param inDegree Array of in-degrees for nodes
74 * @param graph Adjacency list representation of the graph
75 * @param nodes List of nodes to be sorted
76 * @return Topologically sorted list of nodes, or empty list if cycle exists
77 */
78 private List<Integer> topoSort(int[] inDegree, List<Integer>[] graph, List<Integer> nodes) {
79 // Initialize queue with nodes having zero in-degree
80 Deque<Integer> queue = new ArrayDeque<>();
81 for (int node : nodes) {
82 if (inDegree[node] == 0) {
83 queue.offer(node);
84 }
85 }
86
87 // Process nodes in topological order
88 List<Integer> sortedNodes = new ArrayList<>();
89 while (!queue.isEmpty()) {
90 int currentNode = queue.poll();
91 sortedNodes.add(currentNode);
92
93 // Reduce in-degree for adjacent nodes
94 for (int adjacentNode : graph[currentNode]) {
95 if (--inDegree[adjacentNode] == 0) {
96 queue.offer(adjacentNode);
97 }
98 }
99 }
100
101 // Return sorted list if all nodes are processed, otherwise return empty list (cycle exists)
102 return sortedNodes.size() == nodes.size() ? sortedNodes : List.of();
103 }
104}
105
1class Solution {
2public:
3 vector<int> sortItems(int n, int m, vector<int>& group, vector<vector<int>>& beforeItems) {
4 // Assign unique group IDs to items without a group (group[i] == -1)
5 int nextGroupId = m;
6 vector<vector<int>> groupToItems(n + m); // Maps group ID to list of items in that group
7 vector<int> itemInDegree(n); // In-degree for item-level topological sort
8 vector<int> groupInDegree(n + m); // In-degree for group-level topological sort
9 vector<vector<int>> itemAdjList(n); // Adjacency list for item dependencies within same group
10 vector<vector<int>> groupAdjList(n + m); // Adjacency list for group dependencies
11
12 // Assign items to groups, creating new groups for ungrouped items
13 for (int i = 0; i < n; ++i) {
14 if (group[i] == -1) {
15 group[i] = nextGroupId++;
16 }
17 groupToItems[group[i]].push_back(i);
18 }
19
20 // Build dependency graphs for both items and groups
21 for (int currentItem = 0; currentItem < n; ++currentItem) {
22 for (int prerequisiteItem : beforeItems[currentItem]) {
23 if (group[currentItem] == group[prerequisiteItem]) {
24 // Same group: create item-level dependency
25 ++itemInDegree[currentItem];
26 itemAdjList[prerequisiteItem].push_back(currentItem);
27 } else {
28 // Different groups: create group-level dependency
29 ++groupInDegree[group[currentItem]];
30 groupAdjList[group[prerequisiteItem]].push_back(group[currentItem]);
31 }
32 }
33 }
34
35 // Create list of all group IDs for topological sorting
36 vector<int> allGroups(n + m);
37 iota(allGroups.begin(), allGroups.end(), 0);
38
39 // Lambda function to perform topological sort using Kahn's algorithm
40 auto topologicalSort = [](vector<vector<int>>& adjacencyList,
41 vector<int>& inDegree,
42 vector<int>& nodes) -> vector<int> {
43 queue<int> zeroInDegreeQueue;
44
45 // Find all nodes with in-degree 0
46 for (int& node : nodes) {
47 if (inDegree[node] == 0) {
48 zeroInDegreeQueue.push(node);
49 }
50 }
51
52 vector<int> sortedOrder;
53
54 // Process nodes in topological order
55 while (!zeroInDegreeQueue.empty()) {
56 int currentNode = zeroInDegreeQueue.front();
57 zeroInDegreeQueue.pop();
58 sortedOrder.push_back(currentNode);
59
60 // Decrease in-degree of neighbors
61 for (int& neighbor : adjacencyList[currentNode]) {
62 if (--inDegree[neighbor] == 0) {
63 zeroInDegreeQueue.push(neighbor);
64 }
65 }
66 }
67
68 // Return empty vector if cycle detected (not all nodes processed)
69 return sortedOrder.size() == nodes.size() ? sortedOrder : vector<int>();
70 };
71
72 // Sort groups topologically
73 vector<int> sortedGroups = topologicalSort(groupAdjList, groupInDegree, allGroups);
74 if (sortedGroups.empty()) {
75 return vector<int>(); // Cycle detected in group dependencies
76 }
77
78 // Sort items within each group and build final result
79 vector<int> result;
80 for (int& groupId : sortedGroups) {
81 vector<int> itemsInGroup = groupToItems[groupId];
82 vector<int> sortedItemsInGroup = topologicalSort(itemAdjList, itemInDegree, itemsInGroup);
83
84 // Check if all items in the group were successfully sorted
85 if (itemsInGroup.size() != sortedItemsInGroup.size()) {
86 return vector<int>(); // Cycle detected in item dependencies
87 }
88
89 // Append sorted items to final result
90 result.insert(result.end(), sortedItemsInGroup.begin(), sortedItemsInGroup.end());
91 }
92
93 return result;
94 }
95};
96
1/**
2 * Sorts items based on group constraints and dependency relationships
3 * @param n - Number of items
4 * @param m - Number of initial groups
5 * @param group - Array where group[i] is the group of item i (-1 means no group)
6 * @param beforeItems - Array where beforeItems[i] contains items that must come before item i
7 * @returns Sorted array of items, or empty array if no valid ordering exists
8 */
9function sortItems(n: number, m: number, group: number[], beforeItems: number[][]): number[] {
10 // Track next available group ID for ungrouped items
11 let nextGroupId = m;
12
13 // Initialize data structures
14 // groupItems[i] contains all items belonging to group i
15 const groupItems: number[][] = new Array(n + m).fill(0).map(() => []);
16 // itemDegree[i] tracks in-degree for item i within its group
17 const itemDegree: number[] = new Array(n).fill(0);
18 // groupDegree[i] tracks in-degree for group i
19 const groupDegree: number[] = new Array(n + m).fill(0);
20 // itemGraph[i] contains items that depend on item i within the same group
21 const itemGraph: number[][] = new Array(n).fill(0).map(() => []);
22 // groupGraph[i] contains groups that depend on group i
23 const groupGraph: number[][] = new Array(n + m).fill(0).map(() => []);
24
25 // Assign ungrouped items to new groups and populate groupItems
26 for (let i = 0; i < n; i++) {
27 if (group[i] === -1) {
28 // Assign ungrouped item to a new unique group
29 group[i] = nextGroupId++;
30 }
31 groupItems[group[i]].push(i);
32 }
33
34 // Build dependency graphs for both items and groups
35 for (let currentItem = 0; currentItem < n; currentItem++) {
36 for (const prerequisiteItem of beforeItems[currentItem]) {
37 if (group[currentItem] === group[prerequisiteItem]) {
38 // Same group: add edge in item graph
39 itemDegree[currentItem]++;
40 itemGraph[prerequisiteItem].push(currentItem);
41 } else {
42 // Different groups: add edge in group graph
43 groupDegree[group[currentItem]]++;
44 groupGraph[group[prerequisiteItem]].push(group[currentItem]);
45 }
46 }
47 }
48
49 // Create array of all group IDs for topological sorting
50 const allGroupIds = new Array(n + m).fill(0).map((_, index) => index);
51
52 /**
53 * Performs topological sort using Kahn's algorithm
54 * @param graph - Adjacency list representation of the graph
55 * @param degree - In-degree array for nodes
56 * @param nodes - Array of nodes to sort
57 * @returns Topologically sorted array, or empty array if cycle detected
58 */
59 const topologicalSort = (graph: number[][], degree: number[], nodes: number[]): number[] => {
60 const queue: number[] = [];
61
62 // Find all nodes with in-degree 0
63 for (const node of nodes) {
64 if (degree[node] === 0) {
65 queue.push(node);
66 }
67 }
68
69 const result: number[] = [];
70
71 // Process nodes with BFS
72 while (queue.length > 0) {
73 const currentNode = queue.pop()!;
74 result.push(currentNode);
75
76 // Decrease in-degree for all neighbors
77 for (const neighbor of graph[currentNode]) {
78 degree[neighbor]--;
79 if (degree[neighbor] === 0) {
80 queue.push(neighbor);
81 }
82 }
83 }
84
85 // Check if all nodes were processed (no cycle)
86 return result.length === nodes.length ? result : [];
87 };
88
89 // Sort groups topologically
90 const sortedGroups = topologicalSort(groupGraph, groupDegree, allGroupIds);
91 if (sortedGroups.length === 0) {
92 // Cycle detected in group dependencies
93 return [];
94 }
95
96 const finalResult: number[] = [];
97
98 // Process each group in sorted order
99 for (const groupId of sortedGroups) {
100 const itemsInGroup = groupItems[groupId];
101
102 // Sort items within the current group
103 const sortedItemsInGroup = topologicalSort(itemGraph, itemDegree, itemsInGroup);
104
105 // Check if all items in the group were successfully sorted
106 if (sortedItemsInGroup.length !== itemsInGroup.length) {
107 // Cycle detected within group
108 return [];
109 }
110
111 // Add sorted items to final result
112 finalResult.push(...sortedItemsInGroup);
113 }
114
115 return finalResult;
116}
117
Time and Space Complexity
The time complexity is O(n + m + E)
, where n
is the number of items, m
is the initial number of groups, and E
is the total number of edges (dependencies) in the beforeItems
list.
Breaking down the time complexity:
- Creating
group_items
and assigning isolated items to new groups:O(n)
- Building the dependency graphs (
item_graph
andgroup_graph
):O(E)
where we iterate through all items and their dependencies - Topological sort for groups:
O(n + m + E_g)
whereE_g
is the number of group-level edges - Topological sort for items within each group:
O(n + E_i)
whereE_i
is the number of item-level edges - Since
E_g + E_i β€ E
, the total isO(n + m + E)
However, if we consider that E
can be at most O(nΒ²)
in the worst case (each item can depend on all other items), and typically m β€ n
, the time complexity simplifies to O(nΒ²)
in the worst case.
The space complexity is O(n + m + E)
:
group_items
:O(n + m)
listsitem_degree
anditem_graph
:O(n)
andO(n + E_i)
group_degree
andgroup_graph
:O(n + m)
andO(n + m + E_g)
- Queue in topological sort:
O(n + m)
at most - Total space:
O(n + m + E)
Note: The reference answer states O(n + m)
for both complexities, which appears to be considering only the structural overhead without accounting for the edges in the dependency graph. This would be accurate only if the number of dependencies is bounded by O(n + m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Duplicate Edges in Group Dependencies
The Pitfall: When building the group dependency graph, if items A and B are in different groups and there are multiple dependencies between items in their respective groups, we might add the same group-level edge multiple times. This leads to incorrect in-degree counts.
Example Scenario:
- Item 0 and 1 are in group G1
- Item 2 and 3 are in group G2
- Dependencies: item 2 depends on both items 0 and 1
Without proper handling, we'd add the edge G1βG2 twice, making group_in_degree[G2] = 2
instead of 1. This breaks the topological sort since G2 will never reach in-degree 0 even after processing G1.
Solution: Track visited group pairs to avoid duplicate edges:
# Build dependency graphs with duplicate edge prevention
visited_group_edges = set()
for current_item, current_group in enumerate(group):
for prerequisite_item in beforeItems[current_item]:
prerequisite_group = group[prerequisite_item]
if current_group == prerequisite_group:
# Same group: add item-level dependency
item_in_degree[current_item] += 1
item_adjacency[prerequisite_item].append(current_item)
else:
# Different groups: add group-level dependency only if not seen
edge = (prerequisite_group, current_group)
if edge not in visited_group_edges:
visited_group_edges.add(edge)
group_in_degree[current_group] += 1
group_adjacency[prerequisite_group].append(current_group)
2. Self-Loop Detection in Group Dependencies
The Pitfall: The code doesn't explicitly check for self-loops at the group level. If item dependencies create a circular dependency between groups (e.g., item in G1 depends on item in G2, and item in G2 depends on item in G1), this creates an impossible constraint.
Solution: Add explicit self-loop detection or rely on the topological sort to catch cycles:
# When building group dependencies, check for self-loops if current_group == prerequisite_group: # Handle same-group dependency ... elif (prerequisite_group, current_group) in visited_group_edges: # Skip duplicate edge continue elif (current_group, prerequisite_group) in visited_group_edges: # Detected bidirectional dependency - will be caught by topo sort # But could return early here for efficiency pass
3. Incorrect Group Count Assumption
The Pitfall: Allocating exactly n + m
slots for groups assumes worst case where all n
items have group[i] = -1
. However, if some items already have groups assigned, we don't need all n
extra groups.
Solution: While the current approach works (unused groups are harmless), a more memory-efficient approach would be:
# Count actual number of ungrouped items first
ungrouped_count = sum(1 for g in group if g == -1)
total_groups = m + ungrouped_count
groups_to_items = [[] for _ in range(total_groups)]
4. Missing Validation of Input Constraints
The Pitfall: The code doesn't validate that items in beforeItems[i]
are valid item indices (0 to n-1), which could cause index errors.
Solution: Add input validation:
# Validate dependencies reference valid items
for i, deps in enumerate(beforeItems):
for dep in deps:
if not (0 <= dep < n):
return [] # Invalid dependency
if dep == i:
return [] # Self-dependency
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
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
Want a Structured Path to Master System Design Too? Donβt Miss This!