Adjacency Matrix
Learn how graphs can be represented in code using adjacency matrices.
We'll cover the following
Let’s check out how to implement data structures to represent graphs. We’ll work with the following example graph:
Representing the vertices
When discussing graphs, we usually think of the vertices as a set . In the example graph, the set of vertices is .
But when working with graphs in code, it is more convenient to assign increasing integer IDs to the nodes so that information about them can be stored efficiently in an array-like data structure, such as a C++ vector
.
In our example graph, we could map the node names to integer IDs like this:
The transformed graph is
If a graph has nodes, the node IDs are integers . That means that we actually only need the single number to represent the set of nodes.
All of our graph algorithms will work on these integer node IDs. If the original names of the nodes () are still needed in the application, they can be stored for future reference.
vector<string> nodeNames = {"a", "b", "c", "d"};
map<string, int> nameToId = {{"a", 0}, {"b", 1}, {"c", 2}, {"d", 3}};
Adjacency matrix
Now that we’ve got the nodes covered, let’s look into data structures for the edges. There are two common ways to represent them: adjacency matrices and adjacency lists. Recall that two vertices are called adjacent if they are connected by an edge. In this lesson, we’ll check out the adjacency matrix data structure.
The adjacency matrix is a square matrix with rows and columns. The entries of the matrix are binary, either or . The entry in the -th row and the -th column is when the nodes and are connected by an edge, otherwise, it is .
Since our nodes are represented by integers starting with , our matrix is also -indexed: is the top left entry, and is the bottom right entry.
Let’s look again at our example graph from above.
Its adjacency matrix is
For example, the entry is , because there is an edge from node to node in our graph. On the other hand, the entry is , because there is no edge from node to node in our graph.
Since we only consider simple graphs without self-loops, the matrix’s main diagonal is always filled with zeros.
In an undirected graph, the adjacency matrix will always be symmetrical:
This is due to the symmetry condition on the edge set.
Implementing the adjacency matrix
In code, the adjacency matrix can be implemented naively using vectors
of integers.
#include <vector>#include <iostream>using namespace std;int main() {int n = 4;vector<vector<int>> adjacencyMatrix(n, vector<int>(n, 0)); // initialize with zeros// add edges using a loop over a vector of (source, target) pairsvector<pair<int, int>> edges {{0, 1}, {1, 0}, {1, 2}, {1, 3}, {2, 0}, {2, 3}};for (auto [u, v] : edges) {adjacencyMatrix[u][v] = 1;}cout << "Edge from 0 to 1: " << adjacencyMatrix[0][1] << endl;cout << "No edge from 0 to 3: " << adjacencyMatrix[0][3] << endl;}
Some optimizations are possible in this implementation:
- Instead of a nested
vector
ofvectors
, we can use a singlevector
with entries, where is stored at index . - Instead of a
vector<int>
, we can use avector<bool>
, which stores only one bit per entry.
When going with these optimizations, it is best to write a wrapper class that handles the indexing and also hides the
vector<bool>
object from the outside because it does not conform to the STL container interface.
#include <vector>#include <iostream>using namespace std;class AdjacencyMatrix {private:vector<bool> adj;int n;public:AdjacencyMatrix(int _n): n {_n}, adj {vector<bool>(_n*_n, 0)}{}void addEdge(int source, int target) {this->adj[this->n * source + target] = true;}void removeEdge(int source, int target) {this->adj[this->n * source + target] = false;}bool getEdge(int source, int target) {return this->adj[this->n * source + target];}};int main() {int n = 4;AdjacencyMatrix adjacencyMatrix(n); // create empty matrix// add edges using a loop over a vector of (source, target) pairsvector<pair<int, int>> edges {{0, 1}, {1, 0}, {1, 2}, {1, 3}, {2, 0}, {2, 3}};for (auto [u, v] : edges) {adjacencyMatrix.addEdge(u, v);}cout << boolalpha; // print booleans as true / falsecout << "Edge from 0 to 1: " << adjacencyMatrix.getEdge(0, 1) << endl;cout << "No edge from 0 to 3: " << adjacencyMatrix.getEdge(0, 3) << endl;}
In the above code snippet, we implemented an AdjacencyMatrix
class, which uses these optimizations. The interface of the class consists of the functions getEdge
, addEdge
, and removeEdge
that can be used to check and modify the edges. The actual representation of the adjacency matrix as a vector<bool>
is an implementation detail hidden from the user of the class.