Evaluate Division
MedPrompt
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.0Example 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 <= 20equations[i].length == 21 <= Ai.length, Bi.length <= 5values.length == equations.length0.0 < values[i] <= 20.01 <= queries.length <= 20queries[i].length == 21 <= Cj.length, Dj.length <= 5Ai, Bi, Cj, Djconsist of lower case English letters and digits.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach treats the problem as an all-pairs shortest path problem on a graph, but with multiplication as the path combination operator instead of addition. The Floyd-Warshall algorithm is a dynamic programming technique that computes the values of all possible divisions between variables in a single preprocessing step. After this step, any query can be answered in constant time.
Algorithm
- Variable to Index Mapping: Create a mapping from each unique variable string to an integer index (from 0 to
V-1, whereVis the number of unique variables). - Matrix Initialization: Create a
V x Vmatrix, let's call itdist. Initializedist[i][j]to a sentinel value (e.g., -1.0) to represent that the value ofvar_i / var_jis unknown. Initialize the diagonaldist[i][i]to 1.0, asvar_i / var_i = 1. - Populate Direct Edges: Iterate through the input
equations. For each equationA / B = value, leti = index(A)andj = index(B). Setdist[i][j] = valueanddist[j][i] = 1.0 / value. - Floyd-Warshall Execution: Apply the Floyd-Warshall algorithm to fill in the rest of the matrix. The standard algorithm for shortest paths is
dist[i][j] = dist[i][k] + dist[k][j]. We adapt this for division:dist[i][j] = dist[i][k] * dist[k][j]. This is done using three nested loops:for k from 0 to V-1: for i from 0 to V-1: for j from 0 to V-1: if dist[i][k] != -1.0 and dist[k][j] != -1.0: dist[i][j] = dist[i][k] * dist[k][j] - Process Queries: For each query
C / D, find their indicesi = index(C)andj = index(D). The answer isdist[i][j]. If eitherCorDwas not in the original equations, or ifdist[i][j]is still the sentinel value, the answer is -1.0.
Walkthrough
First, we map every unique variable to an integer index to facilitate the use of a 2D array (adjacency matrix). This matrix, dist, will store the results of divisions. We initialize dist[i][i] to 1.0 and populate it with the direct division values from the input equations. For A / B = V, we set dist[idx(A)][idx(B)] = V and dist[idx(B)][idx(A)] = 1/V.
The core of the approach is the Floyd-Warshall algorithm. It systematically considers every variable k as an intermediate in the path between any two other variables i and j. If we know i / k and k / j, we can compute i / j as (i / k) * (k / j). By iterating through all possible i, j, and k, we can fill the entire dist matrix with all derivable division results.
Once the matrix is fully computed, answering a query C / D is a simple lookup in the matrix. If the entry dist[idx(C)][idx(D)] was computed, that's the answer; otherwise, no relationship can be established, and we return -1.0.
import java.util.*; class Solution { public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) { Map<String, Integer> varToIndex = new HashMap<>(); int varCount = 0; for (List<String> eq : equations) { if (!varToIndex.containsKey(eq.get(0))) { varToIndex.put(eq.get(0), varCount++); } if (!varToIndex.containsKey(eq.get(1))) { varToIndex.put(eq.get(1), varCount++); } } double[][] dist = new double[varCount][varCount]; for (int i = 0; i < varCount; i++) { Arrays.fill(dist[i], -1.0); dist[i][i] = 1.0; } for (int i = 0; i < equations.size(); i++) { int u = varToIndex.get(equations.get(i).get(0)); int v = varToIndex.get(equations.get(i).get(1)); dist[u][v] = values[i]; dist[v][u] = 1.0 / values[i]; } for (int k = 0; k < varCount; k++) { for (int i = 0; i < varCount; i++) { for (int j = 0; j < varCount; j++) { if (dist[i][k] != -1.0 && dist[k][j] != -1.0) { dist[i][j] = dist[i][k] * dist[k][j]; } } } } double[] results = new double[queries.size()]; for (int i = 0; i < queries.size(); i++) { List<String> query = queries.get(i); Integer start = varToIndex.get(query.get(0)); Integer end = varToIndex.get(query.get(1)); if (start == null || end == null) { results[i] = -1.0; } else { results[i] = dist[start][end]; } } return results; }}Complexity
Time
O(V^3 + M), where V is the number of unique variables and M is the number of queries. Since V can be at most 2N, the complexity is O(N^3 + M). The V^3 term for the Floyd-Warshall algorithm dominates.
Space
O(V^2) or O(N^2), where V is the number of unique variables and N is the number of equations. This is for storing the V x V distance matrix.
Trade-offs
Pros
Once the preprocessing is done, queries are answered in O(1) time.
The logic is a standard, well-known algorithm, making it relatively straightforward to implement if familiar with it.
Cons
The preprocessing step has a high time complexity of O(N^3), which is inefficient for larger numbers of variables.
It requires O(N^2) space for the adjacency matrix, which can be substantial.
This approach models the problem as a graph, where variables are nodes and an equation A / B = V represents a weighted directed edge from A to B with weight V (and an inverse edge from B to A with weight 1/V). To evaluate a query C / D, we simply need to find a path from node C to node D in the graph. The result of the division is the product of the weights of the edges along this path. A graph traversal algorithm like Depth-First Search (DFS) or Breadth-First Search (BFS) is perfect for this task.
Algorithm
- Graph Representation: Use a
HashMapto build an adjacency list representation of the graph. The map's key is a variable (String), and its value is another map where keys are adjacent variables and values are the division results (edge weights).Map<String, Map<String, Double>> graph - Graph Construction: Iterate through the
equationsandvalues. For each equationA / B = V, add a directed edge fromAtoBwith weightV, and an edge fromBtoAwith weight1/V. - Query Processing via DFS: For each query
C / D, perform a Depth-First Search (DFS) starting from nodeCto find a path to nodeD. - DFS Helper Function: The DFS function, say
dfs(current, target, product, visited), will need:current: The current node in the traversal.target: The destination node of the query.product: The accumulated product of weights along the path from the start node to thecurrentnode.visited: ASetto keep track of visited nodes in the current traversal to prevent infinite loops in case of cycles.
- DFS Logic:
- If
currentortargetis not in the graph, return -1.0. - If
currentequalstarget, a path is found; return the accumulatedproduct. - Mark
currentas visited. - For each
neighborofcurrentwith edge weightw:- Recursively call
dfs(neighbor, target, product * w, visited). - If the recursive call returns a value other than -1.0, it means a path was found, so return that value immediately.
- Recursively call
- If the loop finishes without finding the target, no path exists from
current. Return -1.0.
- If
Walkthrough
We begin by constructing a graph from the given equations. An adjacency list is a suitable representation, implemented using a map of maps (Map<String, Map<String, Double>>). This structure allows us to quickly access the neighbors of any variable and the corresponding division value.
For each query [C, D], we initiate a search (e.g., DFS) from the starting node C. The search explores the graph, keeping track of the cumulative product of edge weights from C to the current node. We also use a visited set for each query to avoid getting stuck in cycles and redundant computations. If the search successfully reaches the target node D, the final accumulated product is our answer. If C or D don't exist in our graph, or if the search completes without reaching D, it means a value cannot be determined, and we return -1.0.
import java.util.*; class Solution { public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) { Map<String, Map<String, Double>> graph = buildGraph(equations, values); double[] results = new double[queries.size()]; for (int i = 0; i < queries.size(); i++) { List<String> query = queries.get(i); String start = query.get(0); String end = query.get(1); results[i] = dfs(start, end, new HashSet<>(), graph); } return results; } private Map<String, Map<String, Double>> buildGraph(List<List<String>> equations, double[] values) { Map<String, Map<String, Double>> graph = new HashMap<>(); for (int i = 0; i < equations.size(); i++) { String u = equations.get(i).get(0); String v = equations.get(i).get(1); graph.computeIfAbsent(u, k -> new HashMap<>()).put(v, values[i]); graph.computeIfAbsent(v, k -> new HashMap<>()).put(u, 1.0 / values[i]); } return graph; } private double dfs(String start, String end, Set<String> visited, Map<String, Map<String, Double>> graph) { if (!graph.containsKey(start) || !graph.containsKey(end)) { return -1.0; } if (start.equals(end)) { return 1.0; } visited.add(start); Map<String, Double> neighbors = graph.get(start); for (Map.Entry<String, Double> neighbor : neighbors.entrySet()) { String nextNode = neighbor.getKey(); if (!visited.contains(nextNode)) { double result = dfs(nextNode, end, visited, graph); if (result != -1.0) { return neighbor.getValue() * result; } } } return -1.0; }}Complexity
Time
O(M * (V + E)), where M is the number of queries. For each query, we might traverse the entire component of the graph. Since V <= 2N and E = 2N, this simplifies to O(M * N).
Space
O(V + E) or O(N), where V is the number of variables and E is the number of equations. This space is used for the adjacency list and the recursion stack/visited set during DFS.
Trade-offs
Pros
More efficient in terms of time and space than the Floyd-Warshall approach for the given problem constraints.
Conceptually simple, relying on a standard graph traversal algorithm.
Handles disconnected components naturally.
Cons
Pathfinding is repeated for each query. If multiple queries share common subpaths, the work is re-done every time.
For a dense graph, its performance can degrade, although for the given constraints, it's efficient enough.
The most efficient approach uses a Disjoint Set Union (DSU) data structure, also known as Union-Find, augmented with weights. This method groups connected variables into sets. For each set, we maintain a relationship between every variable and a single representative 'root' variable. Specifically, for any variable X in a set with root R, we can find the value of X / R. A query C / D can then be answered by finding the roots for C and D. If they share the same root R, the answer is (C / R) / (D / R).
Algorithm
- Data Structures: Use two maps:
parentto store the parent of each variable in the disjoint set structure, andweightto store the value ofvariable / parent[variable]. - Initialization: Initially, each variable is its own parent (
parent[v] = v) with a weight of 1.0 (weight[v] = 1.0). findOperation (with Path Compression): This is the key operation.find(v)returns the root of the set containingvand the value ofv / root.- If
vis its own parent, it's the root. Return(v, 1.0). - Recursively call
findonv's parent to get the root of the set,root, and the valueparent[v] / root. - Path Compression: Set
v's parent directly toroot. - Weight Update: Update
weight[v]to be(v / old_parent) * (old_parent / root). This newweight[v]now representsv / root.
- If
unionOperation: To process an equationA / B = V:- Find the roots and weights for both A and B:
(rootA, weightA) = find(A)and(rootB, weightB) = find(B). - If the roots are different, merge the sets. For example, make
rootAa child ofrootB. - Set
parent[rootA] = rootB. - Calculate the weight for this new link:
weight[rootA] = (V * weightB) / weightA. This formula is derived fromA/B = Vby substitutingA = weightA * rootAandB = weightB * rootB.
- Find the roots and weights for both A and B:
- Main Logic:
- Build Phase: Iterate through all equations, calling
unionfor each one to build the disjoint sets. - Query Phase: For each query
C / D:- If
CorDare not in the data structure, the result is -1.0. - Call
find(C)andfind(D)to get their roots and weights relative to their roots. - If the roots are not the same, they are not connected; result is -1.0.
- If the roots are the same, the result is
weightC / weightD.
- If
- Build Phase: Iterate through all equations, calling
Walkthrough
We use two hashmaps: parent to track the parent of each variable and weight to store the value of variable / parent[variable]. The core of the algorithm lies in the find and union operations.
The find(i) operation not only finds the root of the set containing i but also performs path compression. As it traverses up to the root, it makes each node a direct child of the root. Crucially, it also updates the weight of each node in the path to represent its value divided by the new parent (the root). This ensures that after find(i), weight[i] equals i / root.
The union(i, j, value) operation merges the sets of i and j using the information i / j = value. It finds the roots and weights for both i and j and then links one root to the other, calculating the weight of the new link to maintain consistency.
After processing all equations with union, we can answer queries. For a query C / D, we call find on both. If they belong to different sets (different roots), their ratio is unknown. If they share a root, the answer is simply the ratio of their weights relative to the common root.
import java.util.*; class Solution { public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) { Map<String, String> parent = new HashMap<>(); Map<String, Double> weight = new HashMap<>(); // Initialize parent and weight for all variables for (List<String> eq : equations) { String u = eq.get(0); String v = eq.get(1); parent.put(u, u); parent.put(v, v); weight.put(u, 1.0); weight.put(v, 1.0); } // Process equations with union operation for (int i = 0; i < equations.size(); i++) { String u = equations.get(i).get(0); String v = equations.get(i).get(1); union(u, v, values[i], parent, weight); } // Process queries double[] results = new double[queries.size()]; for (int i = 0; i < queries.size(); i++) { String u = queries.get(i).get(0); String v = queries.get(i).get(1); if (!parent.containsKey(u) || !parent.containsKey(v)) { results[i] = -1.0; continue; } Pair<String, Double> rootU = find(u, parent, weight); Pair<String, Double> rootV = find(v, parent, weight); if (rootU.getKey().equals(rootV.getKey())) { results[i] = rootU.getValue() / rootV.getValue(); } else { results[i] = -1.0; } } return results; } // find operation with path compression and weight update private Pair<String, Double> find(String s, Map<String, String> parent, Map<String, Double> weight) { if (!s.equals(parent.get(s))) { Pair<String, Double> rootInfo = find(parent.get(s), parent, weight); parent.put(s, rootInfo.getKey()); weight.put(s, weight.get(s) * rootInfo.getValue()); } return new Pair<>(parent.get(s), weight.get(s)); } // union operation private void union(String u, String v, double value, Map<String, String> parent, Map<String, Double> weight) { Pair<String, Double> rootU = find(u, parent, weight); Pair<String, Double> rootV = find(v, parent, weight); if (!rootU.getKey().equals(rootV.getKey())) { parent.put(rootU.getKey(), rootV.getKey()); // rootU / rootV = (u / rootU)^-1 * (u/v) * (v/rootV) // weight[rootU] = value * weight[v] / weight[u] weight.put(rootU.getKey(), (value * rootV.getValue()) / rootU.getValue()); } }}// A simple Pair class would be needed for Java < 8 or if not using javafx.util.Pairclass Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; }}Complexity
Time
O((N + M) * α(V)), where N is the number of equations, M is the number of queries, V is the number of variables, and α is the very slow-growing inverse Ackermann function. The complexity is amortized and is practically linear.
Space
O(V) or O(N), where V is the number of unique variables. This space is for the `parent` and `weight` maps.
Trade-offs
Pros
Extremely fast, with a nearly linear time complexity, making it the most efficient solution.
Processes both the initial equations and the subsequent queries very quickly.
Space efficient, requiring only O(N) space.
Cons
The implementation is more complex than a standard DFS, particularly the logic for updating weights during path compression in the
findoperation.
Solutions
Solution
class Solution {private Map<String, String> p;private Map<String, Double> w;public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) { int n = equations.size(); p = new HashMap<>(); w = new HashMap<>(); for (List<String> e : equations) { p.put(e.get(0), e.get(0)); p.put(e.get(1), e.get(1)); w.put(e.get(0), 1.0); w.put(e.get(1), 1.0); } for (int i = 0; i < n; ++i) { List<String> e = equations.get(i); String a = e.get(0), b = e.get(1); String pa = find(a), pb = find(b); if (Objects.equals(pa, pb)) { continue; } p.put(pa, pb); w.put(pa, w.get(b) * values[i] / w.get(a)); } int m = queries.size(); double[] ans = new double[m]; for (int i = 0; i < m; ++i) { String c = queries.get(i).get(0), d = queries.get(i).get(1); ans[i] = !p.containsKey(c) || !p.containsKey(d) || !Objects.equals(find(c), find(d)) ? -1.0 : w.get(c) / w.get(d); } return ans; }private String find(String x) { if (!Objects.equals(p.get(x), x)) { String origin = p.get(x); p.put(x, find(p.get(x))); w.put(x, w.get(x) * w.get(origin)); } return p.get(x); }}class Solution {public: unordered_map<string, string> p; unordered_map<string, double> w; vector<double> calcEquation(vector<vector<string>> &equations, vector<double> &values, vector<vector<string>> &queries) { int n = equations.size(); for (auto e : equations) { p[e[0]] = e[0]; p[e[1]] = e[1]; w[e[0]] = 1.0; w[e[1]] = 1.0; } for (int i = 0; i < n; ++i) { vector<string> e = equations[i]; string a = e[0], b = e[1]; string pa = find(a), pb = find(b); if (pa == pb) continue; p[pa] = pb; w[pa] = w[b] * values[i] / w[a]; } int m = queries.size(); vector<double> ans(m); for (int i = 0; i < m; ++i) { string c = queries[i][0], d = queries[i][1]; ans[i] = p.find(c) == p.end() || p.find(d) == p.end() || find(c) != find(d) ? -1.0 : w[c] / w[d]; } return ans; } string find(string x) { if (p[x] != x) { string origin = p[x]; p[x] = find(p[x]); w[x] *= w[origin]; } return p[x]; }};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] return p[x] w = defaultdict(lambda: 1) p = defaultdict() for a, b in equations: p[a], p[b] = a, b 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] 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]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.