Graph problems summary

In Leetcode graph problems, it usually uses BFS or DFS to retrieval the data. It depends on what problem ask and the understanding of the algorithm, I put notes here to help myself review in the future.


[ basically, steps are ]

1. determine whether the problem is directional or unidirectional graph?
2. use dictionary or different way to translate the problem into the graph
3. analysis the problem and choose the algorithm
4. design, run algorithm(BFS or DFS)
5. return the answer

[ why those steps ]

for 1 & 2 it's important to understand the relationship between vertex( or node) and edge (sometimes I like to call it neighbors). Without step1, I'll not able to build up properly graph to represent the problem.

for 3,4,5, decompose the problem and solve with most efficient way consider as engineering virtue.
Problems.

 [ Problem 133 Clone Graph ]

Clone an undirected graph. Each node in the graph contains a label and a list of its neighbors.

OJ's undirected graph serialization:
Nodes are labeled uniquely.
We use # as a separator for each node, and , as a separator for node label and each neighbor of the node.
As an example, consider the serialized graph {0,1,2#1,2#2,2}.
The graph has a total of three nodes, and therefore contains three parts as separated by #.
  1. First node is labeled as 0. Connect node 0 to both nodes 1 and 2.
  2. Second node is labeled as 1. Connect node 1 to node 2.
  3. Third node is labeled as 2. Connect node 2 to node 2 (itself), thus forming a self-cycle.
Visually, the graph looks like the following:
       1
      / \
     /   \
    0 --- 2
         / \
         \_/
1. it clearly said the problem are unidirectional graph which means [0, 1] and [1, 0] are the same.
2. here I don't need to build up my own graph since problem give it as a directory
3. now, the problem itself are simple, just clone the graph so I prefer DFS cause it will use less memory if a node has many neighbors.
4&5, python implement,

4.1 check corner case, what if there's no graph for me to copy?
4.2 run DFS, each dfs function check if node has been copy? if not, copy it and then handle the neighbors.

5. return the DFS result and that will be a new node.

Time complexity:  O(V+E).  Each node and neighbor will be accessed at most once.
Space complexity: O(V+E). 

# Definition for a undirected graph node
# class UndirectedGraphNode:
#     def __init__(self, x):
#         self.label = x
#         self.neighbors = []

class Solution:
    # @param node, a undirected graph node
    # @return a undirected graph node
    def cloneGraph(self, node):
        def dfs(node, mapp):
            if node in mapp: return mapp[node]
            
            cur = UndirectedGraphNode(node.label)
            mapp[node] = cur
            for i in node.neighbors:
                mapp[node].neighbors.append(dfs(i, mapp))
            return cur
        
        if not node: return 
        return dfs(node, {})

 [ 207. Course Schedule ]

There are a total of n courses you have to take, labeled from 0 to n - 1.
Some courses may have prerequisites, for example to take course 0 you have to first take course 1, which is expressed as a pair: [0,1]
Given the total number of courses and a list of prerequisite pairs, is it possible for you to finish all courses?
For example:
2, [[1,0]]
There are a total of 2 courses to take. To take course 1 you should have finished course 0. So it is possible.
2, [[1,0],[0,1]]
There are a total of 2 courses to take. To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible.
Note:
  1. The input prerequisites is a graph represented by a list of edges, not adjacency matrices. Read more about how a graph is represented.
  2. You may assume that there are no duplicate edges in the input prerequisites.

1. To check if it's possible, we needs to do topology sort. If we can sort it, that means it's possible. This issue is directional graph.
2. use two dictionary two build up the graph
3. now, let's use Kahn's algorithm implement by BFS.
4&5, python implement,

4.1 find the node without in-coming edge
4.2 keep pop the node and it's connect edge

5. return True if count equal to course number else False

Time complexity:  O(V+E).  Each node and neighbor will be accessed at most once.
Space complexity: O(V+E). 

import collections
class Solution(object):
    def canFinish(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: bool
        """
        # build graph
        graph = collections.defaultdict(set)
        edge  = collections.defaultdict(set)
        for course, pre in prerequisites:
            graph[course].add(pre)
            edge[pre].add(course)
        
        # find the node without in-coming edge
        queue = [n for n in range(numCourses) if n not in graph]
        
        # do BFS
        count = 0

        while queue:
            node = queue.pop()
            res.append(node)
            count += 1
            for i in edge[node]:
                graph[i].remove(node)
                if not graph[i]:
                    queue.append(i)

        # return count == numCourses
        return count == numCourses

[ 210. Course Schedule II ]

There may be multiple correct orders, you just need to return one of them.

Time complexity:  O(V+E).  Each node and neighbor will be accessed at most once.
Space complexity: O(V+E). 


        # do BFS
        count = 0
        res = []
        while queue:
            node = queue.pop()
            res.append(node)
            count += 1
            for i in edge[node]:
                graph[i].remove(node)
                if not graph[i]:
                    queue.append(i)
        
        # return count == numCourses
        if count == numCourses:
            return res
        return []

[ 261. Graph Valid Tree ]

Given n nodes labeled from 0 to n - 1 and a list of undirected edges (each edge is a pair of nodes), write a function to check whether these edges make up a valid tree.
For example:
Given n = 5 and edges = [[0, 1], [0, 2], [0, 3], [1, 4]], return true.
Given n = 5 and edges = [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]], return false.
Note: you can assume that no duplicate edges will appear in edges. Since all edges are undirected, [0, 1] is the same as [1, 0]and thus will not appear together in edges.

1. This issue is unidirectional graph since we been ask about verify a tree
2. note here we use only one dict to describe graph
3. we can use both DFS and BFS to check, as long as no loop in the tree, it's valid.
4. here tmp represent to the edges, I choose BFS here
5. If we finish the BFS and graph is empty, that means no loop in the tree.

Time complexity:  O(V+E).  Each node and neighbor will be accessed at most once.
Space complexity: O(V+E). 

   class Solution(object):
    def validTree(self, n, edges):
        """
        :type n: int
        :type edges: List[List[int]]
        :rtype: bool
        """
        # check corner case
        if len(edges) != n - 1: return False
        
        # build graph
        nbr = {i: [] for i in range(n)}
        for v, w in edges:
            nbr[v].append(w)
            nbr[w].append(v)      
        
        # do BFS
        queue = [0]
        while queue:
            cur = queue.pop(0)
            tmp = nbr.pop(cur, [])
            queue.extend(tmp)

        # return not nbr
        return not nbr

[ 323. Number of Connected Components in an Undirected Graph ]

Given n nodes labeled from 0 to n - 1 and a list of undirected edges (each edge is a pair of nodes), write a function to find the number of connected components in an undirected graph.
Example 1:
     0          3
     |          |
     1 --- 2    4
Given n = 5 and edges = [[0, 1], [1, 2], [3, 4]], return 2.
Example 2:
     0           4
     |           |
     1 --- 2 --- 3
Given n = 5 and edges = [[0, 1], [1, 2], [2, 3], [3, 4]], return 1.
Note:
You can assume that no duplicate edges will appear in edges. Since all edges are undirected, [0, 1] is the same as [1, 0] and thus will not appear together in edges.

1. This issue is unidirectional graph since question said so
2. note here we use only one dict to describe graph
3. we can use both DFS and BFS to check, just count how many times algorithm has been call.
4. here I choose BFS(like flooding), use double for loop to check each as root with BFS. we keep delete those node we seen
5.just return count number

Time complexity:  O(V+E).  Each node and neighbor will be accessed at most once.
Space complexity: O(V+E). 

class Solution(object):
 def countComponents(self, n, edges):
  """
        :type n: int
        :type edges: List[List[int]]
        :rtype: int
        """
  if not n or n < 1: return 0
  if not edges: return n
  # build graph

  graph= { i : [] for i in range(n)}

  for node, neighbor in edges:
   graph[node].append(neighbor)
   graph[neighbor].append(node)
  # do bfs
  ret = 0
  for i in xrange(n):
   queue = [i]
   ret += 1 if i in graph else 0
   for j in queue:
    if j in graph:
     queue += graph[j]
     del graph[j]

  # return result
  return ret  

[ 269. Alien Dictionary ]

There is a new alien language which uses the latin alphabet. However, the order among letters are unknown to you. You receive a list of non-empty words from the dictionary, where words are sorted lexicographically by the rules of this new language. Derive the order of letters in this language.
Example 1:
Given the following words in dictionary,
[
  "wrt",
  "wrf",
  "er",
  "ett",
  "rftt"
]
The correct order is: "wertf".
Example 2:
Given the following words in dictionary,
[
  "z",
  "x"
]
The correct order is: "zx".
Example 3:
Given the following words in dictionary,
[
  "z",
  "x",
  "z"
]
The order is invalid, so return "".
Note:
  1. You may assume all letters are in lowercase.
  2. You may assume that if a is a prefix of b, then a must appear before b in the given dictionary.
  3. If the order is invalid, return an empty string.
  4. There may be multiple valid order of letters, return any one of them is fine.
reference
 1. This issue is directional graph, analysis the rules from examples and then translate it.
2. translate base on two rule. First two mins
3. we can use both DFS and BFS to check, as long as no loop in the tree, it's valid.
4. Kahn's algorithm, remember to remove the edge if it's zero in-coming degree
5. If we finish the Kahn's algorithm and edge set is empty, that means no loop in the tree.

Time complexity:  O(V+E).  Each node and neighbor will be accessed at most once.
Space complexity: O(V+E). 

class Solution(object):
    def alienOrder(self, words):
        """
        :type words: List[str]
        :rtype: str
        """
        # build graph
        node = collections.defaultdict(set)
        edge = collections.defaultdict(set)

        for i in range(len(words) - 1):
            minlen = min(len(words[i]), len(words[i+1]))
            for j in range(minlen):
                if words[i][0] != words[i+1][0]:
                    node[words[i][j]].add(words[i+1][j])
                    edge[words[i+1][j]].add(words[i][j])
                    break
                elif words[i][j] != words[i+1][j]:
                    node[words[i][j]].add(words[i + 1][j])
                    edge[words[i + 1][j]].add(words[i][j])

        # find the node with 0 in-coming edge
        charset = set(''.join(words))
        queue = [ x for x in charset if x not in edge]

        # do kahn's algorithm
        res = []
        while queue:
            temp = queue.pop(0)
            res.append(temp)
            for i in node[temp]:
                edge[i].remove(temp)
                if not edge[i]:
                    queue.append(i)
                    del edge[i]

        # return
        return "".join(res) if len(edge) == 0 else ""

To be continued...

留言