{x}
blog image

Evaluate Division

You are given an array of variable pairs equations and an array of real numbers values, where equations[i] = [Ai, Bi] and values[i] represent the equation Ai / Bi = values[i]. Each Ai or Bi is a string that represents a single variable.

You are also given some queries, where queries[j] = [Cj, Dj] represents the jth query where you must find the answer for Cj / Dj = ?.

Return the answers to all queries. If a single answer cannot be determined, return -1.0.

Note: The input is always valid. You may assume that evaluating the queries will not result in division by zero and that there is no contradiction.

Note: The variables that do not occur in the list of equations are undefined, so the answer cannot be determined for them.

 

Example 1:

Input: equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]
Output: [6.00000,0.50000,-1.00000,1.00000,-1.00000]
Explanation: 
Given: a / b = 2.0, b / c = 3.0
queries are: a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ? 
return: [6.0, 0.5, -1.0, 1.0, -1.0 ]
note: x is undefined => -1.0

Example 2:

Input: equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]]
Output: [3.75000,0.40000,5.00000,0.20000]

Example 3:

Input: equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"],["a","c"],["x","y"]]
Output: [0.50000,2.00000,-1.00000,-1.00000]

 

Constraints:

  • 1 <= equations.length <= 20
  • equations[i].length == 2
  • 1 <= Ai.length, Bi.length <= 5
  • values.length == equations.length
  • 0.0 < values[i] <= 20.0
  • 1 <= queries.length <= 20
  • queries[i].length == 2
  • 1 <= Cj.length, Dj.length <= 5
  • Ai, Bi, Cj, Dj consist of lower case English letters and digits.

Solution Explanation

This problem involves finding the values of expressions of the form Cj / Dj, given a set of equations Ai / Bi = values[i]. The most efficient approach is to use a variation of Union-Find with weighted edges.

Algorithm:

  1. Graph Representation: We can represent the equations as a directed graph where nodes are variables (strings), and edges represent the ratios between them. The weight of an edge from A to B is the value of A/B.

  2. Union-Find with Weights: We utilize a Union-Find data structure to efficiently merge connected components. However, instead of simply merging components, we track the weight of the path from a node to its root in the Union-Find tree. This weight represents the accumulated ratio from that node to the root of its component.

  3. Find Operation: The find operation not only finds the root of a node but also calculates the cumulative weight of the path from the node to the root. This path weight is crucial for computing the ratio between two nodes.

  4. Union Operation: When merging two components, we need to update the weights of the nodes in the merged component to reflect the new ratios. This involves careful calculation to maintain consistency in the path weights.

  5. Querying: For each query (Cj, Dj), we perform the find operation for both nodes. If they belong to different components (i.e., no path exists between them), the answer is -1.0. Otherwise, we can compute Cj / Dj by dividing the path weights.

Time Complexity Analysis:

  • Initialization: The initialization of the Union-Find structure takes O(N) time, where N is the number of unique variables.
  • Union and Find: Both union and find operations have amortized time complexity of O(α(N)), where α(N) is the inverse Ackermann function, which is effectively a constant for all practical purposes.
  • Queries: Processing each query involves two find operations, giving a time complexity of O(α(N)) per query. Since there are M queries, the total time complexity for query processing is O(Mα(N)).

Therefore, the overall time complexity of the algorithm is dominated by the number of queries and is approximately O(N + M), which is linear in the number of variables and queries.

Space Complexity Analysis:

The space complexity is O(N) to store the Union-Find data structure (parent pointers and weights), and O(M) in the worst case to store the results of the queries, hence making the overall space complexity O(N + M).

Code Explanation (Python):

The Python code implements the algorithm described above. Here's a breakdown:

class Solution:
    def calcEquation(
        self, equations: List[List[str]], values: List[float], queries: List[List[str]]
    ) -> List[float]:
        def find(x):
            if p[x] != x:
                origin = p[x]
                p[x] = find(p[x])
                w[x] *= w[origin]  # Update path weight
            return p[x]
 
        w = defaultdict(lambda: 1) # Path weights from node to root
        p = defaultdict(str) # Parent pointers for union-find
        for a, b in equations:
            p[a], p[b] = a, b  # Initialize parent pointers for all variables
 
        # Process equations using union-find with path weight tracking
        for i, v in enumerate(values):
            a, b = equations[i]
            pa, pb = find(a), find(b)
            if pa == pb:
                continue
            p[pa] = pb
            w[pa] = w[b] * v / w[a] # Update weights after merging
 
        return [
            -1 if c not in p or d not in p or find(c) != find(d) else w[c] / w[d]
            for c, d in queries
        ]

The code uses defaultdict for efficient handling of nodes that might not exist initially. The find function recursively finds the root and updates the path weight, ensuring efficient ratio calculation. The list comprehension concisely calculates the result for each query. Other language implementations follow a similar approach with appropriate data structure choices for that specific language.