Skip to content

Commit 4bc816b

Browse files
committed
feat(algorithms, graphs): sort items by groups
1 parent 79b7801 commit 4bc816b

File tree

6 files changed

+268
-0
lines changed

6 files changed

+268
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Sort Items by Groups Respecting Dependencies
2+
3+
You are given n items indexed from 0 to n−1. Each item belongs to 0 or one of m groups, described by the array group,
4+
where:
5+
- `group[i]` represents the group of the ith item.
6+
- If `group[i]==−1`, the item isn’t assigned to any existing group and should be treated as belonging to its own unique
7+
group.
8+
9+
You’re also given a list, `beforeItems`, where `beforeItems[i]` contains all items that must precede item `i` in the final
10+
ordering.
11+
12+
Your goal is to arrange all n items in a list that satisfies both of the following rules:
13+
14+
- **Dependency order**: Every item must appear after all the items listed in `beforeItems[i]`.
15+
- **Group continuity**: All items that belong to the same group must appear next to each other in the final order.
16+
17+
If there are multiple valid orderings, return any of them. If there’s no possible ordering, return an empty list.
18+
19+
## Constraints
20+
21+
- 1 <= `m` <= `n` <= 3 * 10^4
22+
- `group.length` == `beforeItems.length` == n
23+
- -1 <= `group[i]` <= m - 1
24+
- 0 <= `beforeItems[i].length` <= n - 1
25+
- 0 <= `beforeItems[i][j]` <= n - 1
26+
- i != `beforeItems[i][j]`
27+
- `beforeItems[i]` does not contain duplicates elements.
28+
29+
## Examples
30+
31+
![Example 1](./images/examples/sort_items_by_groups_respecting_dependencies_example_1.png)
32+
![Example 2](./images/examples/sort_items_by_groups_respecting_dependencies_example_2.png)
33+
![Example 3](./images/examples/sort_items_by_groups_respecting_dependencies_example_3.png)
34+
35+
Example 4
36+
37+
```text
38+
Input: n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3,6],[],[],[]]
39+
Output: [6,3,4,1,5,2,0,7]
40+
```
41+
42+
Example 5
43+
44+
```text
45+
Input: n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3],[],[4],[]]
46+
Output: []
47+
Explanation: This is the same as example 4 except that 4 needs to be before 6 in the sorted list.
48+
```
49+
50+
## Topics
51+
52+
- Depth-First Search
53+
- Breadth-First Search
54+
- Graph Theory
55+
- Topological Sort
56+
57+
## Hints
58+
59+
- Think of it as a graph problem.
60+
- We need to find a topological order on the dependency graph.
61+
- Build two graphs, one for the groups and another for the items.
62+
63+
## Solution
64+
65+
The essence of this problem lies in managing two-level dependencies:
66+
67+
- Item-level dependencies: Each item must appear only after all items in its beforeItems list.
68+
- Group-level contiguity: All items belonging to the same group must appear consecutively in the final order.
69+
70+
This dual requirement naturally maps to a hierarchical topological sorting approach on two directed acyclic graphs (DAGs).
71+
One for groups and one for items. First, we normalize the input by assigning a unique group to every item that originally
72+
has group[i] = -1, so each item belongs to exactly one group. Then, we build two directed graphs:
73+
74+
- An item-level dependency graph is built from beforeItems, where an edge u → v indicates that item u must come before
75+
item v.
76+
- A group-level dependency graph is constructed by examining inter-group dependencies: if an item u depends on an item v
77+
and they belong to different groups, an edge is added from group[v] to group[u], meaning group[v] must come before
78+
group[u].
79+
80+
To solve the problem, we:
81+
82+
- Sort the groups topologically using the group-level graph to determine a valid global sequence of groups. If a cycle
83+
exists (i.e., no valid ordering), return an empty list.
84+
85+
- For each group in that global order, perform a topological sort on its internal items using only the relevant subgraph
86+
of item dependencies. If any group contains a cyclic dependency among its items, return an empty list.
87+
88+
- Concatenate the sorted items group by group, following the global order of groups from step 1.
89+
90+
This hierarchical approach cleanly addresses both concerns: the outer sort enforces inter-group dependency and contiguity,
91+
while the inner sorts respect intra-group ordering. The result is a single linear sequence that satisfies all constraints,
92+
or correctly reports impossibility if cycles prevent a valid schedule.
93+
94+
Using the intuition above, we implement the algorithm as follows:
95+
96+
1. We iterate through all items in the group:
97+
- If an item has no assigned group (indicated by -1):
98+
- We assign it a new unique group number equal to the current value of m.
99+
- We increment m by one.
100+
2. We create two adjacency lists (graphs): item_graph to store item-level dependencies and group_graph to store
101+
group-level dependencies.
102+
3. We create two in-degree arrays initialized with zero: item_indegree of size n to count the number of prerequisites
103+
each item has, and group_indegree of size m to count the number of prerequisite groups each group has.
104+
4. We loop through each item, curr from 0 to n - 1:
105+
- For each prerequisite prev in beforeItems[curr], we:
106+
- Add a directed edge from prev to curr in item_graph.
107+
- Increment item_indegree[curr].
108+
- If group[prev] is not equal to group[curr], we:
109+
- Add an edge from group[prev] to group[curr] in group_graph.
110+
- Increment group_indegree[group[curr]].
111+
5. We perform a topological sort using the helper function, topoSort, on the items and store the ordered items in
112+
item_order.
113+
6. We perform another topological sort using the same helper function on groups and store the ordered groups in the
114+
group_order.
115+
7. If either the item_order or group_order is empty, we return an empty array because it is impossible to create a
116+
valid ordering.
117+
8. We initialize a map, group_to_items, to map each group number to a list of its items.
118+
9. For each item in item_order:
119+
- We append each item to its corresponding group’s list in the group_to_items map. This preserves the topological
120+
order of items within each group.
121+
10. We define an array, result, to store the final valid ordering.
122+
11. For each group g in group_order:
123+
- We add all items in group_to_items[g] to the result array.
124+
12. We return the result array containing all items in a valid order.
125+
126+
The topo_sort helper function receives a list of nodes, a graph (adjacency list), and an indegree array, and returns the
127+
topological order using Kahn’s algorithm. It performs the following steps:
128+
129+
1. We initialize a queue q with all nodes in nodes that have an indegree[node] equal to 0.
130+
2. We also initialize an empty array, order, to store the topological order.
131+
3. We iterate through q and:
132+
- Repeatedly remove a node from q.
133+
- Append this node to the order array.
134+
- For each neighbor, nei, of node, we:
135+
- Decrement the indegree of nei.
136+
- If the indegree of nei is 0:
137+
- We add nei to q.
138+
4. We return the order array if all nodes are processed; otherwise, we return an empty array (indicating a cycle exists).
139+
140+
### Time Complexity
141+
142+
The solution’s time complexity is O(n.2^n) because in the worst case, when the array contains
143+
n distinct elements, the recursion tree generates all 2^n possible subsets, resulting in 2^n recursive calls. In each call,
144+
we create a copy of the current subset, which can take up to O(n) time when the subset is at its largest. Therefore, the
145+
overall time complexity of this approach is O(n.2^n).
146+
147+
### Space Complexity
148+
149+
The space complexity of the sorting step depends on the specific sorting algorithm used by the language or library, but
150+
most standard comparison-based sorts require O(log(n)) auxiliary space. The recursive backtracking process uses up to
151+
O(n) space on the call stack, as the maximum depth of recursion corresponds to building subsets of length n. The space
152+
used to store the final list of subsets is not counted toward auxiliary space.
153+
154+
Therefore, the overall auxiliary space complexity of this approach is O(n).
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from typing import List, DefaultDict
2+
from collections import deque, defaultdict
3+
4+
5+
def sort_items(
6+
n: int, m: int, group: List[int], before_items: List[List[int]]
7+
) -> List[int]:
8+
9+
# Assign new group IDs to ungrouped items. -1 means ungrouped
10+
for i in range(n):
11+
if group[i] == -1:
12+
group[i] = m
13+
m += 1
14+
15+
# Initialize adjacency lists for item level and group level graphs
16+
item_graph: DefaultDict[int, List[int]] = defaultdict(list)
17+
group_graph: DefaultDict[int, List[int]] = defaultdict(list)
18+
19+
# Lists to store in-degree graphs for items and groups
20+
item_indegree: List[int] = [0] * n
21+
group_indegree: List[int] = [0] * m
22+
23+
# Build dependency graphs
24+
for current in range(n):
25+
for previous in before_items[current]:
26+
# Add item-level edge
27+
item_graph[previous].append(current)
28+
item_indegree[current] += 1
29+
30+
# Add group-level edge
31+
if group[previous] != group[current]:
32+
group_graph[group[previous]].append(group[current])
33+
group_indegree[group[current]] += 1
34+
35+
def topological_sort(total_nodes: int, graph: DefaultDict[int, List[int]], in_degree: List[int]) -> List[int]:
36+
queue = deque()
37+
38+
# Add all nodes with 0 in-degree to the queue
39+
for node in range(total_nodes):
40+
if in_degree[node] == 0:
41+
queue.append(node)
42+
43+
order = []
44+
45+
# Process in topological order
46+
while queue:
47+
node = queue.popleft()
48+
order.append(node)
49+
50+
# Reduce in-degree of all neighbours
51+
for neighbor in graph[node]:
52+
in_degree[neighbor] -= 1
53+
if in_degree[neighbor] == 0:
54+
queue.append(neighbor)
55+
56+
# If all nodes are processed, return valid order, otherwise return empty list(cycle detected)
57+
return order if len(order) == total_nodes else []
58+
59+
# Topologically sort items and groups
60+
item_order = topological_sort(n, item_graph, item_indegree)
61+
group_order = topological_sort(m, group_graph, group_indegree)
62+
63+
# If either order is empty, a cycle exists, meaning no valid ordering
64+
if len(item_order) == 0 or len(group_order) == 0:
65+
return []
66+
67+
# Group items based on their group IDs
68+
group_to_items: DefaultDict[int, List[int]] = defaultdict(list)
69+
for item in item_order:
70+
group_to_items[group[item]].append(item)
71+
72+
# Build a final sort order following the group order
73+
result = []
74+
for g in group_order:
75+
result.extend(group_to_items[g])
76+
77+
return result
125 KB
Loading
97.5 KB
Loading
93 KB
Loading
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from algorithms.graphs.sort_items_by_group import sort_items
5+
6+
SORT_ITEMS_BY_GROUPS_TEST_CASES = [
7+
(
8+
8,
9+
2,
10+
[-1, -1, 1, 0, 0, 1, 0, -1],
11+
[[], [6], [5], [6], [3, 6], [], [], []],
12+
[6, 3, 4, 5, 2, 0, 7, 1],
13+
),
14+
(8, 2, [-1, -1, 1, 0, 0, 1, 0, -1], [[], [6], [5], [6], [3], [], [4], []], []),
15+
(6, 2, [0, 0, 1, 1, -1, -1], [[], [0], [], [2], [1, 3], [4]], [0, 1, 2, 3, 4, 5]),
16+
(4, 1, [0, 0, 0, 0], [[], [0], [1], [2]], [0, 1, 2, 3]),
17+
(3, 1, [0, 0, 0], [[2], [0], [1]], []),
18+
(5, 3, [0, 0, 1, 1, -1], [[], [0], [3], [], [2]], [0, 1, 3, 2, 4]),
19+
]
20+
21+
22+
class SortItemsByGroupsTestCase(unittest.TestCase):
23+
@parameterized.expand(SORT_ITEMS_BY_GROUPS_TEST_CASES)
24+
def test_sort_items(
25+
self,
26+
n: int,
27+
m: int,
28+
group: List[int],
29+
before_items: List[List[int]],
30+
expected: List[int],
31+
):
32+
actual = sort_items(n, m, group, before_items)
33+
self.assertEqual(expected, actual)
34+
35+
36+
if __name__ == "__main__":
37+
unittest.main()

0 commit comments

Comments
 (0)