Skill Set - Topological Sort (Graph)


Skills for tackling LeetCode problems - Topological Sort (Graph).

Topological Sort is used to find a linear ordering of elements that have dependencies on each other. Source is any node that has no incoming edge and has only outgoing edges. Sink is any node that has only incoming edges and no outgoing edge. A topological ordering starts with one of the sources and ends at one of the sinks, and it’s only possible when the graph has no directed cycles, i.e. if the graph is a Directed Acyclic Graph (DAG).

1. Topological Sort

Topological Sort of a directed graph (a graph with undirectional edges) is a linear ordering of its vertices such that for every directed edge (U, V) from vertex U to vertex V, U comes before V in the ordering. Given a directed graph, find the topological ordering of its vertices.

We can traverse the graph in a BFS way. We use a HashMap to count the in-degrees of each vertex, any vertex with 0 in-degree will be a source. Then we store the source into a Queue. For each source, we do 1) add it to the sorted list, 2) get all its children from the graph, 3) decrease the in degree of each child by 1, 4) if a child’s in-degree becomes 0, add it to the sources Queue, until the source Queue is empty.

# Time complexity O(V + E). Since each vertex will be a source only once and each edge will be accessed and removed once.
# Space complexity O(V + E). Since we store all edges for each vertex in an adjacency list.

from collections import deque

def topological_sort(vertices, edges):
    sortedList = []
    if vertices <= 0: 
        return sortedList
    
    inDegree = {i: 0 for i in range(vertices)} # count in-degrees of all vertices
    graph = {i: [] for i in range(vertices)} # store all children of each vertex - "adjacency list"
    
    for edge in edges: 
        parent, child = edge[0], edge[1]
        graph[parent].append(child)
        inDegree[child] += 1 # number of children = number directed edges
        
    sources = deque()
    for key in inDegree:
        if inDegree[key] == 0: # find the source
            sources.append(key)
    
    while sources:
        vertex = sources.popleft()
        sortedList.append(vertex)
        for child in graph[vertex]:
            inDegree[child] -= 1 # pop out the source, delete all edges connect to it
            if inDegree[child] == 0: # any child becomes a source
                sources.append(child)
    
    # topological sort is not possible if the graph ahs a cycle
    if len(sortedList) != vertices:
        return []
    
    return sortedList

2. Tasks Scheduling

There are N tasks, labeled from 0 to N-1. Each task can have some prerequisite tasks which need to be completed before it can be scheduled. Given the number of tasks and a list of prerequisite pair, find out if it is possible to schedule all the tasks.

# Time complexity O(V + E). Space complexity O(V + E).

def is_scheduling_possible(tasks, prerequisites):
    sortedList = []
    if tasks <= 0:
        return False
    
    inDegree = {i: 0 for i in range(tasks)} # count incoming edges for each vertex
    graph = {i: [] for i in range(tasks)} # adjacency list
    
    for pre in prerequisites:
        parent, child = pre[0], pre[1]
        graph[parent].append(child) # construct the graph
        inDegree[child] += 1 # count edges for each child, sources have 0 count
        
    sources = deque() 
    for key in inDegree:
        if inDegree[key] == 0: # find sources
            sources.append(key)
            
    while sources:
        vertex = sources.popleft()
        sortedList.append(vertex)
        for child in graph[vertex]:
            inDegree[child] -= 1 # remove the source, then any edges connect to the source are removed
            if inDegree[child] == 0: # find the next source
                sources.append(child)
    
    # if false, there is a cyclic dependency between tasks
    return len(sortedList) == tasks

3. Tasks Scheduling Order

There are N tasks, labeled from 0 to N-1. Each task can have some prerequisite tasks which need to be completed before it can be scheduled. Given the number of tasks and a list of prerequisite pairs, write a method to find the ordering of tasks we should pick to finish all tasks.

# Time complexity O(V + E). Space complexity O(V + E).

def find_order(tasks, prerequisites):
    sortedList = []
    if tasks <= 0:
        return False
    inDegree = {i: 0 for i in range(tasks)}
    graph = {i: [] for i in range(tasks)}
    
    for pre in prerequisites:
        parent, child = pre[0], pre[1]
        graph[parent].append(child)
        inDegree[child] += 1
        
    sources = deque()
    for key in inDegree:
        if inDegree[key] == 0:
            sources.append(key)
            
    while sources:
        vertex = sources.popleft()
        sortedList.append(vertex)
        for child in graph[vertex]:
            inDegree[child] -= 1
            if inDegree[child] == 0:
                sources.append(child)
                
    if len(sortedList) != tasks:
        return []
    return sortedList

4. All Tasks Scheduling Orders

There are N tasks, labeled from 0 to N-1. Each task can have some prerequisite tasks which need to be completed before it can be scheduled. Given the number of tasks and a list of prerequisite pairs, write a method to print all possible ordering of tasks meeting all prerequisites.

At any stage, if we have more than one source available and since we can choose any source, therefore, in this case, we will have multiple orderings of the tasks. We can use a recursive approach with backtracking to consider all sources ay any step.

# Time complexity O(V! * E). Space complexity O(V! * E). 
# V is the total number of tasks and E is the total prerequisites. There can be V! combination for N numbers, and removing all edges in recursive call takes O(E).

def print_orders(tasks, prerequisites):
    sortedList = []
    if tasks <= 0:
        return False
    
    inDegree = {i: 0 for i in range(tasks)}
    graph = {i: [] for i in range(tasks)}
    
    for pre in prerequisites:
        parent, child = pre[0], pre[1]
        graph[parent].append(child)
        inDegree[child] += 1
        
    sources = deque()
    for key in inDegree:
        if inDegree[key] == 0: # find the source
            sources.append(key)
    recursion(graph, inDegree, sources, sortedList)
    
def recursion(graph, inDegree, sources, sortedList):
    if sources:
        for vertex in sources:
            ## condition1: append current vertex into the sortedList
            sortedList.append(vertex)
            
            copy = deque(sources) # make a copy of source
            copy.remove(vertex)
            for child in graph[vertex]:
                inDegree[child] -= 1
                if inDegree[child] == 0:
                    copy.append(child)
            
            # recursive call to print other ordering from thet remaining sources 
            recursion(graph, inDegree, copy, sortedList)
            
            ## condition2: backtrack
            sortedList.remove(vertex) # do not remove
            for child in graph[vertex]:
                inDegree[child] += 1 # get teh in-degrees back
                
    if len(sortedList) == len(inDegree):
        print(sortedList)

  TOC