SwadhyAI Notes

All DSA notes

DSA Graphs

Dijkstra's Algorithm

BFS counts hops. The courier app charges by road time. Dual conflict: the unweighted shortest path is correct for edges, wrong for minutes. You need distances under .

Dijkstra grows a : each extracted minimum distance is final. updates tentative neighbors through a that may hold .

Algorithm journey

1

Check weights ≥ 0

Otherwise the settled-set proof fails.

2

dist[s]=0, others ∞

Push s into the min-heap.

3

Extract minimum

Non-stale pop finalizes that vertex.

4

Improve neighbors; push new states.

What actually happens

Naive idea: BFS for “shortest.” Wrong when edge weights differ. Or: mark a vertex settled on first discovery (as in BFS). Wrong for weighted graphs: the first path found need not be cheapest.

Better idea: always expand the unsettled vertex with the smallest tentative distance. Under non-negative weights, that distance cannot improve later, so the vertex joins the settled set.

The improved design still fails on negative edges, on int overflow that wraps a huge cost into a “better” distance, and if you stop when the target is first pushed instead of when it is settled (popped as minimum).

Invariant

After each non-stale extract-min of u, dist[u] equals the true shortest-path distance from the source (under non-negative weights). The settled set only grows; distances of settled vertices never decrease again.

How to choose Dijkstra

1. All weights 1 (or unweighted)? Prefer BFS: same answer, simpler and often faster.

2. Non-negative weights? Dijkstra. Negatives present? Bellman–Ford or constrained variants.

3. Dense graph? O(V²) array Dijkstra can beat a binary heap when E ≈ V².

Quantitative check

Binary heap: O((V+E) log V). At V = 10⁵, E = 10⁶, expect on the order of 10⁶ log(10⁵) ≈ 2×10⁷ heap operations: usually acceptable. Skipping the stale-entry guard can multiply pops with obsolete states on dense graphs.

Interactive Dijkstra demo

Click "Run Dijkstra" to see how the algorithm explores the graph. Notice how it prioritizes nodes with the smallest current distance.

Start:
Ready to find shortest path!
A∞
B∞
C∞
D∞
E∞
42158102
A
B
C
D
E

Priority Queue

Queue is empty
Sorted by distance (Min-Heap)

Lazy priority-queue trace

Step through the implementation pattern used in interviews and production libraries: insert a new state after every improvement, then discard obsolete states when they reach the top.

Dijkstra lazy-heap trace

Follow successful relaxations, parent changes, and the duplicate heap entries that make decrease-key unnecessary.

Step 1 / 8setup

Only S is reachable. The heap contains one candidate: (S, 0).

10312824Sd=0Bd=∞Ad=∞Cd=∞Td=∞

Min-heap snapshot

(S, 0)

Parent chain to T

unreachable

Rewrite a parent only on a strict distance improvement.

Preconditions and result contract

Weight contract

Every edge reachable from the source must have weight ≥ 0. Validate this if callers can provide arbitrary data.

Distance type

Use long for accumulated Java costs and checked addition when input magnitude is not tightly bounded.

Unreachable

A vertex left at INF has no source path. Keep that state separate from every valid numeric distance.

Path ties

Strict < returns one shortest path. Define a tie rule if the lexicographically smallest path or path count matters.

Algorithm steps

1

Initialize

Set distance to start node as 0, and infinity for all others. Add start node to a priority queue.

2

Pick Smallest

Extract the node with the smallest distance from the priority queue.

3

Relax Neighbors

For each neighbor, calculate new_dist = current_dist + edge_weight. If new_dist < old_dist, update the distance and add to queue.

4

Repeat

Repeat until the priority queue is empty or the destination is reached.

The limitation

Dijkstra fails if there are negative edge weights. Why? Once a node is marked "visited", Dijkstra assumes it found the shortest path. A negative edge later could contradict this assumption. (Use Bellman-Ford for negative weights).

Where it shows up

Google Maps uses variants of Dijkstra (like A*) to find the fastest route.OSPF (Open Shortest Path First) protocol uses it to route data packets across the internet efficiently.

Correctness sketch & implementation details

Non-negative weights guarantee that the shortest path to a node found when it is extracted from the priority queue cannot later be improved: any alternative route would have to leave through an edge of non-negative length, so it could not beat the already-finalized distance.

Lazy heap: pushing every relaxation is fine; stale entries (larger distance than dist[u]) are discarded on pop, simpler than decrease-key in many standard libraries.

Store a parent map during relaxation to reconstruct paths. For single-target search, stop when the target is popped (not merely when it is first pushed).

