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.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:
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.
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.
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.
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.
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:
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.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).
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.