Path with Maximum Probability
MedPrompt
You are given an undirected weighted graph of n nodes (0-indexed), represented by an edge list where edges[i] = [a, b] is an undirected edge connecting the nodes a and b with a probability of success of traversing that edge succProb[i].
Given two nodes start and end, find the path with the maximum probability of success to go from start to end and return its success probability.
If there is no path from start to end, return 0. Your answer will be accepted if it differs from the correct answer by at most 1e-5.
Example 1:

Input: n = 3, edges = [[0,1],[1,2],[0,2]], succProb = [0.5,0.5,0.2], start = 0, end = 2
Output: 0.25000
Explanation: There are two paths from start to end, one having a probability of success = 0.2 and the other has 0.5 * 0.5 = 0.25.Example 2:

Input: n = 3, edges = [[0,1],[1,2],[0,2]], succProb = [0.5,0.5,0.3], start = 0, end = 2
Output: 0.30000Example 3:

Input: n = 3, edges = [[0,1]], succProb = [0.5], start = 0, end = 2
Output: 0.00000
Explanation: There is no path between 0 and 2.
Constraints:
2 <= n <= 10^40 <= start, end < nstart != end0 <= a, b < na != b0 <= succProb.length == edges.length <= 2*10^40 <= succProb[i] <= 1- There is at most one edge between every two nodes.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach adapts the Bellman-Ford algorithm, which is typically used for finding the shortest paths in a weighted graph, even with negative edge weights. Instead of minimizing the sum of edge weights, we modify it to maximize the product of probabilities. The Bellman-Ford algorithm works by iteratively "relaxing" edges. In our modification, a relaxation step involves checking if traversing an edge can lead to a path with a higher total probability to a node and updating it if so.
Algorithm
- Initialize a
doublearraymaxProbof sizenwith all values as 0.0. - Set
maxProb[start] = 1.0. - Repeat the following steps
n-1times:- Create a flag
hasUpdateand set it tofalse. - Iterate through each edge
(u, v)with probabilityp. - If a more probable path to
vis found viau(i.e.,maxProb[u] * p > maxProb[v]), updatemaxProb[v] = maxProb[u] * pand sethasUpdatetotrue. - Since the graph is undirected, do the same for the other direction: if
maxProb[v] * p > maxProb[u], updatemaxProb[u] = maxProb[v] * pand sethasUpdatetotrue. - If after a full iteration over all edges,
hasUpdateisfalse, it means no probabilities were improved, and we can break the loop early.
- Create a flag
- After the loops complete,
maxProb[end]contains the maximum probability. Return this value.
Walkthrough
We maintain an array, maxProb, of size n, where maxProb[i] stores the maximum probability of a path from the start node to node i. We initialize maxProb[start] to 1.0 and all other entries to 0.0, as we have a 100% chance of being at the start initially and zero chance of being anywhere else.
The algorithm then iterates n-1 times. In each iteration, it processes all E edges of the graph. For each edge (u, v) with success probability p, it updates the probability to reach v if the path through u (maxProb[u] * p) is better than the current known probability maxProb[v]. Since the graph is undirected, the same update is considered for u via v. This process is repeated n-1 times to guarantee that the maximum probabilities for paths of all possible lengths (up to n-1 edges) are found. After the iterations, maxProb[end] will hold the solution.
class Solution { public double maxProbability(int n, int[][] edges, double[] succProb, int start, int end) { double[] maxProb = new double[n]; maxProb[start] = 1.0; // Bellman-Ford requires n-1 iterations in the worst case. for (int i = 0; i < n - 1; i++) { boolean hasUpdate = false; for (int j = 0; j < edges.length; j++) { int u = edges[j][0]; int v = edges[j][1]; double prob = succProb[j]; if (maxProb[u] * prob > maxProb[v]) { maxProb[v] = maxProb[u] * prob; hasUpdate = true; } if (maxProb[v] * prob > maxProb[u]) { maxProb[u] = maxProb[v] * prob; hasUpdate = true; } } // Optimization: if no updates in an iteration, we can stop early. if (!hasUpdate) { break; } } return maxProb[end]; }}Complexity
Time
O(n * E), where `n` is the number of nodes and `E` is the number of edges. The algorithm has a main loop that runs `n-1` times, and inside this loop, it iterates through all `E` edges.
Space
O(n), where `n` is the number of nodes. This is for the `maxProb` array used to store the maximum probabilities to each node.
Trade-offs
Pros
Relatively simple to understand and implement without complex data structures like priority queues.
It can handle all types of graphs, including those that would have negative cycles in a standard shortest-path context (though not relevant here).
Cons
The time complexity of O(n * E) is generally too slow for graphs of the size specified in the constraints, and it will likely result in a 'Time Limit Exceeded' error.
This problem can be solved efficiently by adapting Dijkstra's algorithm. Standard Dijkstra's finds the shortest path by minimizing a sum of weights. Here, we need to maximize a product of probabilities. We can modify the algorithm to achieve this by using a max-priority queue instead of a min-priority queue. This greedy approach ensures that we always explore the path that is currently the most probable, which is guaranteed to find the optimal solution because all edge 'weights' (probabilities) are positive, analogous to non-negative weights in the standard algorithm.
An alternative but equivalent method is to transform the problem. Maximizing a product p1 * p2 * ... is the same as maximizing its logarithm log(p1) + log(p2) + .... This, in turn, is equivalent to minimizing the sum of negative logarithms (-log(p1)) + (-log(p2)) + .... Since probabilities are ≤ 1, their logs are ≤ 0, and their negative logs are ≥ 0. This transforms the problem into a standard shortest path problem with non-negative weights, solvable by the classic Dijkstra's algorithm. The implementation below uses the direct maximization approach for simplicity.
Algorithm
- Build an adjacency list representation of the graph where
adj[u]contains a list of pairs(v, p), representing an edge fromutovwith probabilityp. - Initialize a
doublearraymaxProbof sizenwith all values as 0.0. SetmaxProb[start] = 1.0. - Create a max-priority queue that stores pairs of
(probability, node)and orders them by probability in descending order. - Add the starting pair
(1.0, start)to the priority queue. - While the priority queue is not empty:
- Extract the pair
(currentProb, currentNode)with the highest probability. - If
currentNodeis theendnode, we have found the optimal path, so returncurrentProb. - If
currentProbis less thanmaxProb[currentNode], it means we've found a better path to this node already, so we skip this (stale) entry. - For each neighbor
neighborNodeofcurrentNodewith edge probabilityedgeProb:- Calculate the probability of the path through
currentNode:newProb = currentProb * edgeProb. - If
newProbis greater thanmaxProb[neighborNode], we have found a better path. UpdatemaxProb[neighborNode] = newProband add the new pair(newProb, neighborNode)to the priority queue.
- Calculate the probability of the path through
- Extract the pair
- If the loop finishes and the
endnode has not been reached, it is unreachable. Return 0.0.
Walkthrough
The core of this approach is to use a max-priority queue to greedily explore the most promising paths. We start at the start node with a probability of 1.
First, we represent the graph using an adjacency list for efficient access to neighbors. Then, we initialize an array maxProb to keep track of the maximum probability to reach each node from start, setting maxProb[start] to 1 and all others to 0.
We push the starting node and its probability (1.0, start) into a max-priority queue. The algorithm proceeds by repeatedly extracting the node with the highest probability from the queue. For the extracted node, we examine all its neighbors. For each neighbor, we calculate the probability of reaching it via the current node. If this new probability is higher than the previously recorded maximum probability for that neighbor, we update the maxProb array and push the neighbor with its new, higher probability into the queue.
We also include a check to discard stale entries from the queue—if we pull a node from the queue whose probability is already lower than what's in our maxProb array, we ignore it. The algorithm terminates when we extract the end node from the queue, as the greedy nature of Dijkstra's ensures this is the highest probability path.
import java.util.*; class Solution { public double maxProbability(int n, int[][] edges, double[] succProb, int start, int end) { // Adjacency list: Map<Node, List<Pair<Neighbor, Probability>>> Map<Integer, List<double[]>> adj = new HashMap<>(); for (int i = 0; i < edges.length; i++) { int u = edges[i][0]; int v = edges[i][1]; double prob = succProb[i]; adj.computeIfAbsent(u, k -> new ArrayList<>()).add(new double[]{v, prob}); adj.computeIfAbsent(v, k -> new ArrayList<>()).add(new double[]{u, prob}); } double[] maxProb = new double[n]; maxProb[start] = 1.0; // Max-priority queue: {probability, node} PriorityQueue<double[]> pq = new PriorityQueue<>((a, b) -> Double.compare(b[0], a[0])); pq.offer(new double[]{1.0, (double)start}); while (!pq.isEmpty()) { double[] current = pq.poll(); double currentProb = current[0]; int currentNode = (int)current[1]; if (currentNode == end) { return currentProb; } if (currentProb < maxProb[currentNode]) { continue; } if (!adj.containsKey(currentNode)) { continue; } for (double[] neighbor : adj.get(currentNode)) { int neighborNode = (int)neighbor[0]; double edgeProb = neighbor[1]; double newProb = currentProb * edgeProb; if (newProb > maxProb[neighborNode]) { maxProb[neighborNode] = newProb; pq.offer(new double[]{newProb, (double)neighborNode}); } } } return 0.0; // end is not reachable }}Complexity
Time
O(E log n), where `n` is the number of nodes and `E` is the number of edges. Building the adjacency list takes O(E). The main loop processes each node and edge once. Each priority queue operation (insertion and extraction) takes O(log n) time. In the worst case, we perform one extraction per node and one insertion per edge.
Space
O(n + E), where `n` is the number of nodes and `E` is the number of edges. This space is used for the adjacency list O(E), the `maxProb` array O(n), and the priority queue, which can hold up to O(n) nodes in the worst case.
Trade-offs
Pros
Highly efficient and the standard algorithm for this type of problem.
Guaranteed to find the optimal path because the problem structure (multiplying probabilities between 0 and 1) is analogous to summing non-negative weights.
Cons
Slightly more complex to implement than the Bellman-Ford approach due to the need for an adjacency list and a priority queue.
Solutions
Solution
class Solution {public double maxProbability(int n, int[][] edges, double[] succProb, int start, int end) { List<Pair<Integer, Double>>[] g = new List[n]; Arrays.setAll(g, k->new ArrayList<>()); for (int i = 0; i < edges.length; ++i) { int a = edges[i][0], b = edges[i][1]; double s = succProb[i]; g[a].add(new Pair<>(b, s)); g[b].add(new Pair<>(a, s)); } PriorityQueue<Pair<Double, Integer>> q = new PriorityQueue<>(Comparator.comparingDouble(Pair : : getKey)); double[] d = new double[n]; d[start] = 1.0; q.offer(new Pair<>(-1.0, start)); while (!q.isEmpty()) { Pair<Double, Integer> p = q.poll(); double w = p.getKey(); w *= -1; int u = p.getValue(); for (Pair<Integer, Double> ne : g[u]) { int v = ne.getKey(); double t = ne.getValue(); if (d[v] < d[u] * t) { d[v] = d[u] * t; q.offer(new Pair<>(-d[v], v)); } } } return d[end]; }}class Solution {public: double maxProbability(int n, vector<vector<int>> &edges, vector<double> &succProb, int start, int end) { vector<vector<pair<int, double>>> g(n); for (int i = 0; i < edges.size(); ++i) { int a = edges[i][0], b = edges[i][1]; double s = succProb[i]; g[a].push_back({b, s}); g[b].push_back({a, s}); } vector<double> d(n); d[start] = 1.0; queue<pair<double, int>> q; q.push({1.0, start}); while (!q.empty()) { auto p = q.front(); q.pop(); double w = p.first; int u = p.second; if (d[u] > w) continue; for (auto &e : g[u]) { int v = e.first; double t = e.second; if (d[v] < d[u] * t) { d[v] = d[u] * t; q.push({d[v], v}); } } } return d[end]; }};class Solution: def maxProbability(self, n: int, edges: List[List[int]], succProb: List[float], start: int, end: int, ) -> float: g = defaultdict(list) for (a, b), s in zip(edges, succProb): g[a]. append((b, s)) g[b]. append((a, s)) q = [(- 1, start)] d = [0] * n d[start] = 1 while q: w, u = heappop(q) w = - w if d[u] > w: continue for v, t in g[u]: if d[v] < d[u] * t: d[v] = d[u] * t heappush(q, (- d[v], v)) return d[end]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.