Cut argument

  1. Assume u is the current minimum non-stale heap entry but a cheaper source-to-u path exists.
  2. On that alleged path, take the first vertex y not yet settled and its settled predecessor x.
  3. When x was settled, relaxation made dist[y] ≤ dist[x] + w(x,y).
  4. The remaining edges are non-negative, so dist[y] cannot exceed the alleged cheaper cost to u. That contradicts choosing u as the minimum.

A stale heap record is not a competing answer; it is an old snapshot. The authoritative state is dist[u], so poppedDistance != dist[u] is enough to skip it. A separate visited set is optional and often less clear.

Complexity analysis

Time Complexity

  • •
    O((V + E) log V)

    Using a Binary Heap (Priority Queue)

  • •
    O(V²)

    Using a simple array (good for dense graphs)

Space Complexity

  • •
    O(V + E)

    To store the graph and distances

Implementation

We use a Min-Heap (Priority Queue) to efficiently get the node with the smallest distance.

Python

python
import heapq

def dijkstra(graph, start):
    """
    Dijkstra's Algorithm for Shortest Paths
    
    Args:
        graph: Dictionary {node: {neighbor: weight}}
        start: Starting node
    
    Returns:
        Dictionary {node: shortest_distance}
    """
    # Priority queue stores tuples: (distance, vertex)
    # Initialize with start node at distance 0
    pq = [(0, start)]
    
    # Dictionary to store shortest distance to each node
    # Initialize all to Infinity
    distances = {vertex: float('infinity') for vertex in graph}
    distances[start] = 0

    while pq:
        # 1. Get the node with the smallest distance
        current_dist, current_node = heapq.heappop(pq)

        # Optimization: If we found a shorter path before, skip this stale entry
        if current_dist > distances[current_node]:
            continue

        # 2. Explore neighbors
        for neighbor, weight in graph[current_node].items():
            distance = current_dist + weight

            # 3. Relaxation: If we found a shorter path, update it
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                heapq.heappush(pq, (distance, neighbor))

    return distances

# Example Usage
graph = {
    'A': {'B': 4, 'C': 2},
    'B': {'D': 3, 'E': 1},
    'C': {'B': 1, 'D': 5},
    'D': {'E': 2},
    'E': {}
}

shortest_paths = dijkstra(graph, 'A')
print(f"Shortest distances from A: {shortest_paths}")
# Output: Shortest distances from A: {'A': 0, 'B': 3, 'C': 2, 'D': 6, 'E': 4}

Java

java
import java.util.*;

class Node implements Comparable<Node> {
    public String id;
    public long distance;

    public Node(String id, long distance) {
        this.id = id;
        this.distance = distance;
    }

    @Override
    public int compareTo(Node other) {
        return Long.compare(this.distance, other.distance);
    }
}

public class Dijkstra {
    public static Map<String, Long> dijkstra(Map<String, Map<String, Integer>> graph, String start) {
        // Priority queue to store nodes sorted by distance
        PriorityQueue<Node> pq = new PriorityQueue<>();
        pq.add(new Node(start, 0));

        // Map to store shortest distance to each node
        Map<String, Long> distances = new HashMap<>();
        for (String node : graph.keySet()) {
            distances.put(node, Long.MAX_VALUE);
        }
        distances.put(start, 0L);

        while (!pq.isEmpty()) {
            // 1. Get the node with the smallest distance
            Node current = pq.poll();
            String currentNode = current.id;
            long currentDist = current.distance;

            // Optimization: Skip stale entries
            if (currentDist > distances.get(currentNode)) {
                continue;
            }

            // 2. Explore neighbors
            Map<String, Integer> neighbors = graph.get(currentNode);
            if (neighbors != null) {
                for (Map.Entry<String, Integer> neighborEntry : neighbors.entrySet()) {
                    String neighbor = neighborEntry.getKey();
                    int weight = neighborEntry.getValue();
                    if (weight < 0) {
                        throw new IllegalArgumentException("Dijkstra requires non-negative weights");
                    }
                    long newDist = Math.addExact(currentDist, (long) weight);

                    // 3. Relaxation
                    if (newDist < distances.get(neighbor)) {
                        distances.put(neighbor, newDist);
                        pq.add(new Node(neighbor, newDist));
                    }
                }
            }
        }

        return distances;
    }

    public static void main(String[] args) {
        Map<String, Map<String, Integer>> graph = new HashMap<>();
        
        // Building the graph
        graph.put("A", new HashMap<>(Map.of("B", 4, "C", 2)));
        graph.put("B", new HashMap<>(Map.of("D", 3, "E", 1)));
        graph.put("C", new HashMap<>(Map.of("B", 1, "D", 5)));
        graph.put("D", new HashMap<>(Map.of("E", 2)));
        graph.put("E", new HashMap<>());

        Map<String, Long> shortestPaths = dijkstra(graph, "A");
        System.out.println("Shortest distances from A: " + shortestPaths);
        // Output: Shortest distances from A: {A=0, B=3, C=2, D=6, E=4}
    }
}

