Graph Algorithms
Introduction to Graph Algorithms
Graphs are fundamental data structures representing relationships between entities. Graph algorithms solve problems like finding shortest paths, detecting cycles, and determining connectivity. Understanding graphs is essential for network analysis, social media, GPS navigation, and many other applications.
Graph Representation
Graphs can be represented using adjacency lists (space-efficient for sparse graphs) or adjacency matrices (fast lookup for dense graphs).
Graph Representation Methods
#include <iostream>
#include <vector>
#include <list>
using namespace std;
// Adjacency List Representation
class GraphAdjList {
private:
int V; // Number of vertices
vector<list<int>> adj; // Adjacency list
public:
GraphAdjList(int vertices) : V(vertices) {
adj.resize(V);
}
void addEdge(int u, int v) {
adj[u].push_back(v);
adj[v].push_back(u); // For undirected graph
}
void printGraph() {
cout << "Adjacency List Representation:" << endl;
for (int i = 0; i < V; i++) {
cout << "Vertex " << i << ": ";
for (int neighbor : adj[i]) {
cout << neighbor << " ";
}
cout << endl;
}
cout << endl;
}
vector<list<int>>& getAdjList() { return adj; }
int getVertices() { return V; }
};
// Adjacency Matrix Representation
class GraphAdjMatrix {
private:
int V;
vector<vector<int>> adjMatrix;
public:
GraphAdjMatrix(int vertices) : V(vertices) {
adjMatrix.resize(V, vector<int>(V, 0));
}
void addEdge(int u, int v) {
adjMatrix[u][v] = 1;
adjMatrix[v][u] = 1; // For undirected graph
}
void printGraph() {
cout << "Adjacency Matrix Representation:" << endl;
cout << " ";
for (int i = 0; i < V; i++) {
cout << i << " ";
}
cout << endl;
for (int i = 0; i < V; i++) {
cout << i << ": ";
for (int j = 0; j < V; j++) {
cout << adjMatrix[i][j] << " ";
}
cout << endl;
}
cout << endl;
}
bool hasEdge(int u, int v) {
return adjMatrix[u][v] == 1;
}
vector<vector<int>>& getMatrix() { return adjMatrix; }
int getVertices() { return V; }
};
int main() {
// Create graph with 5 vertices
GraphAdjList graphList(5);
GraphAdjMatrix graphMatrix(5);
// Add edges: 0-1, 0-4, 1-2, 1-3, 1-4, 2-3, 3-4
vector<pair<int, int>> edges = {{0,1}, {0,4}, {1,2}, {1,3}, {1,4}, {2,3}, {3,4}};
for (auto edge : edges) {
graphList.addEdge(edge.first, edge.second);
graphMatrix.addEdge(edge.first, edge.second);
}
graphList.printGraph();
graphMatrix.printGraph();
// Compare space complexity
cout << "Space Analysis:" << endl;
cout << "Adjacency List: O(V + E) = O(" << graphList.getVertices() << " + " << edges.size() << ") = O(" << (graphList.getVertices() + edges.size()) << ")" << endl;
cout << "Adjacency Matrix: O(VΒ²) = O(" << graphMatrix.getVertices() << "Β²) = O(" << (graphMatrix.getVertices() * graphMatrix.getVertices()) << ")" << endl;
return 0;
}Choose adjacency list for sparse graphs and adjacency matrix for dense graphs or frequent edge queries.
Dijkstra's Shortest Path Algorithm
Dijkstra's algorithm finds the shortest path from a source vertex to all other vertices in a weighted graph with non-negative edge weights. It uses a greedy approach with a priority queue.
Dijkstra's Algorithm Implementation
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;
class DijkstraGraph {
private:
int V;
vector<vector<pair<int, int>>> adj; // {neighbor, weight}
public:
DijkstraGraph(int vertices) : V(vertices) {
adj.resize(V);
}
void addEdge(int u, int v, int weight) {
adj[u].push_back({v, weight});
adj[v].push_back({u, weight}); // For undirected graph
}
vector<int> dijkstra(int src) {
// Distance vector initialized to infinity
vector<int> dist(V, INT_MAX);
vector<int> parent(V, -1);
// Priority queue: {distance, vertex}
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
dist[src] = 0;
pq.push({0, src});
cout << "Dijkstra's algorithm execution:" << endl;
cout << "Step | Current | Distance | Neighbors Updated" << endl;
cout << "-----|---------|----------|------------------" << endl;
int step = 1;
while (!pq.empty()) {
int currentDist = pq.top().first;
int u = pq.top().second;
pq.pop();
// Skip if we've already found a shorter path
if (currentDist > dist[u]) continue;
cout << " " << step++ << " | " << u << " | " << currentDist << " | ";
bool updated = false;
for (auto& edge : adj[u]) {
int v = edge.first;
int weight = edge.second;
if (dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
parent[v] = u;
pq.push({dist[v], v});
cout << v << "(" << dist[v] << ") ";
updated = true;
}
}
if (!updated) cout << "none";
cout << endl;
}
return dist;
}
void printShortestPaths(int src, const vector<int>& dist) {
cout << "\nShortest distances from vertex " << src << ":" << endl;
cout << "Vertex | Distance | Path" << endl;
cout << "-------|----------|-----" << endl;
for (int i = 0; i < V; i++) {
cout << " " << i << " | ";
if (dist[i] == INT_MAX) {
cout << "β" << " | No path" << endl;
} else {
cout << dist[i] << " | ";
printPath(src, i, dist);
cout << endl;
}
}
}
private:
void printPath(int src, int dest, const vector<int>& dist) {
if (src == dest) {
cout << src;
return;
}
// Reconstruct path (simplified version)
cout << src << " -> ... -> " << dest;
}
};
// Weighted graph for testing
class WeightedGraphExample {
public:
static void demonstrateDijkstra() {
DijkstraGraph g(6);
// Add weighted edges
g.addEdge(0, 1, 4);
g.addEdge(0, 2, 2);
g.addEdge(1, 2, 1);
g.addEdge(1, 3, 5);
g.addEdge(2, 3, 8);
g.addEdge(2, 4, 10);
g.addEdge(3, 4, 2);
g.addEdge(3, 5, 6);
g.addEdge(4, 5, 3);
cout << "Graph edges with weights:" << endl;
cout << "0-1: 4, 0-2: 2, 1-2: 1, 1-3: 5, 2-3: 8," << endl;
cout << "2-4: 10, 3-4: 2, 3-5: 6, 4-5: 3" << endl << endl;
vector<int> distances = g.dijkstra(0);
g.printShortestPaths(0, distances);
}
};
int main() {
WeightedGraphExample::demonstrateDijkstra();
return 0;
}Dijkstra maintains a priority queue of vertices ordered by their current shortest distance.
Topological Sorting
Topological sorting produces a linear ordering of vertices in a directed acyclic graph (DAG) such that for every directed edge (u,v), vertex u comes before v in the ordering. It's useful for scheduling tasks with dependencies.
Topological Sort (Kahn's Algorithm)
#include <iostream>
#include <vector>
#include <queue>
#include <string>
using namespace std;
class TopologicalSort {
private:
int V;
vector<vector<int>> adj;
vector<string> vertexNames;
public:
TopologicalSort(int vertices) : V(vertices) {
adj.resize(V);
vertexNames.resize(V);
}
void setVertexName(int v, const string& name) {
vertexNames[v] = name;
}
void addEdge(int u, int v) {
adj[u].push_back(v); // Directed edge from u to v
}
// Kahn's Algorithm for Topological Sort
vector<int> topologicalSortKahn() {
vector<int> inDegree(V, 0);
// Calculate in-degrees
for (int u = 0; u < V; u++) {
for (int v : adj[u]) {
inDegree[v]++;
}
}
// Queue for vertices with 0 in-degree
queue<int> q;
for (int i = 0; i < V; i++) {
if (inDegree[i] == 0) {
q.push(i);
}
}
vector<int> topOrder;
cout << "Kahn's Algorithm execution:" << endl;
cout << "Step | Removed | Queue Status | In-degrees Updated" << endl;
cout << "-----|---------|-------------|-------------------" << endl;
int step = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
topOrder.push_back(u);
cout << " " << step++ << " | " << vertexNames[u] << " | ";
// Reduce in-degree for all neighbors
for (int v : adj[u]) {
inDegree[v]--;
if (inDegree[v] == 0) {
q.push(v);
}
}
// Print current queue status
queue<int> tempQ = q;
cout << "[";
bool first = true;
while (!tempQ.empty()) {
if (!first) cout << ",";
cout << vertexNames[tempQ.front()];
tempQ.pop();
first = false;
}
cout << "] | ";
// Print neighbors updated
for (int v : adj[u]) {
cout << vertexNames[v] << "(" << inDegree[v] << ") ";
}
cout << endl;
}
return topOrder;
}
// DFS-based Topological Sort
vector<int> topologicalSortDFS() {
vector<bool> visited(V, false);
vector<int> topoOrder;
for (int i = 0; i < V; i++) {
if (!visited[i]) {
topologicalSortDFSUtil(i, visited, topoOrder);
}
}
// Reverse the order
reverse(topoOrder.begin(), topoOrder.end());
return topoOrder;
}
private:
void topologicalSortDFSUtil(int v, vector<bool>& visited, vector<int>& topoOrder) {
visited[v] = true;
for (int u : adj[v]) {
if (!visited[u]) {
topologicalSortDFSUtil(u, visited, topoOrder);
}
}
topoOrder.push_back(v);
}
public:
void printTopologicalOrder(const vector<int>& topOrder, const string& method) {
cout << "\nTopological ordering (" << method << "):" << endl;
for (int i = 0; i < topOrder.size(); i++) {
cout << vertexNames[topOrder[i]];
if (i < topOrder.size() - 1) cout << " -> ";
}
cout << endl;
}
bool hasCycle() {
vector<int> topOrder = topologicalSortKahn();
return topOrder.size() != V; // If not all vertices are included, there's a cycle
}
};
// Course scheduling example
class CourseScheduler {
public:
static void demonstrateTopologicalSort() {
// Create a course dependency graph
TopologicalSort courses(8);
// Set course names
courses.setVertexName(0, "Math");
courses.setVertexName(1, "Physics");
courses.setVertexName(2, "Chemistry");
courses.setVertexName(3, "Biology");
courses.setVertexName(4, "AdvMath");
courses.setVertexName(5, "AdvPhys");
courses.setVertexName(6, "Research");
courses.setVertexName(7, "Thesis");
// Add prerequisites (dependencies)
courses.addEdge(0, 4); // Math -> AdvMath
courses.addEdge(1, 5); // Physics -> AdvPhys
courses.addEdge(0, 1); // Math -> Physics
courses.addEdge(2, 3); // Chemistry -> Biology
courses.addEdge(4, 6); // AdvMath -> Research
courses.addEdge(5, 6); // AdvPhys -> Research
courses.addEdge(3, 6); // Biology -> Research
courses.addEdge(6, 7); // Research -> Thesis
cout << "Course Prerequisites:" << endl;
cout << "Math -> Physics, AdvMath" << endl;
cout << "Physics -> AdvPhys" << endl;
cout << "Chemistry -> Biology" << endl;
cout << "AdvMath, AdvPhys, Biology -> Research" << endl;
cout << "Research -> Thesis" << endl << endl;
vector<int> kahnOrder = courses.topologicalSortKahn();
courses.printTopologicalOrder(kahnOrder, "Kahn's Algorithm");
vector<int> dfsOrder = courses.topologicalSortDFS();
courses.printTopologicalOrder(dfsOrder, "DFS-based");
}
};
int main() {
CourseScheduler::demonstrateTopologicalSort();
return 0;
}Kahn's algorithm removes vertices with zero in-degree iteratively, while DFS-based approach uses post-order traversal.