Skip to content

1514. Path with Maximum Probability (Medium)

Problem

You are given an undirected graph with n nodes. edges[i] = [a, b] with succProb[i] means the probability of success traveling from a to b (and vice versa). Find the path from start to end with the maximum probability of success. Return 0 if no path exists.

Example

  • n=3, edges [[0,1],[1,2],[0,2]], succProb [0.5,0.5,0.2], start=0, end=2 → 0.25
    • Path 0->1->2 has probability 0.5 * 0.5 = 0.25
    • Direct 0->2 has probability 0.2
    • 0.25 > 0.2, so answer is 0.25

LeetCode 1514 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Brute force, Bellman-Ford style relaxation

Repeat relaxation until no updates occur. Works because probabilities are always in (0, 1], so multiplying never increases the value — no negative-cycle analog.

def max_probability(n, edges, succ_prob, start, end):
prob = [0.0] * n # L1: O(V) init
prob[start] = 1.0 # L2: O(1) seed
for _ in range(n - 1): # L3: at most V-1 rounds
updated = False
for i, (u, v) in enumerate(edges): # L4: O(E) per round
p = succ_prob[i]
if prob[u] * p > prob[v]: # L5: O(1) relax u->v
prob[v] = prob[u] * p
updated = True
if prob[v] * p > prob[u]: # L6: O(1) relax v->u (undirected)
prob[u] = prob[v] * p
updated = True
if not updated: # L7: O(1) early exit
break
return prob[end] # L8: O(1)

Where the time goes, line by line

Variables: V = n (nodes), E = number of edges.

LinePer-call costTimes executedContribution
L1-L2 (init)O(1)O(1)VO(V)O(V)
L3 (outer rounds)O(1)O(1)up to V-1O(V)O(V)
L4 (edge scan)O(1)O(1)E * (V-1)O(VE)O(V * E) ← dominates
L5/L6 (relax)O(1)O(1)up to 2E per roundO(VE)O(V * E)
L7 (early exit)O(1)O(1)up to V-1O(V)O(V)

With early exit this is fast in practice, but worst-case is O(VE)O(V * E). For a dense graph that is O(V3)O(V^3).

Complexity

  • Time: O(VE)O(V * E), driven by L4 (edge scan per round).
  • Space: O(V)O(V).

Approach 2: Modified Dijkstra with max-heap (optimal)

Standard Dijkstra minimizes cost; here we maximize probability. Swap the min-heap for a max-heap by negating probabilities (Python’s heapq is a min-heap).

Key insight: probabilities are multiplied along a path, so the “best” path uses the product of edge probabilities. This is still monotone: adding more edges can only decrease or maintain probability, just as adding more edges (with positive weight) increases distance in standard Dijkstra.

import heapq
from collections import defaultdict
def max_probability(n, edges, succ_prob, start, end):
graph = defaultdict(list) # L1: O(1) init
for i, (u, v) in enumerate(edges): # L2: O(E) build adjacency
graph[u].append((v, succ_prob[i]))
graph[v].append((u, succ_prob[i]))
prob = [0.0] * n # L3: O(V) init probabilities
prob[start] = 1.0 # L4: O(1) seed
# max-heap: negate probability so largest comes out first
heap = [(-1.0, start)] # L5: O(1) seed heap
while heap: # L6: main loop
neg_p, u = heapq.heappop(heap) # L7: O(log V) pop best prob
p = -neg_p
if p < prob[u]: # L8: O(1) stale check
continue
if u == end: # L9: O(1) early exit
return p
for v, edge_p in graph[u]: # L10: O(deg(u)) neighbors
new_p = p * edge_p # L11: O(1) path prob
if new_p > prob[v]: # L12: O(1) improvement check
prob[v] = new_p # L13: O(1) update
heapq.heappush(heap, (-new_p, v)) # L14: O(log V) push
return prob[end] # L15: O(1) result

Where the time goes, line by line

Variables: V = n (nodes), E = number of edges.

