Convert amounts between multiple currencies
Company: Rippling
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Quick Answer: This question evaluates understanding of graph modeling and path-optimization in directed weighted graphs, specifically reasoning about chained conversions and maximizing multiplicative path weights.
Constraints
- 0 <= len(rates) <= 2 * 10^4
- 0 <= len(queries) <= 10^4
- There are at most 10^4 distinct currencies in the rate list
- All exchange rates are positive
- No directed cycle has a product of rates greater than 1
Examples
Input: ([('USD', 'EUR', 0.9), ('EUR', 'JPY', 130.0), ('USD', 'JPY', 100.0)], [('USD', 'JPY', 2.0), ('JPY', 'USD', 1.0), ('USD', 'USD', 5.0)])
Expected Output: [234.0, -1, 5.0]
Explanation: For USD -> JPY, the best path is USD -> EUR -> JPY with rate 0.9 * 130 = 117, so 2 * 117 = 234. There is no path from JPY to USD. Converting USD to itself uses zero exchanges, so the answer is 5.0.
Input: ([('A', 'B', 2.0), ('A', 'B', 3.0), ('B', 'C', 4.0), ('A', 'C', 10.0)], [('A', 'C', 1.0), ('C', 'C', 2.0)])
Expected Output: [12.0, 2.0]
Explanation: Between the duplicate A -> B edges, the better rate is 3.0. Then A -> B -> C gives 3 * 4 = 12, which beats the direct rate 10.0. Converting C to C returns the original amount.
Input: ([('X', 'Y', 1.5), ('Y', 'Z', 2.0)], [('X', 'Z', 3.0), ('Z', 'X', 1.0), ('Q', 'Q', 7.0), ('Q', 'X', 1.0)])
Expected Output: [9.0, -1, 7.0, -1]
Explanation: X -> Z is possible through Y with total rate 1.5 * 2.0 = 3.0, so 3.0 units become 9.0. There is no path from Z to X. Q -> Q returns 7.0 even though Q is not in the rate list. Q -> X is impossible.
Input: ([('A', 'B', 0.5), ('A', 'C', 2.0), ('C', 'B', 0.4), ('B', 'D', 3.0), ('C', 'D', 0.7)], [('A', 'D', 10.0)])
Expected Output: [24.0]
Explanation: The best path is A -> C -> B -> D with rate 2.0 * 0.4 * 3.0 = 2.4, so 10.0 becomes 24.0. Other paths produce less.
Input: ([], [('USD', 'EUR', 1.0), ('USD', 'USD', 1.0)])
Expected Output: [-1, 1.0]
Explanation: With no exchange rates, converting between different currencies is impossible. A currency can always remain unchanged, so USD -> USD returns 1.0.
Solution
def solution(rates, queries):
import math
import heapq
if not rates:
return [round(float(amount), 10) if src == dst else -1 for src, dst, amount in queries]
currency_to_id = {}
for src, dst, rate in rates:
if src not in currency_to_id:
currency_to_id[src] = len(currency_to_id)
if dst not in currency_to_id:
currency_to_id[dst] = len(currency_to_id)
# Keep only the best direct rate for each ordered pair.
best_edge = {}
for src, dst, rate in rates:
u = currency_to_id[src]
v = currency_to_id[dst]
w = -math.log(rate)
key = (u, v)
if key not in best_edge or w < best_edge[key]:
best_edge[key] = w
v_count = len(currency_to_id)
edges = []
for (u, v), w in best_edge.items():
edges.append((u, v, w))
# Bellman-Ford from an implicit super source connected to every vertex with 0 weight.
# This is equivalent to initializing all distances to 0.
h = [0.0] * v_count
for _ in range(v_count - 1):
updated = False
for u, v, w in edges:
candidate = h[u] + w
if candidate < h[v] - 1e-15:
h[v] = candidate
updated = True
if not updated:
break
# Reweight edges so all weights become non-negative.
graph = [[] for _ in range(v_count)]
for u, v, w in edges:
rw = w + h[u] - h[v]
if rw < 0 and rw > -1e-12:
rw = 0.0
graph[u].append((v, rw))
answers = [None] * len(queries)
grouped = {}
for i, (src, dst, amount) in enumerate(queries):
amount = float(amount)
if src == dst:
answers[i] = round(amount, 10)
elif src not in currency_to_id or dst not in currency_to_id:
answers[i] = -1
else:
s = currency_to_id[src]
t = currency_to_id[dst]
grouped.setdefault(s, []).append((i, t, amount))
for s, bucket in grouped.items():
dist = [float('inf')] * v_count
dist[s] = 0.0
heap = [(0.0, s)]
while heap:
d, u = heapq.heappop(heap)
if d > dist[u] + 1e-15:
continue
for v, w in graph[u]:
nd = d + w
if nd + 1e-15 < dist[v]:
dist[v] = nd
heapq.heappush(heap, (nd, v))
for idx, t, amount in bucket:
if math.isinf(dist[t]):
answers[idx] = -1
else:
original_dist = dist[t] - h[s] + h[t]
result = amount * math.exp(-original_dist)
answers[idx] = round(result, 10)
return answersTime complexity: O(V * n + s * (n + V) log V), where V is the number of distinct currencies, n is the number of direct exchange rates, and s <= q is the number of distinct source currencies appearing in the queries.. Space complexity: O(n + V + q).
Hints
- A product along a path becomes a sum if you take logarithms. Maximizing a product can be turned into minimizing a path cost.
- After transforming each edge weight to `-log(rate)`, the graph has no negative cycles. Reweight once, then run Dijkstra for each distinct query source.