I share real-world lessons from building scalable systems at Binance, and running mission-critical cloud ops at GovTech and Singapore Air Force. No fluff, just practical takeaways, hard-earned fixes, and deep dives that matter.
Gradually finds better paths by combining known paths with new edges
Updates distances whenever a shorter path is found (edge relaxation)
Source Vertex
The starting node from which we want to find shortest paths to all other nodes
Edge Relaxation
Process of updating the shortest known distance to a node when a shorter path is found
For positive-weight graphs:
Each node typically needs only one relaxation when it’s first discovered
Priority Queue ensures we process shorter paths before longer ones
For negative-weight graphs:
Multiple relaxations may be needed for the same node
Later paths through negative edges could be shorter than earlier direct paths
Triangle Inequality
For any three nodes A, B, and C in a graph, the shortest path distances found by Dijkstra’s algorithm satisfy:
d(A,B) ≤ d(A,C) + d(C,B)
Where d(X,Y) represents the shortest path distance from X to Y found by Dijkstra’s algorithm
Key Insight
This inequality holds because:
If there was a shorter path from A to B through C, Dijkstra’s algorithm would have found it
The direct path from A to B is guaranteed to be no longer than any path that goes through C
This property is fundamental to why Dijkstra’s algorithm works correctly for non-negative edge weights
Important Note
The triangle inequality only holds for shortest path distances found by Dijkstra’s algorithm
It does not hold for arbitrary paths in the graph
For example, if S→B is not the shortest path from S to B, then d(S,B) > d(S,C) + d(C,B) might be possible
This is why Dijkstra’s algorithm must always find the actual shortest paths to maintain the triangle inequality
Complexity Intuition
Dijkstra’s algorithm always performs two key operations:
Process all vertices (V extract-min operations)
Each vertex must be processed exactly once
This is necessary to guarantee we find the shortest path to every node
The cost of each extraction depends on the priority queue implementation
Consider all edges (up to E decrease-key operations)
Each edge may need to be relaxed (updated) if a shorter path is found
The number of relaxations is bounded by the number of edges
The cost of each relaxation depends on the priority queue’s decrease-key operation
Total complexity = Cost of V vertex extractions + Cost of E edge relaxations
Key Insight
Different priority queue implementations change the cost of these operations, not the number of operations performed. This explains why the time complexity varies based on the data structure used, while the algorithm’s fundamental approach remains unchanged.
Limitations and Common Misconceptions
Negating Weights for Longest Path
A common misconception is that we can find the longest path by negating all edge weights and running Dijkstra’s algorithm. This approach fails because:
Greedy Property: Dijkstra’s algorithm relies on the greedy property that once a node is processed, we’ve found its shortest path. This property doesn’t hold for longest paths
Dijkstra’s algorithm will always return the correct answer (relaxing each edge at most once) only for:
Unweighted trees
Weighted graphs with non-negative edge weights
Java Code Templates
Construct Adjacency List
From Adjacency Matrix
# InitList < int[] > [] adjList = new ArrayList[n];for (int from = 0; from < n; from++) adjList[from] = new ArrayList < > ();# Convert from Adjacency Matrixfor (int from = 0; from < n; from++) { int[] edges = adjMaxtrix[from]; for (int to = 0; to < edges.length; to++) { int edgeWeight = edges[to]; if (edgeWeight == 0 || from == to) continue; # Use this line to skip adding relationship between 2 nodes when there isn 't a valid relationship present adjList[from].add(new int[] { to, edgeWeight }); }}
Main Algorithm
class State { int id; int distFromStart; public State(int id, int distFromStart) { this.id = id; this.distFromStart = distFromStart; }}int[] dijkstra(int start, List < int[] > [] graph) { // DP Table (Integer.MAX_VALUE for minimization problems) int[] distTo = new int[graph.length]; Arrays.fill(distTo, Integer.MAX_VALUE); distTo[start] = 0; // Greedy (min heap for minimization problems) PriorityQueue < State > pq = new PriorityQueue < > ((a, b) -> { return a.distFromStart - b.distFromStart; }); pq.offer(new State(start, 0)); while (!pq.isEmpty()) { State nodeState = pq.poll(); // Greedy approach: start from the smallest int nodeID = nodeState.id; int curDistFromStart = nodeState.distFromStart; // Skip when I already have a shorter path to reach the node if (distTo[nodeID] < curDistFromStart) continue; for (int[] neighbor: graph[nodeID]) { int nextNodeID = neighbor[0]; int distToNextNode = curDistFromStart + neighbor[1]; if (distToNextNode < distTo[nextNodeID]) { // Edge Relaxation, update dp table distTo[nextNodeID] = distToNextNode; pq.offer(new State(nextNodeID, distToNextNode)); } } } return distTo;}
Debugging Codes
Examine the node relationships of the adjacency list
for (int i = 0; i < n; i++) { List < int[] > n = adjList[i]; System.out.printf("Outward edges from node %d: \n", i); for (int[] r: n) { System.out.println(Arrays.toString(r)); } System.out.println();}