|
| 1 | +from collections import defaultdict |
| 2 | +from typing import List |
| 3 | + |
| 4 | + |
| 5 | +class OfficialSolution: |
| 6 | + """ |
| 7 | + == Overview == |
| 8 | + The problem can be solved with 2 important data structures, namely Graph and Union- |
| 9 | + Find. |
| 10 | + """ |
| 11 | + |
| 12 | + def calcEquation( |
| 13 | + self, equations: List[List[str]], values: List[float], queries: List[List[str]] |
| 14 | + ) -> List[float]: |
| 15 | + """ |
| 16 | + == Approach 1: Path Search in Graph == |
| 17 | + == Intuition == |
| 18 | + First, let us look at the example given in the problem description. Given two |
| 19 | + equations, namely a/b=2, b/c=3, we could derive the following equations: |
| 20 | + - 1) b/a = 1/2, c/b = 1/3 |
| 21 | + - 2) a/c = a/b . b/c = 6 |
| 22 | + Each division implies the reverse of the division, which is how we derive the |
| 23 | + equations in (1). While by chaining up equations, we could obtain new equations |
| 24 | + in (2). |
| 25 | +
|
| 26 | + We could reformulate the equations with the graph data structure, where each |
| 27 | + variable can be represented as a node in the graph, and the division |
| 28 | + relationship between variables can be modeled as edge with direction and weight. |
| 29 | +
|
| 30 | + The direction of edge indicates the order of division, and the weight of edge |
| 31 | + indicates the result of division. |
| 32 | +
|
| 33 | + With the above formulation, we then can convert the initial equations into the |
| 34 | + following graph: |
| 35 | +
|
| 36 | + a/b=2 b/c=3 |
| 37 | + a ------> b -------> c |
| 38 | + a <------- b <------ c |
| 39 | + b/a=1/2 c/b=1/3 |
| 40 | +
|
| 41 | + To evaluate the query (e.g. a/c = ?) is equivalent to performing two tasks on |
| 42 | + the graph: |
| 43 | + 1. Find if there exists a path between the two entities. |
| 44 | + 2. If so, calculate the cumulative products along the paths. |
| 45 | +
|
| 46 | + In the above example (a/c = ?), we could find a path between them, and the |
| 47 | + cumulative products are 6. As a result, we can conclude that the result of |
| 48 | + a/c is 2.3 = 6. |
| 49 | +
|
| 50 | + == Algorithm == |
| 51 | + As one can see, we just transform the problem into a path searching problem in |
| 52 | + a graph. |
| 53 | +
|
| 54 | + More precisely, we can reinterpret the problem as "given two nodes, we are asked |
| 55 | + to check if there exists a path between them. If so, we should return the |
| 56 | + cumulative products along the path as the result." |
| 57 | +
|
| 58 | + Given the above problem statement, it seems intuitive that one could apply the |
| 59 | + backtracking algorithm, or sometimes people might call it DFS (Depth-First |
| 60 | + Search). |
| 61 | +
|
| 62 | + Essentially, we can break down the algorithm into 2 steps overall: |
| 63 | + Step 1. We build the graph out of the list of input equations. |
| 64 | + - Each equation corresponds to two edges in the graph. |
| 65 | + Step 2. Once the graph is built, we then can evaluate the query one by one. |
| 66 | + - The evaluation of the query is done via searching the path between the |
| 67 | + given two variables. |
| 68 | + - Other than the above searching operation, we need to handle two |
| 69 | + exceptional cases as follows: |
| 70 | + - Case 1. If either of the nodes does not exist in the graph, i.e. the |
| 71 | + variables did not appear in any of the input equations, then we can |
| 72 | + assert that no path exists. |
| 73 | + - Case 2. If the origin and the destination are the same node, i.e. a/a, |
| 74 | + we can assume that therre exists an invisible self-loop path for each |
| 75 | + node and the result is 1. |
| 76 | +
|
| 77 | + Note: With the built graph, one could also apply the BFS (Breadth-First Search) |
| 78 | + algorithm, as opposed to the DFS algorithm we employed. |
| 79 | +
|
| 80 | + However, the essence of the solution remains the same, i.e. we are searching for |
| 81 | + a path in a graph. |
| 82 | +
|
| 83 | + == Complexity Analysis == |
| 84 | + Let N be the number of input equations and M be the number of queries. |
| 85 | + Time Complexity: |
| 86 | + O(MN) |
| 87 | + - First of all, we iterate through the equations to build a graph. Each |
| 88 | + equation takes O(1) time to process. Therefore, this step will take O(N) |
| 89 | + time in total. |
| 90 | + - For each query, we need to traverse the graph. In the worst case, we might |
| 91 | + need to traverse the entire graph, which could take O(N). Hence, in total, |
| 92 | + the evaluation of queries could take M*O(N) = O(MN). |
| 93 | + - To sum up, the overall time complexity of the algorithm is |
| 94 | + O(N) + O(MN) = O(MN). |
| 95 | +
|
| 96 | + Space Complexity: |
| 97 | + O(N) |
| 98 | + - We build a graph out of the equations. In the worst case where there is no |
| 99 | + overlapping among the equations, we would have N edges and 2N nodes in the |
| 100 | + graph. Therefore, the space complexity of the graph is O(N+2N)=O(3N)=O(N). |
| 101 | + - Since we employ the recursion in the backtracking, we would consume |
| 102 | + additional memory in the function call stack, which could amount to O(N) |
| 103 | + space. |
| 104 | + - In addition, we used a set |
| 105 | + """ |
| 106 | + |
| 107 | + # Graph as Adjacency Lists |
| 108 | + graph = defaultdict(defaultdict) |
| 109 | + |
| 110 | + for (numerator, denominator), value in zip(equations, values): |
| 111 | + graph[numerator][denominator] = value |
| 112 | + graph[denominator][numerator] = 1 / value |
| 113 | + |
| 114 | + def dfs(curr: str, end: str, visited=None, cum_weight: float = 1.0) -> float: |
| 115 | + if visited is None: |
| 116 | + visited = set() |
| 117 | + |
| 118 | + if curr == end: |
| 119 | + return cum_weight |
| 120 | + |
| 121 | + visited.add(curr) |
| 122 | + |
| 123 | + for node, value in graph[curr].items(): |
| 124 | + if node in visited: |
| 125 | + continue |
| 126 | + |
| 127 | + ans = dfs( |
| 128 | + curr=node, end=end, visited=visited, cum_weight=cum_weight * value |
| 129 | + ) |
| 130 | + |
| 131 | + if ans != -1.0: |
| 132 | + return ans |
| 133 | + |
| 134 | + visited.remove(curr) |
| 135 | + |
| 136 | + return -1.0 |
| 137 | + |
| 138 | + ans = [] |
| 139 | + for (start, end) in queries: |
| 140 | + if start not in graph: |
| 141 | + ans.append(-1.0) |
| 142 | + else: |
| 143 | + ans.append(dfs(curr=start, end=end)) |
| 144 | + |
| 145 | + return ans |
0 commit comments