Skip to content

Commit 4ed00ca

Browse files
committed
Complete Chapter 17 (Advanced Graphs): max flow, bipartite matching, MST, Held-Karp TSP, bitmask BFS, edge-labeled graphs
1 parent 20e87d1 commit 4ed00ca

10 files changed

Lines changed: 2061 additions & 0 deletions

‎CodingInterviewFightClub/src/SUMMARY.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,13 @@
184184
- [16.5 Maximum XOR Of Two Numbers](ch16-bit-manipulation/maximum-xor-of-two-numbers.md)
185185
- [16.6 Sum Of All Subset XOR Totals](ch16-bit-manipulation/sum-of-all-subset-xor-totals.md)
186186
- [16.7 Smallest Number With All Set Bits](ch16-bit-manipulation/smallest-number-with-all-set-bits.md)
187+
188+
- [17. Advanced Graphs](ch17-advanced-graphs/index.md)
189+
- [17.0 Pattern Primer: Flow, Matching, MST, and State-Space BFS](ch17-advanced-graphs/pattern-primer.md)
190+
- [17.1 Max Flow (Edmonds-Karp)](ch17-advanced-graphs/max-flow-edmonds-karp.md)
191+
- [17.2 Maximum Bipartite Matching](ch17-advanced-graphs/maximum-bipartite-matching.md)
192+
- [17.3 Min Cost To Connect All Points (Prim's)](ch17-advanced-graphs/min-cost-to-connect-all-points-prims.md)
193+
- [17.4 Travelling Salesman (Held-Karp)](ch17-advanced-graphs/travelling-salesman-held-karp.md)
194+
- [17.5 Shortest Path Visiting All Nodes](ch17-advanced-graphs/shortest-path-visiting-all-nodes.md)
195+
- [17.6 Reorder Routes To City Zero](ch17-advanced-graphs/reorder-routes-to-make-all-paths-lead-to-city-zero.md)
196+
- [17.7 Evaluate Division](ch17-advanced-graphs/evaluate-division.md)
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
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.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Chapter 17 — Advanced Graphs
2+
3+
> **Source:** `src/main/kotlin/graph/flow_network/`, `src/main/kotlin/graph/tsp/`, `src/main/kotlin/tree/mst/`, and the `graph/` root
4+
>
5+
> **Master idea:** beyond BFS/DFS (Chapter 6) lie the *optimization* graph problems: **flow networks** (how much can travel through a capacitated graph?), **bipartite matching** (assignments with conflicts), **minimum spanning trees** (connect everything cheaply), **TSP** (visit everything optimally), and the **state-space BFS** tricks (bitmask states, edge weights as graph labels).
6+
>
7+
> **Prerequisites:** BFS/DFS from [Chapter 6](../ch06-graphs/index.md), DP from [Chapter 2](../ch02-dynamic-programming/index.md) (Held-Karp), bitmasks from [Chapter 16](../ch16-bit-manipulation/index.md), and heaps from [Chapter 7](../ch07-heaps/index.md) (Prim's).
8+
9+
## Problems at a glance (this chapter's core set)
10+
11+
| # | Problem | Pattern | Complexity | Page |
12+
|---|---------|---------|------------|------|
13+
| 17.1 | Max Flow (Edmonds-Karp) | BFS augmenting paths | $O(VE^2)$ | [→](max-flow-edmonds-karp.md) |
14+
| 17.2 | Maximum Bipartite Matching | Kuhn's augmenting path | $O(VE)$ | [→](maximum-bipartite-matching.md) |
15+
| 17.3 | Min Cost To Connect All Points | Prim's MST | $O(n^2 \log n)$ | [→](min-cost-to-connect-all-points-prims.md) |
16+
| 17.4 | Travelling Salesman (Held-Karp) | bitmask DP | $O(n^2 2^n)$ | [→](travelling-salesman-held-karp.md) |
17+
| 17.5 | Shortest Path Visiting All Nodes | BFS over bitmask states | $O(n \cdot 2^n)$ | [→](shortest-path-visiting-all-nodes.md) |
18+
| 17.6 | Reorder Routes To City Zero | directed-edge DFS | $O(n)$ | [→](reorder-routes-to-make-all-paths-lead-to-city-zero.md) |
19+
| 17.7 | Evaluate Division | edge-labeled graph BFS | $O(Q \cdot E)$ | [→](evaluate-division.md) |
20+
21+
## The rest of the graph/ directories
22+
23+
`flow_network/` also holds several Edmonds-Karp variants and `BipartileMatching.kt` (the same Kuhn's algorithm as 17.2). `tsp/` adds `ShortestPathVisitingAllNodes.kt` (17.5), the brute-force and top-down TSP versions, and `TravellingSalesmanRecursiveDP.kt`. `tree/mst/` adds the Kruskal version of 17.3 ([6.6](../ch06-graphs/min-cost-to-connect-all-points.md) already covers it). `graph/` also has articulation points, SCC, topological sorts, chromatic number, and more.
24+
25+
New pages are appended to the table above as they're written.

0 commit comments

Comments
 (0)