LinePer-call costTimes executedContribution
L1-L2 (build graph)O(1)O(1)EO(E)O(E)
L3-L5 (init)O(1)O(1)VO(V)O(V)
L6 (loop)O(1)O(1)up to EO(E)O(E)
L7 (heappop)O(logV)O(log V)up to EO(ElogV)O(E log V) ← dominates
L8/L9 (checks)O(1)O(1)up to EO(E)O(E)
L10 (neighbors)O(1)O(1)E totalO(E)O(E)
L14 (heappush)O(logV)O(log V)up to EO(ElogV)O(E log V) ← dominates

Each edge can produce at most one push (L14). With at most E pushes and each costing O(logV)O(log V) (heap size bounded by V), total heap work is O(ElogV)O(E log V).

Complexity

  • Time: O((V+E)O((V + E) log V), driven by L7/L14 (heap operations).
  • Space: O(V+E)O(V + E) for the adjacency list and heap.

Why negation works

Python’s heapq pops the smallest item. We want the largest probability first. Negating flips the ordering:

max-heap of probabilities == min-heap of negated probabilities
heappush(heap, (-0.5, node)) -> pops as (-0.5) before (-0.2)
i.e., prob 0.5 before prob 0.2

Trace on example

Graph: 0-1 (0.5), 1-2 (0.5), 0-2 (0.2)
prob = [1.0, 0.0, 0.0]
heap = [(-1.0, 0)]
Pop (-1.0, 0), p=1.0:
neighbor 1: new_p = 1.0*0.5 = 0.5 > 0.0 -> push (-0.5, 1), prob[1]=0.5
neighbor 2: new_p = 1.0*0.2 = 0.2 > 0.0 -> push (-0.2, 2), prob[2]=0.2
Pop (-0.5, 1), p=0.5:
neighbor 0: new_p = 0.5*0.5 = 0.25 < prob[0]=1.0 -> skip
neighbor 2: new_p = 0.5*0.5 = 0.25 > prob[2]=0.2 -> push (-0.25, 2), prob[2]=0.25
Pop (-0.25, 2), p=0.25: u==end -> return 0.25

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Summary

ApproachTimeSpaceNotes
Bellman-Ford relaxationO(VE)O(V * E)O(V)O(V)Simple, handles all cases
Modified DijkstraO((V+E)O((V+E) log V)O(V+E)O(V+E)Optimal for non-zero probabilities

The mapping from standard Dijkstra: min distance becomes max probability, sum of weights becomes product of probabilities, infinity becomes 0.0, 0 (source distance) becomes 1.0 (certainty at source).

Test cases

import heapq
from collections import defaultdict
def max_probability(n, edges, succ_prob, start, end):
graph = defaultdict(list)
for i, (u, v) in enumerate(edges):
graph[u].append((v, succ_prob[i]))
graph[v].append((u, succ_prob[i]))
prob = [0.0] * n
prob[start] = 1.0
heap = [(-1.0, start)]
while heap:
neg_p, u = heapq.heappop(heap)
p = -neg_p
if p < prob[u]:
continue
if u == end:
return p
for v, edge_p in graph[u]:
new_p = p * edge_p
if new_p > prob[v]:
prob[v] = new_p
heapq.heappush(heap, (-new_p, v))
return prob[end]
def _run_tests():
assert abs(max_probability(3, [[0,1],[1,2],[0,2]], [0.5,0.5,0.2], 0, 2) - 0.25) < 1e-5
assert abs(max_probability(3, [[0,1],[1,2],[0,2]], [0.5,0.5,0.3], 0, 2) - 0.3) < 1e-5
assert max_probability(3, [[0,1]], [0.5], 0, 2) == 0.0
assert abs(max_probability(2, [[0,1]], [0.9], 0, 1) - 0.9) < 1e-5
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Dijkstra, the priority queue shortest path pattern for non negative edge weights.
  • Shortest Paths, the frontier model for minimizing distance, cost, or probability through a graph.