1203. Sort Items by Groups Respecting Dependencies


Problem Description

In this problem, we have n items and m groups. Items are assigned to groups and some items must come before others. We need to find a sorted list of the items such that items from the same group are adjacent in the sorted list, and any item that must come before another item does so in this list.

Specifically:

  • Items are identified by integers from 0 to n - 1.
  • Groups are identified by integers from 0 to m - 1.
  • Each item may belong to one group or no group at all (indicated by -1).
  • Some items have dependencies, i.e., they must come before certain other items in the sorted list, and these dependencies are given as beforeItems, where beforeItems[i] indicates the list of items that should come before item i.

The goal is to return a sorted list of items fulfilling all the given criteria, or an empty list if no such arrangement is possible.

Intuition

To solve this problem, we need a way to deal with groupings and item dependencies at the same time. One effective approach is to use topological sort, which is used in dependency resolution algorithms where you need to find a linear ordering of nodes based on their dependencies.

Here's the intuition broken down:

  1. Handle Group Assignments:

    • Items without a group should be treated as if they are in their own unique group. This can be done by assigning new group numbers starting from m to n - 1 to items without a group.
    • Create a list of items for each group so that we can process them together later.
  2. Topological Sorting:

    • Perform a topological sort on the groups themselves to ensure that groups with dependencies are ordered correctly.
    • Perform a topological sort for the items within each group to ensure that items with dependencies are ordered correctly.
  3. Creating a Dependency Graph and a Degree List:

    • For each item, have a list (or adjacency list) to represent its dependencies (edges towards other items and groups).
    • For each item and group, calculate the number of incoming edges (in-degree), which represents how many dependencies they have.
  4. Building the Solution:

    • Use topological sorting to order the groups. If the groups can't be sorted due to circular dependencies, return an empty list.
    • For each group, use topological sorting to order the items within that group. If the items can't be sorted due to circular dependencies, again return an empty list.
    • If both topological sorts succeed, combine the sorted items from each group according to the sorted group order to form the final list.
  5. Resolving Possible Multiple Valid Solutions:

    • The problem statement allows for any valid solution if there is more than one. Hence, we don't need to find all possible solutions but just need to return one if it exists.

The provided Python code is a direct implementation of this intuition. It manages the group assignments, constructs the dependency graphs and in-degree lists, performs topological sorting, and assembles the final sorted list of items based on the sorting of groups and items within those groups.

Learn more about Depth-First Search, Breadth-First Search, Graph and Topological Sort patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece๏ผš

Which of the following shows the order of node visit in a Breadth-first Search?

Solution Approach

The solution approach implements a topological sort to handle dependency resolution between items and groups. Here is a step-by-step implementation walkthrough:

  1. Assign New Groups to Ungrouped Items:

    • Imagine each ungrouped item as its own group. Start assigning new group identifiers, beginning with m and incrementing for each new item without a group.
    • Use this process to create a group_items list that holds lists of items, each representing a group.
  2. Creating Graphs and Degree Lists:

    • Prepare two sets of graphs (item_graph and group_graph) and two degree lists (item_degree and group_degree). These data structures are used to represent dependencies between items and dependencies between groups, respectively.
    • Graphs are represented using adjacency lists. In-degree lists (item_degree and group_degree) are used to keep track of the number of incoming edges (dependencies) for each node (item or group).
  3. Adding Edges to Graphs:

    • Iterate over the groups and beforeItems list to add dependencies to the graphs.
    • If the current item and the before item belong to the same group, add a dependency edge in the item_graph and increase the item's in-degree.
    • If they belong to different groups, add a dependency edge in the group_graph and increase the group's in-degree.
  4. Topological Sort Function:

    • Define the topo_sort() function that performs a topological sort based on a degree list and graph.
    • Use a queue to process the nodes that have an in-degree of 0 (no dependencies).
    • For each processed node, decrease the in-degree of its dependent nodes. If a node's in-degree reaches 0, add it to the queue.
    • If the number of processed nodes is equal to the total items, this implies a valid topological order is found. Otherwise, it implies a cycle, and the function returns an empty list.
  5. Sorting Groups:

    • Perform a topological sort on the groups using the group_graph and group_degree.
    • If the topological sort does not return a valid order, it indicates that a cycle exists between groups and there is no valid solution, so return an empty list.
  6. Sorting Items Within Each Group:

    • Iterate over each group based on the order determined by the group topological sort.
    • Perform a topological sort on the items of the current group based on the item_graph and item_degree.
    • If the topological sort returns a valid order for items within the group, append this order to the final answer.
  7. Final Answer:

    • After processing all groups and their corresponding items, assemble the final sorted list of items. Ensure that if there is a valid topological order for both items and groups, this list is returned.

