|
| 1 | +# 17.7 Evaluate Division |
| 2 | + |
| 3 | +> **Source:** [`src/main/kotlin/graph/EvalualteDivisions.kt`](https://github.com/arpanpathak/AdvancedAlgorithmPatterns/blob/main/src/main/kotlin/graph/EvalualteDivisions.kt) |
| 4 | +> **Pattern:** edge-labeled graph BFS · **Core page** |
| 5 | +
|
| 6 | +## The Problem |
| 7 | + |
| 8 | +Given `equations` like `["a","b"]` with `values` like `2.0` (meaning `a / b = 2.0`), answer `queries` of the form `["x","y"]` with `x / y`, or `-1.0` if undeterminable. |
| 9 | + |
| 10 | +- Constraints: small graphs; values positive; answers fit in `Double`. |
| 11 | + |
| 12 | +## Examples |
| 13 | + |
| 14 | +``` |
| 15 | +equations = [["a","b"],["b","c"]], values = [2.0, 3.0] |
| 16 | +queries: a/c = 6.0, b/a = 0.5, a/e = -1.0, a/a = 1.0 |
| 17 | +``` |
| 18 | + |
| 19 | +## Intuition — `a / b = 2.0` is an edge with a *multiplier* label |
| 20 | + |
| 21 | +The equation `a / b = v` defines a directed edge `a -> b` with weight `v` **and** the reciprocal edge `b -> a` with weight `1/v`. Then a query `x / y` is: walk from `x` to `y` in this graph, **multiplying edge weights** along the way — the product is the ratio (cancellation telescopes along the path: `a/b · b/c = a/c`). |
| 22 | + |
| 23 | +``` |
| 24 | +graph[a][b] = v; graph[b][a] = 1/v |
| 25 | +
|
| 26 | +bfs(start, target): |
| 27 | + if either not in the graph: -1.0 |
| 28 | + if start == target: 1.0 |
| 29 | + queue of (node, product); visited set |
| 30 | + for (next, weight) in graph[node]: queue.add((next, product * weight)) |
| 31 | + return the product when target is popped, else -1.0 |
| 32 | +``` |
| 33 | + |
| 34 | +**Why does the product work?** Any path `a -> x1 -> x2 -> y` multiplies to `a/x1 · x1/x2 · x2/y = a/y` — the intermediate variables cancel. The graph encodes a *consistent system* (given), so every path between two nodes yields the same product. |
| 35 | + |
| 36 | +**Why BFS?** Shortest path in hops; the multiplier accumulates in the state `(node, product)` — the same "state carries more than the node id" move as [17.6](reorder-routes-to-make-all-paths-lead-to-city-zero.md) and the [17.0](pattern-primer.md) state-space pattern. (Union-Find with weights is the alternative — same math, different structure.) |
| 37 | + |
| 38 | +**The special cases:** `start == target` → 1.0 (anything divided by itself); `start` or `target` unknown → -1.0; no path → -1.0. The repo handles all three explicitly. |
| 39 | + |
| 40 | +## Approach 1 — Floyd-Warshall over the ratio graph (O(n^3)) |
| 41 | + |
| 42 | +Precompute all-pairs ratios: fine for tiny graphs, overkill for per-query BFS. |
| 43 | + |
| 44 | +## Approach 2 — Product-accumulating BFS (the repo's version, optimal) |
| 45 | + |
| 46 | +```kotlin |
| 47 | +class EvaluateDivisions { |
| 48 | + private data class NodeState(val id: String, val product: Double) |
| 49 | + |
| 50 | + /** |
| 51 | + * @param equations pairs defining ratios |
| 52 | + * @param values a / b = values[i] |
| 53 | + * @param queries x / y to evaluate |
| 54 | + * @return answers, -1.0 if undeterminable |
| 55 | + */ |
| 56 | + fun calcEquation(equations: List<List<String>>, values: DoubleArray, |
| 57 | + queries: List<List<String>>): DoubleArray { |
| 58 | + // Build graph: a -> {b: value}, b -> {a: 1/value} |
| 59 | + val graph = mutableMapOf<String, MutableMap<String, Double>>() |
| 60 | + equations.forEachIndexed { i, (u, v) -> |
| 61 | + graph.getOrPut(u) { mutableMapOf() }[v] = values[i] |
| 62 | + graph.getOrPut(v) { mutableMapOf() }[u] = 1.0 / values[i] |
| 63 | + } |
| 64 | + |
| 65 | + fun bfs(start: String, target: String): Double { |
| 66 | + if (start !in graph || target !in graph) return -1.0 |
| 67 | + if (start == target) return 1.0 |
| 68 | + |
| 69 | + val queue = ArrayDeque<NodeState>().apply { add(NodeState(start, 1.0)) } |
| 70 | + val visited = mutableSetOf(start) |
| 71 | + |
| 72 | + while (queue.isNotEmpty()) { |
| 73 | + val (curr, ratio) = queue.removeFirst() |
| 74 | + if (curr == target) return ratio |
| 75 | + |
| 76 | + graph[curr]?.forEach { (next, weight) -> |
| 77 | + if (visited.add(next)) { |
| 78 | + queue.add(NodeState(next, ratio * weight)) |
| 79 | + } |
| 80 | + } |
| 81 | + } |
| 82 | + return -1.0 |
| 83 | + } |
| 84 | + |
| 85 | + return DoubleArray(queries.size) { i -> bfs(queries[i][0], queries[i][1]) } |
| 86 | + } |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +```java |
| 91 | +import java.util.*; |
| 92 | + |
| 93 | +public class EvaluateDivision { |
| 94 | + /** |
| 95 | + * @param equations pairs defining ratios |
| 96 | + * @param values a / b = values[i] |
| 97 | + * @param queries x / y to evaluate |
| 98 | + * @return answers, -1.0 if undeterminable |
| 99 | + */ |
| 100 | + public double[] calcEquation(List<List<String>> equations, double[] values, |
| 101 | + List<List<String>> queries) { |
| 102 | + Map<String, Map<String, Double>> graph = new HashMap<>(); |
| 103 | + for (int i = 0; i < equations.size(); i++) { |
| 104 | + String u = equations.get(i).get(0), v = equations.get(i).get(1); |
| 105 | + graph.computeIfAbsent(u, k -> new HashMap<>()).put(v, values[i]); |
| 106 | + graph.computeIfAbsent(v, k -> new HashMap<>()).put(u, 1.0 / values[i]); |
| 107 | + } |
| 108 | + |
| 109 | + double[] result = new double[queries.size()]; |
| 110 | + for (int i = 0; i < queries.size(); i++) { |
| 111 | + result[i] = bfs(graph, queries.get(i).get(0), queries.get(i).get(1)); |
| 112 | + } |
| 113 | + return result; |
| 114 | + } |
| 115 | + |
| 116 | + private double bfs(Map<String, Map<String, Double>> graph, String start, String target) { |
| 117 | + if (!graph.containsKey(start) || !graph.containsKey(target)) return -1.0; |
| 118 | + if (start.equals(target)) return 1.0; |
| 119 | + |
| 120 | + Deque<Object[]> queue = new ArrayDeque<>(); // {node, product} |
| 121 | + queue.add(new Object[]{start, 1.0}); |
| 122 | + Set<String> visited = new HashSet<>(); |
| 123 | + visited.add(start); |
| 124 | + |
| 125 | + while (!queue.isEmpty()) { |
| 126 | + Object[] state = queue.poll(); |
| 127 | + String node = (String) state[0]; |
| 128 | + double product = (double) state[1]; |
| 129 | + if (node.equals(target)) return product; |
| 130 | + |
| 131 | + for (Map.Entry<String, Double> e : graph.get(node).entrySet()) { |
| 132 | + if (visited.add(e.getKey())) { |
| 133 | + queue.add(new Object[]{e.getKey(), product * e.getValue()}); |
| 134 | + } |
| 135 | + } |
| 136 | + } |
| 137 | + return -1.0; |
| 138 | + } |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +```cpp |
| 143 | +#include <queue> |
| 144 | +#include <string> |
| 145 | +#include <unordered_map> |
| 146 | +#include <unordered_set> |
| 147 | +#include <vector> |
| 148 | + |
| 149 | +class EvaluateDivision { |
| 150 | +public: |
| 151 | + /** |
| 152 | + * @param equations pairs defining ratios |
| 153 | + * @param values a / b = values[i] |
| 154 | + * @param queries x / y to evaluate |
| 155 | + * @return answers, -1.0 if undeterminable |
| 156 | + */ |
| 157 | + std::vector<double> calcEquation(std::vector<std::vector<std::string>>& equations, |
| 158 | + std::vector<double>& values, |
| 159 | + std::vector<std::vector<std::string>>& queries) { |
| 160 | + std::unordered_map<std::string, std::unordered_map<std::string, double>> graph; |
| 161 | + for (int i = 0; i < (int)equations.size(); i++) { |
| 162 | + auto& u = equations[i][0], & v = equations[i][1]; |
| 163 | + graph[u][v] = values[i]; |
| 164 | + graph[v][u] = 1.0 / values[i]; |
| 165 | + } |
| 166 | + |
| 167 | + std::vector<double> result; |
| 168 | + for (auto& q : queries) result.push_back(bfs(graph, q[0], q[1])); |
| 169 | + return result; |
| 170 | + } |
| 171 | + |
| 172 | +private: |
| 173 | + double bfs(std::unordered_map<std::string, std::unordered_map<std::string, double>>& graph, |
| 174 | + const std::string& start, const std::string& target) { |
| 175 | + if (!graph.count(start) || !graph.count(target)) return -1.0; |
| 176 | + if (start == target) return 1.0; |
| 177 | + |
| 178 | + std::queue<std::pair<std::string, double>> q; // {node, product} |
| 179 | + q.push({start, 1.0}); |
| 180 | + std::unordered_set<std::string> visited{start}; |
| 181 | + |
| 182 | + while (!q.empty()) { |
| 183 | + auto [node, product] = q.front(); q.pop(); |
| 184 | + if (node == target) return product; |
| 185 | + |
| 186 | + for (auto& [next, weight] : graph[node]) { |
| 187 | + if (visited.insert(next).second) { |
| 188 | + q.push({next, product * weight}); |
| 189 | + } |
| 190 | + } |
| 191 | + } |
| 192 | + return -1.0; |
| 193 | + } |
| 194 | +}; |
| 195 | +``` |
| 196 | +
|
| 197 | +```python |
| 198 | +from collections import deque |
| 199 | +
|
| 200 | +def calc_equation(equations: list[list[str]], values: list[float], |
| 201 | + queries: list[list[str]]) -> list[float]: |
| 202 | + """ |
| 203 | + @param equations: pairs defining ratios |
| 204 | + @param values: a / b = values[i] |
| 205 | + @param queries: x / y to evaluate |
| 206 | + @return: answers, -1.0 if undeterminable |
| 207 | + """ |
| 208 | + graph = {} |
| 209 | + for (u, v), val in zip(equations, values): |
| 210 | + graph.setdefault(u, {})[v] = val |
| 211 | + graph.setdefault(v, {})[u] = 1.0 / val |
| 212 | +
|
| 213 | + def bfs(start: str, target: str) -> float: |
| 214 | + if start not in graph or target not in graph: |
| 215 | + return -1.0 |
| 216 | + if start == target: |
| 217 | + return 1.0 |
| 218 | +
|
| 219 | + q = deque([(start, 1.0)]) |
| 220 | + visited = {start} |
| 221 | +
|
| 222 | + while q: |
| 223 | + node, product = q.popleft() |
| 224 | + if node == target: |
| 225 | + return product |
| 226 | + for nxt, weight in graph[node].items(): |
| 227 | + if nxt not in visited: |
| 228 | + visited.add(nxt) |
| 229 | + q.append((nxt, product * weight)) |
| 230 | + return -1.0 |
| 231 | +
|
| 232 | + return [bfs(u, v) for u, v in queries] |
| 233 | +``` |
| 234 | + |
| 235 | +```rust |
| 236 | +use std::collections::{HashMap, HashSet, VecDeque}; |
| 237 | + |
| 238 | +impl Solution { |
| 239 | + /// @param equations pairs defining ratios |
| 240 | + /// @param values a / b = values[i] |
| 241 | + /// @param queries x / y to evaluate |
| 242 | + /// @return answers, -1.0 if undeterminable |
| 243 | + pub fn calc_equation(equations: Vec<Vec<String>>, values: Vec<f64>, |
| 244 | + queries: Vec<Vec<String>>) -> Vec<f64> { |
| 245 | + let mut graph: HashMap<&str, HashMap<&str, f64>> = HashMap::new(); |
| 246 | + for (i, e) in equations.iter().enumerate() { |
| 247 | + graph.entry(&e[0]).or_default().insert(&e[1], values[i]); |
| 248 | + graph.entry(&e[1]).or_default().insert(&e[0], 1.0 / values[i]); |
| 249 | + } |
| 250 | + |
| 251 | + fn bfs(graph: &HashMap<&str, HashMap<&str, f64>>, start: &str, target: &str) -> f64 { |
| 252 | + if !graph.contains_key(start) || !graph.contains_key(target) { return -1.0; } |
| 253 | + if start == target { return 1.0; } |
| 254 | + |
| 255 | + let mut q: VecDeque<(&str, f64)> = VecDeque::new(); |
| 256 | + q.push_back((start, 1.0)); |
| 257 | + let mut visited: HashSet<&str> = HashSet::new(); |
| 258 | + visited.insert(start); |
| 259 | + |
| 260 | + while let Some((node, product)) = q.pop_front() { |
| 261 | + if node == target { return product; } |
| 262 | + if let Some(neighbors) = graph.get(node) { |
| 263 | + for (nxt, weight) in neighbors { |
| 264 | + if visited.insert(nxt) { |
| 265 | + q.push_back((nxt, product * weight)); |
| 266 | + } |
| 267 | + } |
| 268 | + } |
| 269 | + } |
| 270 | + -1.0 |
| 271 | + } |
| 272 | + |
| 273 | + queries.iter().map(|q| bfs(&graph, &q[0], &q[1])).collect() |
| 274 | + } |
| 275 | +} |
| 276 | +``` |
| 277 | + |
| 278 | +## Dry run |
| 279 | + |
| 280 | +**Input:** `equations = [["a","b"],["b","c"]]`, `values = [2.0, 3.0]`. |
| 281 | + |
| 282 | +``` |
| 283 | +graph: a -> {b: 2.0} b -> {a: 0.5, c: 3.0} c -> {b: 1/3} |
| 284 | +
|
| 285 | +query a/c: bfs(a, c): |
| 286 | + q=[(a,1.0)]. pop a -> b: push (b, 1.0*2.0=2.0). |
| 287 | + pop b -> target? no. neighbors: a (visited), c: push (c, 2.0*3.0=6.0). |
| 288 | + pop c == target -> return 6.0 ✓ |
| 289 | +query b/a: bfs(b, a): |
| 290 | + pop b -> a: push (a, 1.0*0.5=0.5). pop a == target -> 0.5 ✓ |
| 291 | +query a/e: e not in graph -> -1.0 ✓ |
| 292 | +query a/a: start == target -> 1.0 ✓ |
| 293 | +``` |
| 294 | + |
| 295 | +The telescoping is visible in `a/c`: `a/b · b/c = 2.0 · 3.0 = 6.0 = a/c` — the intermediate `b` cancels in the multiplication. The reciprocal edge (`b -> a: 0.5`) handles queries in the "wrong" direction, and the three special cases cover everything else. |
| 296 | + |
| 297 | +## Complexity |
| 298 | + |
| 299 | +**Time.** Per query, a BFS over the ratio graph: |
| 300 | + |
| 301 | +$$ |
| 302 | +T(Q, V, E) = O(Q \cdot (V + E)) |
| 303 | +$$ |
| 304 | + |
| 305 | +**Space.** The graph: |
| 306 | + |
| 307 | +$$ |
| 308 | +S = O(V + E) |
| 309 | +$$ |
| 310 | + |
| 311 | +## Variants & follow-ups |
| 312 | + |
| 313 | +- **Weighted Union-Find** — the alternative structure: store parent + ratio-to-parent; find returns the accumulated product. Same math, $O(\alpha)$ per query after build. |
| 314 | +- **Network Delay / longest-path** — the same edge-labeled traversal with sums instead of products. |
| 315 | +- **Interview follow-up:** "Why is the ratio along any path the same?" The equations define a *consistent* system (the problem guarantees it), so the products telescope — `a/x · x/y = a/y` regardless of the intermediate path. That consistency is what lets BFS return the first path's product without checking alternatives. |
0 commit comments