Graphs

Introduction

This is a starter post for an introduction to Graphs.

Imports and Setup

Imports

# python
from __future__ import annotations
from collections import defaultdict
from enum import Enum

# pypi
from attrs import define

Constants

class Color(Enum):
    WHITE = 1
    GRAY = 2
    BLACK = 3

INFINITY = float("inf")

A Vertex Implementation

Note to future self: the default setting for attrs.define makes the object to unhashable, to make it hashable by object identity pass in eq=False otherwise trying to add the object to a set will raise an error.

@define(eq=False)
class Vertex:
    """A single node in a graph

    Args:
     identifier: something to identify the node
     color: the 'discovery' state of the node
     distance: The number of edges to the root
     predecessor: The 'parent' of the node in a tree
    """
    identifier: int
    color: Enum=Color.WHITE
    distance: float=INFINITY
    predecessor: Vertex=None

    def __str__(self) -> str:
        return (f"{self.identifier}: {self.color}, "
                f"distance: {self.distance}, predecessor: {self.predecessor}")

A Graph Implementation

class Graph:
    """A graph implementation

    """
    def __init__(self) -> None:
        self._adjacent = None
        self._vertices = None
        return

Adjacencies

The book uses an adjacency linked list, but I'll assume that there will be only one edge between each pair of vertices and use a dictionary with sets of adjacent nodes instead.

@property
def adjacent(self) -> defaultdict:
    """The dictionary of adjacent vertices"""
    if self._adjacent is None:
        self._adjacent = defaultdict(set)
    return self._adjacent

@adjacent.setter
def adjacent(self, new_adjacent: dict) -> None:
    """Sets the dictionary of adjacent vertices (converting to default dict)

    Note:
     This expects the new_adjacent to be a dict of node: set of nodes
    """
    if type(new_adjacent) is not defaultdict:
        new_new_adjacent = defaultdict(set)
        for key, nodes in new_adjacent.items():
            new_new_adjacent[key] = nodes
        new_adjacent = new_new_adjacent
    self._adjacent = new_adjacent
    return

Vertices

The representation we're using is an adjacency list, but sometimes you need to traverse the vertices. So I'm going to make an alias to the adjacency list keys, in the assumption that every node in the graph is a key in the dict. This might not be true in a directed graph, so I'll have to revisit it later.

@property
def vertices(self) -> set:
    """The vertices in this graph"""
    if self._vertices is None:
        self._vertices = set()
    return self._vertices

Add Item

def add(self, node_1: Vertex, node_2: Vertex) -> None:
    """Add edge 

    Warning:
     This assumes an undirected graph, change it for a directed graph

    Args:
     node_1: node on one end of the edge
     node_2: Node on the other end of the edge
    """
    self.vertices.add(node_1)
    self.vertices.add(node_2)
    self.adjacent[node_1].add(node_2)
    self.adjacent[node_2].add(node_1)
    return

Getitem

def __getitem__(self, key):
    """Get the list from the adjacencies dict

    Args:
     key: vertex whose list we want
    """
    return self.adjacent[key]

Testing

# pypi
from expects import be, contain, equal, expect

# software under test
from bowling.data_structures.graphs import graph
from bowling.data_structures.graphs.graph import Color, Graph, Vertex

v = Vertex(1)
expect(v.color).to(be(Color.WHITE))

g = Graph()
v2 = Vertex(2)
v3 = Vertex(3)
g.add(v, v2)
g.add(v, v3)

expect(g.adjacent.keys()).to(contain(v, v2))
expect(g.adjacent[v]).to(contain(v2))
expect(g[v2]).to(contain(v))
expect(g[v3]).to(contain(v))
expect(g[v3]).not_to(contain(v2))