By using this approach, the implementation handles both items' and groups' internal dependencies. The defined topo_sort function processes items within groups and the groups themselves, resolving these dependencies and obtaining the final sorted list of items.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

Breadth first search can be used to find the shortest path between two nodes in a directed graph.

Example Walkthrough

Let's consider a small example with n = 5 items and m = 2 groups to walk through the solution approach.

Suppose we have the following group assignment:

  • Item 0 belongs to group 0.
  • Item 1 belongs to group 0.
  • Item 2 belongs to group 1.
  • Item 3 has no group (indicated by -1).
  • Item 4 has no group (indicated by -1).

And we have these dependencies in beforeItems:

  • beforeItems[0] = [2] (Item 2 must come before Item 0).
  • beforeItems[1] = [] (No dependencies for Item 1).
  • beforeItems[2] = [] (No dependencies for Item 2).
  • beforeItems[3] = [1,4] (Items 1 and 4 must come before Item 3).
  • beforeItems[4] = [] (No dependencies for Item 4).

Step 1: Assign New Groups to Ungrouped Items

Items 3 and 4 don't belong to any group, so we assign them to new groups:

  • Item 3 will belong to group 2 (since m = 2).
  • Item 4 will belong to group 3.

The group_items list will be:

  • Group 0: [0, 1]
  • Group 1: [2]
  • Group 2: [3]
  • Group 3: [4]

Step 2: Creating Graphs and Degree Lists

We create two separate graphs (item_graph and group_graph) and degree lists (item_degree and group_degree) for dependency representation.

Our graphs and degree lists start off empty, but will be populated as follows:

  • item_graph: {0: [], 1: [], 2: [], 3: [], 4: []}
  • item_degree: {0: 0, 1: 0, 2: 0, 3: 0, 4: 0}
  • group_graph: {0: [], 1: [], 2: [], 3: []}
  • group_degree: {0: 0, 1: 0, 2: 0, 3: 0}

Step 3: Adding Edges to Graphs

We add the necessary edges from the beforeItems specification:

  • Add an edge from group 1 to group 0 because Item 2 (in group 1) must come before Item 0 (in group 0). This also increases the in-degree of group 0.
  • For Item 3, add edges from Items 1 and 4 to Item 3 in item_graph. This increases the in-degree of Item 3 by 2.

Updated graphs and in-degree lists after this step:

  • item_graph: {0: [], 1: [], 2: [], 3: [1, 4], 4: []}
  • item_degree: {0: 0, 1: 0, 2: 0, 3: 2, 4: 0}
  • group_graph: {0: [1], 1: [], 2: [0, 3], 3: [2]}
  • group_degree: {0: 1, 1: 0, 2: 0, 3: 1}

Step 4: Topological Sort Function

We aim to sort both the groups and the items within groups topologically using the topo_sort() function.

Step 5: Sorting Groups

Using topo_sort() on groups, we can find an order, for example, [1, 3, 2, 0], based on the dependencies (group 1 has no dependencies, followed by group 3, and so on).

Step 6: Sorting Items Within Each Group

Following the sorted groups, we proceed to sort the items within each group:

  • Group 1: Item 2 has no dependencies.
  • Group 3: Item 4 has no dependencies.
  • Group 2: Item 3 can be sorted directly as its dependencies are from other groups.
  • Group 0: Item 1 has no dependencies, and Item 0 comes after Item 2 from another group.

Step 7: Final Answer

After sorting, we combine the lists of items within the sorted groups to get the final answer, which could be [2, 4, 3, 1, 0].

