Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

Adjacency List Representation in C++

Recommended resources

Introduction

In our last session, we explored the adjacency matrix, a straightforward method for representing graphs. We concluded that its significant space complexity makes it impractical for the large, sparse graphs often encountered in competitive programming. This limitation naturally leads us to seek a more memory-efficient alternative.

Today, we will delve into that alternative: the adjacency list. Your goal is to learn how to represent a graph using an adjacency list in C++, a technique that is arguably the most common and versatile for competitive programming. We will examine its structure, implementation for various graph types, and the crucial performance trade-offs it entails.


The Concept of an Adjacency List

The core idea behind an adjacency list is to store only the information that actually exists. Instead of a massive matrix where most entries might be zero, we maintain an array (or a vector in C++) of lists. Each index i in this array corresponds to vertex i, and the list at that index contains only the vertices that are direct neighbors of i.

This is far more space-efficient. If a vertex has only three neighbors, we store just those three, not an entire row of V elements.

Let's visualize this. The image below shows a simple undirected graph and its corresponding adjacency list. For each vertex (0, 1, 2), there is a linked list containing its neighbors. In a C++ context, we typically use a std::vector instead of a linked list for better performance and ease of use.

An undirected graph where each vertex in the list points to all of its neighbors. Notice that if 0 is a neighbor of 1, then 1 is also a neighbor of 0.

For a directed graph, the representation is similar, but the relationships are one-way. An edge from A to B means B will be in A's list, but A will not necessarily be in B's list.

A directed graph where the list for each vertex contains only its outgoing neighbors. For example, there's an edge from A to B, so B is in A's list.

Implementation in C++

In C++, the most common way to implement an adjacency list is with a vector of vectors: vector<vector<int>> adj;. This creates a dynamic array where adj[i] is itself a vector that will store the neighbors of vertex i.

The take U forward video provides an excellent walkthrough of this concept, tailored specifically for the context of competitive programming.

G-2. Graph Representation in C++ | Two Ways to Represent

This video from the take U forward channel clearly explains the rationale behind adjacency lists and demonstrates their C++ implementation.

First, watch the conceptual introduction from this segment. It explains how an array of lists (vectors) is constructed. Then, see the actual C++ code for an undirected graph from this part. Finally, observe how simple it is to adapt this for a directed graph by watching from this clip.

As explained in the video, for a standard problem input with vertices and edges, we would implement it as follows:

  1. Initialize the main vector: vector<vector<int>> adj(N); (or N+1 for 1-based indexing).
  2. Loop times to read each edge.
  3. For each edge (u, v):
    • If the graph is directed, add v to u's list: adj[u].push_back(v);.
    • If the graph is undirected, add the connection in both directions: adj[u].push_back(v); and adj[v].push_back(u);.

For a clean, written summary and code snippet, the following resource is very helpful.

Graph Input Methods for Competitive Programming | Data Structures ...

This article presents several standard methods for graph input in C++, including the adjacency list.

Please read the section titled "Method 3". This provides a concise C++ function that handles the typical input format for an unweighted graph. Also, look at the code under "Standard Graph Input", which shows the same logic within a common competitive programming template.


Representing Weighted Graphs

Just as with adjacency matrices, we can adapt adjacency lists to handle weighted graphs. Instead of storing just the integer ID of a neighbor, we need to store a pair of values: the neighbor's ID and the weight of the edge connecting to it.

In C++, std::pair<int, int> is perfect for this. Our data structure becomes vector<vector<pair<int, int>>> adj;. Now, adj[u] will be a list of pairs, where each pair is of the form {neighbor, weight}.

The take U forward video explains this extension clearly.

G-2. Graph Representation in C++ | Two Ways to Represent

This part of the video explains how to modify the adjacency list to store edge weights using pairs.

Watch from this section to understand how to use std::pair to incorporate weights into your adjacency list.

The process of building the list is almost identical. When you read an edge (u, v) with weight w, you simply push a pair instead of an integer.

  • For a directed edge: adj[u].push_back({v, w});
  • For an undirected edge: adj[u].push_back({v, w}); and adj[v].push_back({u, w});

This is also covered in the article we looked at earlier.

Graph Input Methods for Competitive Programming | Data Structures ...

This section shows the C++ code for a weighted adjacency list.

Now, read "Method 4". Note how vector<vector<int>> is changed to vector<vector<pair<int, int>>> and how the push_back operation now includes the weight.


An Advanced Aside: Map-based Adjacency Lists

In most contest problems, vertices are conveniently numbered from 0 or 1 up to . This makes them perfect indices for a vector. However, what if the vertex labels were large, non-sequential numbers (e.g., user IDs) or even strings? In such cases, a vector is not a suitable primary container.

Here, you can leverage your knowledge of hash maps. The C++ std::unordered_map can be used to create a map-based adjacency list: unordered_map<int, vector<int>> adj;. The key of the map is the vertex label, and the value is the list of its neighbors. This provides flexibility when dealing with unconventional vertex labeling. The Graph Input Methods article briefly touches on this under "Method 5".


Conclusion

Today, you've learned how to implement the adjacency list, the workhorse of graph representation in competitive programming. Its efficiency with sparse graphs makes it an essential tool in your arsenal.

Key Takeaways:

  • An adjacency list represents a graph as an array of lists, where adj[i] stores only the direct neighbors of vertex i.
  • It is most commonly implemented in C++ using vector<vector<int>> for unweighted graphs and vector<vector<pair<int, int>>> for weighted graphs.
  • The primary advantage is its space efficiency. The space complexity is , which is a significant improvement over the adjacency matrix's for sparse graphs (where ).
  • The adjacency list is optimal for iterating through a vertex's neighbors, which is a fundamental operation in most graph algorithms. This takes time.
  • The main trade-off is that checking for a specific edge (u, v) is slower than with an adjacency matrix, requiring a linear scan of u's neighbor list, taking time.

In our next lesson, we will formalize the comparison between the adjacency matrix and the adjacency list. We'll create a clear summary of their respective time and space complexities for different operations, helping you solidify your understanding of when to choose one over the other.