Skip to content
CCC Python Course

Graphs and adjacency lists

Module
M4.12
Lesson
1 of 1
Reading time
5 min

In this lesson

  • Recognize when a problem has a graph structure (vertices and edges).
  • Represent a graph as an adjacency list and populate it from input.
  • Understand the tradeoff between adjacency lists and adjacency matrices.
  • Iterate over a vertex's neighbours and apply an algorithm to each.

Many problems are really about things connected to other things. People in a social network are connected by friendships. Cities are connected by roads. Pages are connected by links. All of these are graphs: a set of vertexA single node in a graph; the thing that edges connect.In the glossary (the things) and edgeA connection between two vertices in a graph, sometimes carrying a weight.In the glossary (the connections between them).

Vertices and edges

Number the vertices from 0 to N − 1. If you can travel both ways along an edge, the graph is undirected; a social network is usually like this, since a friendship goes both ways. If an edge only lets you travel one direction, the graph is directed, the way a one-way street does.

An edge can also carry a weight, a number attached to it, such as a distance on a map or a closeness score in a social network. This lesson sticks to unweighted graphs, where every edge counts the same.

Adjacency list representation

An adjacency listA way to store a graph as a list or dictionary that maps each vertex to the list of its neighbours.In the glossary is a dictionary where each vertex maps to a list of its neighbours.

Python
graph = {    0: [1, 2],    1: [0, 2],    2: [0, 1, 3],    3: [2]}

Vertex 0 connects to 1 and 2, vertex 1 connects to 0 and 2, and so on. For an undirected graph, every edge appears twice: reading an edge between A and B means storing B in A's list and A in B's list.

An adjacency list uses space proportional to the number of vertices plus the number of edges, so it stays small on a sparse graph, one where most vertices only connect to a few others. That covers most graphs you will meet in a contest.

Building an adjacency list from input

Suppose the input gives the number of vertices and edges, followed by one edge per line:

4 30 11 22 3

That is 4 vertices and 3 edges, connecting 0-1, 1-2 and 2-3 in a line.

examples/build_graph.py
n, m = map(int, input().split())graph = {i: [] for i in range(n)}
for _ in range(m):    a, b = map(int, input().split())    graph[a].append(b)    graph[b].append(a)
for vertex in range(n):    print(f"{vertex}: {graph[vertex]}")

Input

4 3
0 1
1 2
2 3

Output

0: [1]
1: [0, 2]
2: [1, 3]
3: [2]
Building an adjacency list from input

The program starts every vertex with an empty list, then reads each edge and adds the neighbour to both lists, since the graph is undirected. The printed result confirms it: vertex 0 only lists [1], since it has one edge, while vertex 1 sits in the middle of the line and lists both [0, 2].

Iterating over neighbours

Once the graph is built, exploring it means looping over a vertex's list.

Python
graph = {    0: [1, 2],    1: [0, 2],    2: [0, 1, 3],    3: [2]}current = 0for neighbor in graph[current]:    print(f"Vertex {current} connects to {neighbor}")

This loop is the piece every graph search is built from. BFS and DFS both start at a vertex, look at every neighbour in its list, and repeat from each new vertex they discover. Everything past this lesson builds on being able to ask "what does this vertex connect to?" quickly, which is exactly what the adjacency list gives you.

Adjacency matrix versus adjacency list

An adjacency matrix represents the same information as a 2D grid, where matrix[i][j] is true exactly when an edge connects i and j.

Python
matrix = [    [False, True, True, False],    [True, False, True, False],    [True, True, False, True],    [False, False, True, False]]

A matrix answers "does this edge exist?" instantly, in one lookup, but it always uses space proportional to the square of the vertex count, no matter how many edges actually exist. With 1,000 vertices and only 2,000 edges, the matrix has a million cells for 2,000 real connections. An adjacency list uses space proportional to the edges you actually have, which is why it is the default choice unless a problem specifically needs fast edge-existence checks on a small, dense graph.

Recognizing a graph in disguise

The hard part of graph problems is rarely the code, it is noticing that a problem is a graph problem at all. A set of tasks where some depend on others is a graph: vertices are tasks, and an edge points from a dependency to the task waiting on it. Finding a valid order to do them is a search over that graph.

A maze is the same idea with a different name. Each room is a vertex, each passage between rooms is an edge, and finding a way out is a graph search from your starting room. A flight timetable fits the same shape: each city is a vertex, and each flight is a directed edge from its origin to its destination. Asking whether you can fly from one city to another is asking whether a path exists between two vertices, and the departure times can be ignored entirely for that question.

None of these problems mention the word "graph." Once you notice vertices and edges hiding underneath tasks, rooms or flights, building the adjacency list and running a standard search over it is the same routine every time.

Getting these details right

The most common slip is adding only one direction of an undirected edge. Reading an edge between 0 and 1 and storing only 1 in vertex 0's list loses the fact that 0 is also a neighbour of 1; for an undirected graph, every edge needs both directions stored.

The input's numbering is another place to check carefully. Some problems number vertices 1 through N instead of 0 through N − 1. Read the first example before you write any code, and if the input starts at 1, subtract 1 from every vertex you read so your 0-indexed adjacency list lines up.

And pick the container to match the graph. A dictionary of lists, like the one above, is the right default. If the vertices are a small, dense range, such as 0 to 999 with no gaps, a plain list of lists works too and avoids the overhead of a dictionary; but if the vertex numbers are large and sparse, a dictionary is the one that will not waste memory on vertices that never appear.

Practice

Try this on the judge. The link opens the problem on DMOJ.

  1. 2018 J5
    Choose your own path (opens on DMOJ in a new tab) DMOJ

    Model a choose-your-own-adventure book as a graph of pages and links.