This reflects that the items within the same group are adjacent, and dependencies are preserved. If any step in this process had found a cycle (a dependency that can't be resolved), we would have returned an empty list instead.

Solution Implementation

1from collections import deque  # importing deque from collections for queue functionality
2
3class Solution:
4    def sortItems(self, n: int, m: int, group: List[int], beforeItems: List[List[int]]) -> List[int]:
5        # Function to perform topological sort
6        def topological_sort(degrees, graph, items):
7            # Initialize a queue with items that have no prerequisites (degree = 0)
8            queue = deque([item for item in items if degrees[item] == 0])
9            result = []
10            while queue:
11                current = queue.popleft()
12                result.append(current)
13                for neighbor in graph[current]:
14                    # Decrement the degree of neighboring items since current is processed
15                    degrees[neighbor] -= 1
16                    # If neighbor's degree is zero, add it to the queue
17                    if degrees[neighbor] == 0:
18                        queue.append(neighbor)
19            # Check whether we have sorted all the items
20            return result if len(result) == len(items) else []
21
22        # Assign each standalone item (-1 group) a unique group number
23        index = m
24        group_to_items = [[] for _ in range(n + m)]
25        for item, grp in enumerate(group):
26            if grp == -1:
27                group[item] = index
28                index += 1
29            group_to_items[group[item]].append(item)
30
31        # Initialize degrees and graphs for items and groups
32        item_degree = [0] * n
33        group_degree = [0] * (n + m)
34        item_graph = [[] for _ in range(n)]
35        group_graph = [[] for _ in range(n + m)]
36      
37        # Build graphs and compute degrees for items and groups
38        for i, gi in enumerate(group):
39            for j in beforeItems[i]:
40                gj = group[j]
41                if gi == gj:
42                    # Same group; add edge in item graph and update item degree
43                    item_degree[i] += 1
44                    item_graph[j].append(i)
45                else:
46                    # Different groups; add edge in group graph and update group degree
47                    if gi not in group_graph[gj]:
48                        group_degree[gi] += 1
49                        group_graph[gj].append(gi)
50
51        # Perform topological sort on the groups
52        group_order = topological_sort(group_degree, group_graph, list(range(n + m)))
53        if not group_order:
54            return []  # if sorting fails, return empty list
55      
56        final_order = []  # final sorted order of items to return
57      
58        # For each group in the sorted order, perform topological sort on its items
59        for group_index in group_order:
60            items_in_group = group_to_items[group_index]
61            item_order = topological_sort(item_degree, item_graph, items_in_group)
62            if len(items_in_group) != len(item_order):
63                return []  # if sorting fails for items within the group, return empty list
64            final_order.extend(item_order)  # append the successfully sorted items to final order
65      
66        return final_order  # return the final topologically sorted order of all items
67
1import java.util.*;
2
3class Solution {
4  
5    // Sorts a set of items with given dependencies and groups
6    public int[] sortItems(int n, int m, int[] group, List<List<Integer>> beforeItems) {
7        // Create a new unique index for items with no group
8        int newGroupIndex = m;
9      
10        // Group items by their group index
11        List<Integer>[] groupItems = new List[n + m];
12        int[] itemCountByGroup = new int[n];
13        int[] groupDegree = new int[n + m];
14        List<Integer>[] itemGraph = new List[n];
15        List<Integer>[] groupGraph = new List[n + m];
16      
17        // Initialize lists for group items and graphs
18        Arrays.setAll(groupItems, e -> new ArrayList<>());
19        Arrays.setAll(itemGraph, e -> new ArrayList<>());
20        Arrays.setAll(groupGraph, e -> new ArrayList<>());
21      
22        // Assign items without a group to a new index
23        for (int i = 0; i < n; ++i) {
24            if (group[i] == -1) {
25                group[i] = newGroupIndex++;
26            }
27            groupItems[group[i]].add(i);
28        }
29      
30        // Create dependency graphs for items and groups
31        for (int i = 0; i < n; ++i) {
32            for (int beforeItemIndex : beforeItems.get(i)) {
33                if (group[i] == group[beforeItemIndex]) {
34                    itemCountByGroup[i]++;
35                    itemGraph[beforeItemIndex].add(i);
36                } else {
37                    groupDegree[group[i]]++;
38                    groupGraph[group[beforeItemIndex]].add(group[i]);
39                }
40            }
41        }
42      
43        // Sort groups using topological sort
44        List<Integer> allItems = new ArrayList<>();
45        for (int i = 0; i < n + m; ++i) {
46            allItems.add(i);
47        }
48        List<Integer> groupOrder = topologicalSort(groupDegree, groupGraph, allItems);
49        if (groupOrder.isEmpty()) {
50            return new int[0];
51        }
52      
53        // Sort items within sorted groups
54        List<Integer> result = new ArrayList<>();
55        for (int gid : groupOrder) {
56            List<Integer> groupItemList = groupItems[gid];
57            List<Integer> itemOrder = topologicalSort(itemCountByGroup, itemGraph, groupItemList);
58            if (itemOrder.size() != groupItemList.size()) {
59                return new int[0];
60            }
61            result.addAll(itemOrder);
62        }
63      
64        // Convert the result to primitive int array
65        return result.stream().mapToInt(i -> i).toArray();
66    }
67  
68    // Performs a topological sort on the graph based on incoming degree count
69    private List<Integer> topologicalSort(int[] degree, List<Integer>[] graph, List<Integer> items) {
70        Deque<Integer> queue = new ArrayDeque<>();
71        for (int i : items) {
72            if (degree[i] == 0) {
73                queue.offer(i);
74            }
75        }
76      
77        List<Integer> sortedItems = new ArrayList<>();
78        while (!queue.isEmpty()) {
79            int current = queue.poll();
80            sortedItems.add(current);
81            for (int nextItem : graph[current]) {
82                if (--degree[nextItem] == 0) {
83                    queue.offer(nextItem);
84                }
85            }
86        }
87      
88        // Return the sorted items or an empty list if the sorting is not possible
89        return sortedItems.size() == items.size() ? sortedItems : Collections.emptyList();
90    }
91}
92
1#include <vector>
2#include <queue>
3#include <numeric>
4using namespace std;
5
6class Solution {
7public:
8    vector<int> sortItems(int n, int m, vector<int>& group, vector<vector<int>>& beforeItems) {
9        int nextGroupId = m;
10        vector<vector<int>> groupItems(n + m);
11        vector<int> itemInDegrees(n, 0);
12        vector<int> groupInDegrees(n + m, 0);
13        vector<vector<int>> itemGraph(n);
14        vector<vector<int>> groupGraph(n + m);
15
16        // Assign unique group ids to items with no group
17        for (int i = 0; i < n; ++i) {
18            if (group[i] == -1) {
19                group[i] = nextGroupId++;
20            }
21            groupItems[group[i]].push_back(i);
22        }
23
24        // Construct item and group dependency graphs, and calculate in-degrees
25        for (int i = 0; i < n; ++i) {
26            for (int j : beforeItems[i]) {
27                if (group[i] == group[j]) {
28                    // j must come before i and both are in the same group
29                    ++itemInDegrees[i];
30                    itemGraph[j].push_back(i);
31                } else {
32                    // j must come before i and both are in different groups
33                    ++groupInDegrees[group[i]];
34                    groupGraph[group[j]].push_back(group[i]);
35                }
36            }
37        }
38      
39        // List of items and groups for topological sort
40        vector<int> entities(n + m);
41        iota(entities.begin(), entities.end(), 0);
42
43        // Function for topological sorting
44        auto topoSort = [](const vector<vector<int>>& graph, vector<int>& inDegrees, vector<int>& entities) -> vector<int> {
45            queue<int> queue;
46            for (int entity : entities) {
47                if (inDegrees[entity] == 0) {
48                    queue.push(entity);
49                }
50            }
51            vector<int> sorted;
52            while (!queue.empty()) {
53                int current = queue.front();
54                queue.pop();
55                sorted.push_back(current);
56                for (int next : graph[current]) {
57                    if (--inDegrees[next] == 0) {
58                        queue.push(next);
59                    }
60                }
61            }
62            // If sorted contains all entities, sorting succeeded
63            return sorted.size() == entities.size() ? sorted : vector<int>();
64        };
65
66        // Perform topological sort on the group graph
67        vector<int> sortedGroups = topoSort(groupGraph, groupInDegrees, entities);
68        if (sortedGroups.empty()) {
69            return {}; // unable to sort groups
70        }
71
72        vector<int> result;
73      
74        // For each group, sort items within that group
75        for (int groupId : sortedGroups) {
76            entities = groupItems[groupId]; // Get items in current group
77            vector<int> sortedItems = topoSort(itemGraph, itemInDegrees, entities);
78            if (sortedItems.size() != entities.size()) {
79                return {}; // unable to sort items within a group
80            }
81            // Add sorted items to result
82            result.insert(result.end(), sortedItems.begin(), sortedItems.end());
83        }
84        return result;
85    }
86};
87
1function sortItems(numItems: number, numGroups: number, groupAssignments: number[], beforeItems: number[][]): number[] {
2    // Initialize an index for new groups starting from the current group count.
3    let newIndex = numGroups;
4  
5    const groupToItems: number[][] = new Array(numItems + numGroups).fill(0).map(() => []);
6    const itemInDegree: number[] = new Array(numItems).fill(0);
7    const groupInDegree: number[] = new Array(numItems + numGroups).fill(0);
8    const itemGraph: number[][] = new Array(numItems).fill(0).map(() => []);
9    const groupGraph: number[][] = new Array(numItems + numGroups).fill(0).map(() => []);
10  
11    // Group assignment for items. If an item has no group, a new group is assigned.
12    for (let i = 0; i < numItems; ++i) {
13        if (groupAssignments[i] === -1) {
14            groupAssignments[i] = newIndex++;
15        }
16        groupToItems[groupAssignments[i]].push(i);
17    }
18  
19    // Build graphs for items and groups and calculate in-degrees.
20    for (let i = 0; i < numItems; ++i) {
21        for (const j of beforeItems[i]) {
22            if (groupAssignments[i] === groupAssignments[j]) {
23                ++itemInDegree[i];
24                itemGraph[j].push(i);
25            } else {
26                ++groupInDegree[groupAssignments[i]];
27                groupGraph[groupAssignments[j]].push(groupAssignments[i]);
28            }
29        }
30    }
31  
32    // Merge and list all items and groups.
33    let combinedItems = new Array(numItems + numGroups).fill(0).map((_, i) => i);
34  
35    // A generic topological sort function that can be used for both items and groups.
36    const topologicalSort = (graph: number[][], degree: number[], elements: number[]): number[] => {
37        const queue: number[] = [];
38        // Initialize queue with nodes having zero in-degree.
39        for (const element of elements) {
40            if (degree[element] === 0) {
41                queue.push(element);
42            }
43        }
44        const sortedOrder: number[] = [];
45        while (queue.length) {
46            const element = queue.pop()!;
47            sortedOrder.push(element);
48            for (const neighbor of graph[element]) {
49                if (--degree[neighbor] === 0) {
50                    queue.push(neighbor);
51                }
52            }
53        }
54        // If sortedOrder's length matches elements length, all nodes were processed.
55        return sortedOrder.length === elements.length ? sortedOrder : [];
56    };
57  
58    // Sort all groups.
59    const sortedGroups = topologicalSort(groupGraph, groupInDegree, combinedItems);
60    if (sortedGroups.length === 0) {
61        // If no valid group ordering is possible, return an empty array.
62        return [];
63    }
64  
65    const finalOrder: number[] = [];
66    for (const groupId of sortedGroups) {
67        combinedItems = groupToItems[groupId];
68        // Sort all items within the current group.
69        const sortedItems = topologicalSort(itemGraph, itemInDegree, combinedItems);
70        if (sortedItems.length !== combinedItems.length) {
71            // If no valid items ordering within the group is possible, return an empty array.
72            return [];
73        }
74        finalOrder.push(...sortedItems);
75    }
76  
77    // Return the final sorted order.
78    return finalOrder;
79}
80
Not Sure What to Study? Take the 2-min Quiz๏ผš

Which algorithm should you use to find a node that is close to the root of the tree?

Time and Space Complexity

Time Complexity

The algorithm involves several steps with different complexities. Below is the breakdown:

  1. Initialization of Data Structures: Initializing group_items, item_degree, group_degree, item_graph, and group_graph involves iterating over n and m, resulting in O(n + m) time.

  2. Setting up degrees and graphs:

    • For each item from 0 to n, the algorithm checks the groups of items in beforeItems list, which, in the worst case, may include all other items, leading to O(n^2) time.
    • This is done for items and groups, so the worst-case time complexity here is O(n^2 + m^2), considering groups could be as numerous as items.
  3. Topological Sort:

    • The function topo_sort performs a BFS-like algorithm which, in the worst case, processes each vertex and each edge once. For items, there are n vertices and, in the worst case, n*(n-1) edges for a dense graph. This amounts to O(n + n*(n-1)) which is O(n^2).
    • For groups, there are n + m vertices and, in the worst case, (n + m)*(n + m - 1) edges. The complexity for groups, therefore, is O((n + m) + (n + m)*(n + m - 1)) which simplifies to O(n^2 + m^2).

Combining these parts, the overall worst-case time complexity of the algorithm is O(n^2 + m^2).

Space Complexity

The space complexity is determined by the amount of memory allocated for variables and data structures in the algorithm, which relates to the following:

  1. Data Structures for Graphs and Degrees:

    • group_items: O(n + m)
    • item_degree, group_degree: O(n + m)
    • item_graph, group_graph: These are adjacency lists, which, in the worst case, can store n*(n-1) edges for items and (n + m)*(n + m - 1) edges for groups, resulting in O(n^2) and O((n + m)^2) space respectively.
  2. Queue in topo_sort: In the worst case, where all vertices are put into the queue before getting dequeued, we need O(n) space for items and O(n + m) space for groups.

  3. Res and ans Lists: These lists will contain exactly n and n + m elements respectively, providing a space complexity of O(n) and O(n + m).

Combining the space complexities for the data structures and the temporary lists, the overall worst-case space complexity of the algorithm is O(n^2 + m^2).

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings


Got a question?ย Ask the Teaching Assistantย anything you don't understand.

Still not clear? Ask in the Forum, ย Discordย orย Submitย the part you don't understand to our editors.

โ†
โ†‘TA ๐Ÿ‘จโ€๐Ÿซ