Basics of Graphs
Graphs are fundamental data structures that model relationships between objects. Understanding graph terminology and representation is essential for solving many algorithmic problems.
Graph Terminology
A graph G = (V, E) consists of a set of vertices (nodes) V and a set of edges E that connect pairs of vertices.
Key Terms:
- Vertex/Node: Individual points in the graph
- Edge: Connection between two vertices
- Degree: Number of edges connected to a vertex
- Path: Sequence of vertices connected by edges
- Cycle: Path that starts and ends at the same vertex
- Connected: Two vertices are connected if a path exists between them
Types of Graphs
Directed vs Undirected
In a directed graph, edges have direction. In an undirected graph, edges are bidirectional.
// Directed graph example
// Edge from A to B doesn't imply edge from B to A
vector<vector<int>> directed_graph(n);
directed_graph[a].push_back(b); // Only a -> b
// Undirected graph example
// Edge between A and B implies both directions
vector<vector<int>> undirected_graph(n);
undirected_graph[a].push_back(b);
undirected_graph[b].push_back(a); // Both a -> b and b -> a
Weighted vs Unweighted
Weighted graphs have values (weights) associated with edges, while unweighted graphs treat all edges equally.
// Unweighted graph
vector<vector<int>> unweighted(n);
// Weighted graph using pairs (neighbor, weight)
vector<vector<pair<int, int>>> weighted(n);
weighted[a].push_back({b, weight});
Graph Representation
Adjacency List (Recommended)
Most efficient for sparse graphs. Each vertex maintains a list of its neighbors.
int n, m; // n vertices, m edges
vector<vector<int>> adj(n + 1); // 1-indexed
// Reading edges
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
adj[a].push_back(b);
adj[b].push_back(a); // For undirected graph
}
// Iterate through neighbors of vertex v
for (int neighbor : adj[v]) {
// Process neighbor
}
Memory Usage:
Adjacency list uses O(V + E) space, making it ideal for sparse graphs commonly found in competitive programming.
Adjacency Matrix
Uses a 2D array where adj[i][j] = 1 if there's an edge from i to j.
int n;
vector<vector<int>> adj_matrix(n + 1, vector<int>(n + 1, 0));
// Add edge
adj_matrix[a][b] = 1;
adj_matrix[b][a] = 1; // For undirected graph
// Check if edge exists
if (adj_matrix[a][b]) {
// Edge exists
}
Space Complexity:
Adjacency matrix uses O(V²) space. Only use for dense graphs or when you need O(1) edge lookup.
Weighted Graph Representation
// Method 1: Vector of pairs
vector<vector<pair<int, int>>> adj(n + 1); // {neighbor, weight}
// Add weighted edge
adj[a].push_back({b, weight});
adj[b].push_back({a, weight}); // For undirected
// Method 2: Separate weight array (if needed)
vector<vector<int>> adj(n + 1);
map<pair<int, int>, int> edge_weight;
adj[a].push_back(b);
edge_weight[{a, b}] = weight;
Common Graph Input Formats
Standard Format
int n, m;
cin >> n >> m; // vertices, edges
vector<vector<int>> adj(n + 1);
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
adj[a].push_back(b);
adj[b].push_back(a);
}
Weighted Graph Input
int n, m;
cin >> n >> m;
vector<vector<pair<int, int>>> adj(n + 1);
for (int i = 0; i < m; i++) {
int a, b, w;
cin >> a >> b >> w;
adj[a].push_back({b, w});
adj[b].push_back({a, w});
}
Grid as Graph
int rows, cols;
cin >> rows >> cols;
vector<string> grid(rows);
for (int i = 0; i < rows; i++) {
cin >> grid[i];
}
// 4-directional movement
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};
bool isValid(int x, int y) {
return x >= 0 && x < rows && y >= 0 && y < cols && grid[x][y] != '#';
}
Basic Graph Properties
// Calculate degree of each vertex
vector<int> degree(n + 1, 0);
for (int v = 1; v <= n; v++) {
degree[v] = adj[v].size();
}
// Check if graph is complete
bool isComplete(int n, vector<vector<int>>& adj) {
for (int v = 1; v <= n; v++) {
if (adj[v].size() != n - 1) return false;
}
return true;
}
// Count edges in undirected graph
int countEdges(vector<vector<int>>& adj) {
int edges = 0;
for (int v = 1; v <= n; v++) {
edges += adj[v].size();
}
return edges / 2; // Each edge counted twice
}
Graph Construction Examples
Building a Tree from Parent Array
vector<int> parent(n + 1);
vector<vector<int>> tree(n + 1);
for (int i = 2; i <= n; i++) {
cin >> parent[i]; // Parent of node i
tree[parent[i]].push_back(i);
tree[i].push_back(parent[i]);
}
Creating Graph from Edge List
struct Edge {
int from, to, weight;
};
vector<Edge> edges;
vector<vector<int>> adj(n + 1);
for (int i = 0; i < m; i++) {
int a, b, w;
cin >> a >> b >> w;
edges.push_back({a, b, w});
adj[a].push_back(b);
adj[b].push_back(a);
}
Practical Tips
Memory Optimization:
- Use adjacency list for sparse graphs (most competitive programming problems)
- Reserve capacity if you know the number of edges:
adj[v].reserve(expected_degree) - Use 0-indexed or 1-indexed consistently throughout your solution
Common Patterns:
- Trees have exactly n-1 edges and no cycles
- In an undirected graph, sum of all degrees = 2 × number of edges
- A graph with n vertices and n or more edges must contain a cycle
- Maximum edges in simple undirected graph: n(n-1)/2
Common Mistakes:
- Forgetting to add both directions for undirected graphs
- Using adjacency matrix for large sparse graphs (memory limit exceeded)
- Mixing 0-indexed and 1-indexed vertex numbering