Undirected Graphs

Updated: 03 January 2025

Notes based on Algorithms, Part 2 on Coursera

Undirected Graphs

A graph is a set of vertices connected pairwise by edges. Graphs can be huge and have complex properties

Terminology

  • Path: Sequence of vertices conencted by edges
  • Cycle: Path whose first and last vertices are the same

Common Graph Problems

  • Path: Is there a path between s and t
  • Shortest path: what is the shortest path between s and t
  • Cycle: Is there a cycle
  • Euler tour: Is there a cycle that visits each edge exactly once
  • Hamilton tour: Is there a cycle that uses each vertex exactly once
  • Connectivity: Is there a way to connect all vertices
  • MST: What is the best way to connect all of the vertices
  • Biconnectivity: Is there a vertex whose removal disconnects the graph
  • Planarity: Can you draw the graph on a plane without crossing edges
  • Graph Isomorphishm: Do two adjancey lists represent the same graph

One of the biggest challenges is to determine the difficulty of these problems

Graph API

In general, we can define the Graph API as follows:

algorithms/graphs/definition.ts
1
export interface Graph {
2
/**
3
* Add an edge v-w
4
*/
5
addEdge(v: number, w: number): void
6
7
/**
8
* Vertices adjacent to v
9
*/
10
adj(v:number): Iterable<number>
11
12
/**
13
* Number of vertices
14
*/
15
V:number
16
17
/**
18
* Number of edges
19
*/
20
E:number
21
}

Representations

There are different ways we can represent graphs

Drawing

We can represent graphs by drawing them, this can provide intuition about the structure but can be misleading

List of Vertices

We can also represent graphs using a list of vertices, different representations may impose challenges in representing self-loops or parallel edges

List of Edges

Maintaining a list of edge-pairs in a Set/Linked List/Array/etc.

Adjacency Matrix

Maintain a two dimensional V-by-V boolean array, for each edge v-w we set adj[v][w] = adj[w][v] = true Which is a very efficient method for lookups but is very poor spacially

Adjacency List

Maintain a vertex-indexed array of lists, this can be any Bag implementation in general

A simple implementation of an adjacency list using a bag can be seen below

algorithms/graphs/bag-graph.ts
1
import type { Graph } from "./definition"
2
import { LinkedListBag } from '../iterator/linked-list'
3
import type { Bag } from '../iterator/bag'
4
5
6
/**
7
* Adjacency list implementation of a Graph using a Bag
8
*/
9
export class BagGraph implements Graph {
10
#adj: Bag<number>[]
11
12
V: number
13
E: number = 0
14
15
constructor(V: number) {
16
this.V = V
17
this.#adj = new Array(V)
18
19
for (let v = 0; v < V; v++) {
20
this.#adj[v] = new LinkedListBag()
21
}
22
}
23
24
addEdge(v: number, w: number): void {
25
this.#adj[v].add(w)
26
this.#adj[w].add(v)
27
28
this.E++
29
}
30
31
adj(v: number): Iterable<number> {
32
return this.#adj[v]
33
}
34
}

The adjacency mechanism works well since in real world applications graphs tend to have a lot of vertices but a small average number of edges per vertex

Performance Characteristics

RepresentationSpaceAdd EdgeEdge Between V and WIterate over Vertices
List of EdgesEIEE
Adjacency MatrixV^2IIV
Adjacency ListE + VIdegree(V)degree(V)

Depth First Search

Maze Exploration

We can search for a path though a maze using a Depth first search such that:

  • Vertex = Intersection
  • Edge = Passage

Tremaux Maze Exploration

An early algorithm for exploring a maze is the Tremaux Maze Exploration Algorithm which goes as follows:

  • Unroll a ball of string behind you
  • Mark each visited intersection and visited passage
  • Retrace steps when no unvisited options exist

Algorithm

To find a path from one vertex to another

  1. Mark current vertex as visited
  2. Recursively visit all unmarked vetices adjacent to current vertex

Applications

  • Find all vertices connected to a given source
  • Find a path between two vertices

Graph Processing Pattern

We can implement this using the following design pattern by decoupling the Graph Data type from the processing of the graph itself

  1. Create a graph object
  2. Pass the graph to the processing method
  3. Query the graph processing method for information

We can decopuple this by creating a Paths type that contains the logic of holding a Graph and a source vertex. The API of Paths can look like so:

algorithms/graphs/path-definition.ts
1
export interface Paths {
2
/**
3
* Does a path exist from source to v
4
*/
5
hasPathTo(v: number): boolean
6
7
/**
8
* Get the path from the start of the graph to v
9
*/
10
pathTo(v: number): Iterable<number> | undefined
11
}

Implementation

We can implement DFS using our pattern as seen below:

algorithms/graphs/dfs.ts
1
import type { Paths } from './path-definition'
2
import type { Graph } from './definition'
3
import { LinkedListStack } from '../stack/linked-list'
4
5
export class DFS implements Paths {
6
#s: number
7
#marked: boolean[]
8
#edgeTo: number[]
9
10
constructor(
11
graph: Graph,
12
source: number
13
) {
14
this.#s = source
15
this.#marked = new Array(graph.V).fill(false)
16
this.#edgeTo = new Array(graph.V)
17
18
this.dfs(graph, source)
19
}
20
21
private dfs(G: Graph, v: number): void {
22
this.#marked[v] = true
23
for (let w of G.adj(v)) {
24
if (!this.#marked[w]) {
25
this.dfs(G, w)
26
this.#edgeTo[w] = v
27
}
28
}
29
}
30
31
hasPathTo(v: number): boolean {
32
return this.#marked[v]
33
}
34
35
pathTo(v: number): Iterable<number> | undefined {
36
if (!this.hasPathTo(v)) {
37
return undefined
38
}
39
40
const path = Array<number>()
41
42
for (let x = v; x !== this.#s; x = this.#edgeTo[x]) {
43
path.push(x)
44
}
45
46
return path
47
}
48
}

The implementation above uses two internal properties - marked to track whether a vertex was visited, and edgeTo to mark where that vertex was reached from

DFS marks all connected vertices in a time proportional to the sum of their degrees

After the DFS is done it becomes possible to find connected vertices in constant time and we can find a path if one exists in a time proportional to its length