This course is designed to help learners master the core concepts of Data Structures and Algorithms (DSA) using the C programming language. Starting from the basics and advancing to complex topics like graphs, dynamic programming, and memory optimization, this course is ideal for students, job seekers, and aspiring developers. You’ll learn how to structure and manipulate data efficiently, solve real-world coding problems, and prepare for technical interviews at top companies. The content is structured step-by-step, combining theory with hands-on coding examples and practice problems to reinforce understanding. Whether you're preparing for university exams, campus placements, or competitive programming, this course provides a strong foundation in logic building, code efficiency, and problem-solving using C. Key Highlights: Covers all major DSA topics from beginner to advanced level 100+ coding examples with explanations Focus on time and space complexity optimization Designed for coding interviews, competitive exams, and CS fundamentals
The Shortest Path Problem is one of the most fundamental and widely studied problems in graph theory. Given a graph with vertices and edges (often with associated "weights" or "costs"), the goal is to find a path between two specified vertices (or from a source to all other vertices) such that the sum of the weights of its constituent edges is minimized. This problem has countless real-world applications, from GPS navigation to network routing.
For unweighted graphs, BFS naturally finds the shortest path (in terms of number of edges). However, for weighted graphs, we need more sophisticated algorithms.
Dijkstra's Algorithm is a greedy algorithm that finds the shortest paths from a single source vertex to all other vertices in a graph with non-negative edge weights. It works by iteratively building a set of vertices for which the shortest path from the source has been finalized. It's similar in spirit to BFS, but it uses a **Min-Priority Queue** to efficiently select the next vertex to process.
// Conceptual C++ for Dijkstra's Algorithm using std::priority_queue // Adjacency list: vector> adj[V]; // {neighbor, weight} void dijkstra(int source, int V, const vector<vector<pair<int, int>>>& adj) { vector<int> dist(V, INT_MAX); // Stores shortest distance from source priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; // {distance, vertex} dist[source] = 0; pq.push({0, source}); while (!pq.empty()) { int d = pq.top().first; int u = pq.top().second; pq.pop(); // If we found a shorter path to u already, skip this one if (d > dist[u]) continue; // Explore neighbors for (auto& edge : adj[u]) { int v = edge.first; int weight = edge.second; // Relaxation step if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) { dist[v] = dist[u] + weight; pq.push({dist[v], v}); } } } cout << "Shortest distances from source " << source << ":" << endl; for (int i = 0; i < V; ++i) { cout << "Node " << i << ": " << (dist[i] == INT_MAX ? "INF" : to_string(dist[i])) << endl; } }
Limitation: Dijkstra's algorithm does NOT work correctly with negative edge weights because its greedy nature assumes that once a node's distance is finalized, it won't be improved by a path going through a later-discovered negative edge.
The Bellman-Ford Algorithm solves the single-source shortest path problem for graphs that can have negative edge weights. Unlike Dijkstra, it's not greedy. Instead, it repeatedly relaxes all edges in the graph $V-1$ times.
// Conceptual C++ for Bellman-Ford Algorithm // Edges stored as a list of tuples: vector> edges; // {u, v, weight} void bellman_ford(int source, int V, const vector<tuple<int, int, int>>& edges) { vector<int> dist(V, INT_MAX); dist[source] = 0; // Relax all edges V-1 times for (int i = 0; i < V - 1; ++i) { for (const auto& edge : edges) { int u = get<0>(edge); int v = get<1>(edge); int weight = get<2>(edge); if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) { dist[v] = dist[u] + weight; } } } // Check for negative cycles bool has_negative_cycle = false; for (const auto& edge : edges) { int u = get<0>(edge); int v = get<1>(edge); int weight = get<2>(edge); if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) { has_negative_cycle = true; break; } } if (has_negative_cycle) { cout << "Graph contains a negative cycle reachable from source " << source << "!" << endl; } else { cout << "Shortest distances from source " << source << ":" << endl; for (int i = 0; i < V; ++i) { cout << "Node " << i << ": " << (dist[i] == INT_MAX ? "INF" : to_string(dist[i])) << endl; } } }
Advantages: Can handle negative edge weights, detects negative cycles.
Disadvantages: Slower than Dijkstra for graphs with only non-negative weights.
| Feature | Dijkstra's Algorithm | Bellman-Ford Algorithm |
|---|---|---|
| Edge Weights | Non-negative only | Can handle negative weights |
| Negative Cycles | Fails/produces incorrect results | Detects them |
| Time Complexity | $O(E \log V)$ (with binary heap) | $O(V \cdot E)$ |
| Best For | Large graphs with non-negative weights (most common). | Graphs with negative weights, or when negative cycle detection is needed. |
For all-pairs shortest paths, the Floyd-Warshall algorithm (dynamic programming) or running SSSP algorithms from each vertex are used.
Shortest path algorithms are critical for optimizing routes, resource allocation, and connectivity in many systems, including educational platforms:
From finding the shortest path between points, we now shift our focus to connecting all points with minimum cost. Our next chapter will introduce Minimum Spanning Trees (MSTs) and explore two classical algorithms for finding them: Kruskal's Algorithm and Prim's Algorithm. This is crucial for network design and optimization problems.