Great question, AlgoMaster99! Dijkstra's is fundamental. The key to the manual trace is always to pick the node with the *absolutely smallest* distance among those *not yet finalized*. For your example, starting with A:
- Initialize: dist(A)=0, dist(B)=inf, dist(C)=inf, dist(D)=inf. PQ: {(0, A)}. Visited: {}.
- Extract A. dist(A)=0. Neighbors of A: B(10), C(3). Update B: dist(B) = 0 + 10 = 10. PQ: {(10, B)}. Update C: dist(C) = 0 + 3 = 3. PQ: {(3, C), (10, B)}. Visited: {A}.
- Extract C (dist 3 is min). dist(C)=3. Neighbors of C: B(4), D(8). Update B: Current dist(B) is 10. Path through C: 3 + 4 = 7. 7 < 10, so dist(B) = 7. PQ: {(7, B), (10, B)}. (Note: depending on PQ implementation, the old (10, B) might remain or be updated/removed). Update D: dist(D) = 3 + 8 = 11. PQ: {(7, B), (10, B), (11, D)}. Visited: {A, C}.
- Extract B (dist 7 is min). dist(B)=7. Neighbors of B: C(1), D(2). Neighbor C is already visited. Update D: Current dist(D) is 11. Path through B: 7 + 2 = 9. 9 < 11, so dist(D) = 9. PQ: {(9, D), (10, B), (11, D)}. Visited: {A, C, B}.
- Extract D (dist 9 is min). dist(D)=9. Neighbors of D: A(7). Neighbor A is already visited. Visited: {A, C, B, D}.
- PQ is empty.
Final shortest distances from A: dist(A)=0, dist(B)=7, dist(C)=3, dist(D)=9.
For implementation, a binary heap for the priority queue gives O((V+E) log V) or O(E log V) if we are careful with PQ updates (decrease-key operation). A Fibonacci heap can achieve O(E + V log V), which is better for dense graphs, but often has higher constant factors in practice.
A common pitfall is forgetting to check `if dist > distances[u]: continue` after extracting from the PQ. This handles stale entries when a shorter path to `u` was found after `u` was initially added to the PQ.