Java template: distances + paths

This array-based template validates the weight contract, uses a lazy min-heap, performs checked long addition, and refuses to fabricate a path for an unreachable or corrupt parent chain.

java
import java.util.*;

final class DijkstraPaths {
    static final long INF = Long.MAX_VALUE;

    static final class Edge {
        final int to;
        final long weight;
        Edge(int to, long weight) {
            this.to = to;
            this.weight = weight;
        }
    }

    static final class State implements Comparable<State> {
        final int vertex;
        final long distance;
        State(int vertex, long distance) {
            this.vertex = vertex;
            this.distance = distance;
        }
        public int compareTo(State other) {
            return Long.compare(distance, other.distance);
        }
    }

    static final class Result {
        final int source;
        final long[] distance;
        final int[] parent;
        Result(int source, long[] distance, int[] parent) {
            this.source = source;
            this.distance = distance;
            this.parent = parent;
        }
    }

    static Result shortestPaths(List<Edge>[] graph, int source) {
        int n = graph.length;
        if (source < 0 || source >= n) throw new IllegalArgumentException("bad source");
        for (List<Edge> edges : graph) {
            for (Edge edge : edges) {
                if (edge.weight < 0) {
                    throw new IllegalArgumentException("negative edge");
                }
            }
        }

        long[] dist = new long[n];
        int[] parent = new int[n];
        Arrays.fill(dist, INF);
        Arrays.fill(parent, -1);
        dist[source] = 0L;

        PriorityQueue<State> minHeap = new PriorityQueue<>();
        minHeap.add(new State(source, 0L));

        while (!minHeap.isEmpty()) {
            State current = minHeap.poll();
            int u = current.vertex;
            if (current.distance != dist[u]) continue; // stale heap entry

            for (Edge edge : graph[u]) {
                long candidate = Math.addExact(dist[u], edge.weight);
                if (candidate >= dist[edge.to]) continue;
                dist[edge.to] = candidate;
                parent[edge.to] = u;
                minHeap.add(new State(edge.to, candidate));
            }
        }
        return new Result(source, dist, parent);
    }

    static List<Integer> pathTo(Result result, int target) {
        if (target < 0 || target >= result.distance.length) {
            throw new IllegalArgumentException("bad target");
        }
        if (result.distance[target] == INF) return List.of();

        List<Integer> reversed = new ArrayList<>();
        boolean[] seen = new boolean[result.distance.length];
        for (int at = target; at != -1; at = result.parent[at]) {
            if (seen[at]) throw new IllegalStateException("parent cycle");
            seen[at] = true;
            reversed.add(at);
            if (at == result.source) {
                Collections.reverse(reversed);
                return reversed;
            }
        }
        throw new IllegalStateException("parent chain missed source");
    }
}

Complexity trade-offs and failure modes

Choice / bugEffectGuidance
Adjacency list + binary heapO((V+E) log V), O(V+E) graph spaceDefault for sparse graphs and standard libraries.
Matrix + linear minimum scanO(V²), O(V²) graph spaceCompetitive when E≈V²; avoids heap overhead.
No stale-entry guardStill often correct, but repeatedly scans old states and can destroy practical performance.Compare the popped snapshot with authoritative dist[u].
Visited on insertionLocks the first discovered path, which need not be cheapest.Finalize only a current minimum pop.
Negative edgeInvalidates the greedy proof even if a sample happens to pass.Reject input or select DAG/Bellman–Ford logic.
int accumulated costWraparound can make a huge path look negative and win the heap.Use long and bounded or checked addition.

Short takeaway

Dijkstra computes shortest paths under non-negative weights by growing a settled set via extract-min. Use a priority queue, discard stale entries, and switch algorithms when negatives appear.

Exercises

  1. Give a tiny graph with one negative edge where Dijkstra settles a vertex too early. Which vertex enters the settled set with the wrong distance?
  2. Trace a lazy heap on a diamond where a cheaper path arrives after an expensive one was pushed. Which pops are stale, and what is dist[] after each?
  3. V = 10⁴ dense (E ≈ V²). Compare binary-heap Dijkstra with the O(V²) array variant. Which would you pick and why?

Primary sources

  • Dijkstra, E. W. (1959): A note on two problems in connexion with graphs.
  • Cormen et al., Introduction to Algorithms (CLRS): Dijkstra's algorithm and the settled-set proof.
  • Fredman & Tarjan: Fibonacci heaps (theoretical decrease-key; contrast with lazy binary heaps in practice).