313 lines
10 KiB
Python
313 lines
10 KiB
Python
""" Python Classes for Oriented and Non Oriented Graphs
|
|
"""
|
|
|
|
from graphviz import Digraph # for dot output
|
|
from typing import List, Dict, Set, Tuple, Any
|
|
|
|
|
|
class GraphError(Exception):
|
|
"""Exception raised for self loops.
|
|
"""
|
|
|
|
message: str
|
|
|
|
def __init__(self, message: str):
|
|
self.message = message
|
|
|
|
|
|
class GeneralGraph(object):
|
|
"""
|
|
General class regrouping similarities
|
|
between directed and non oriented graphs.
|
|
The only differences between the two are:
|
|
|
|
- how to compute the set of edges
|
|
- how to add an edge
|
|
- how to print the graph
|
|
- how to delete a vertex
|
|
- how to delete an edge
|
|
- we only color undirected graphs
|
|
"""
|
|
|
|
graph_dict: Dict[Any, Set]
|
|
|
|
def __init__(self, graph_dict=None):
|
|
"""
|
|
Initializes a graph object.
|
|
If no dictionary or None is given,
|
|
an empty dictionary will be used.
|
|
"""
|
|
if graph_dict is None:
|
|
graph_dict = {}
|
|
self.graph_dict = graph_dict
|
|
|
|
def vertices(self) -> List[Any]:
|
|
"""Return the vertices of a graph."""
|
|
return list(self.graph_dict.keys())
|
|
|
|
def add_vertex(self, vertex: Any) -> None:
|
|
"""
|
|
If the vertex "vertex" is not in
|
|
self.graph_dict, a key "vertex" with an empty
|
|
list as a value is added to the dictionary.
|
|
Otherwise nothing has to be done.
|
|
"""
|
|
if vertex not in self.graph_dict:
|
|
self.graph_dict[vertex] = set()
|
|
|
|
def edges(self) -> List[Set]:
|
|
"""Return the edges of the graph."""
|
|
return []
|
|
|
|
def __str__(self):
|
|
res = "vertices: "
|
|
for k in self.graph_dict:
|
|
res += str(k) + " "
|
|
res += "\nedges: "
|
|
for edge in self.edges():
|
|
res += str(edge) + " "
|
|
return res
|
|
|
|
def dfs_traversal(self, root: Any) -> List[Any]:
|
|
"""
|
|
Compute a depth first search of the graph,
|
|
from the vertex root.
|
|
"""
|
|
seen: List[Any] = []
|
|
todo: List[Any] = [root]
|
|
while len(todo) > 0: # while todo ...
|
|
current = todo.pop()
|
|
seen.append(current)
|
|
for neighbour in self.graph_dict[current]:
|
|
if neighbour not in seen:
|
|
todo.append(neighbour)
|
|
return seen
|
|
|
|
def is_reachable_from(self, v1: Any, v2: Any) -> bool:
|
|
"""True if there is a path from v1 to v2."""
|
|
return v2 in self.dfs_traversal(v1)
|
|
|
|
def connected_components(self) -> List[List[Any]]:
|
|
"""
|
|
Compute the list of all connected components of the graph,
|
|
each component being a list of vetices.
|
|
"""
|
|
components: List[List[Any]] = []
|
|
done: List[Any] = []
|
|
for v in self.vertices():
|
|
if v not in done:
|
|
v_comp = self.dfs_traversal(v)
|
|
components.append(v_comp)
|
|
done.extend(v_comp)
|
|
return components
|
|
|
|
def bfs_traversal(self, root: Any) -> List[Any]:
|
|
"""
|
|
Compute a breadth first search of the graph,
|
|
from the vertex root.
|
|
"""
|
|
seen: List[Any] = []
|
|
todo: List[Any] = [root]
|
|
while len(todo) > 0: # while todo ...
|
|
current = todo.pop(0) # list.pop(0): for dequeuing (on the left...) !
|
|
seen.append(current)
|
|
for neighbour in self.graph_dict[current]:
|
|
if neighbour not in seen:
|
|
todo.append(neighbour)
|
|
return seen
|
|
|
|
|
|
class Graph(GeneralGraph):
|
|
"""Class for non oriented graphs."""
|
|
|
|
def edges(self) -> List[Set]:
|
|
"""
|
|
A static method generating the set of edges
|
|
(they appear twice in the dictionnary).
|
|
Return a list of sets.
|
|
"""
|
|
edges = []
|
|
for vertex in self.graph_dict:
|
|
for neighbour in self.graph_dict[vertex]:
|
|
if {neighbour, vertex} not in edges:
|
|
edges.append({vertex, neighbour})
|
|
return edges
|
|
|
|
def add_edge(self, edge: Tuple[Any, Any]) -> None:
|
|
"""
|
|
Add an edge in the graph.
|
|
edge should be a pair and not (c,c)
|
|
(we call g.add_edge((v1,v2)))
|
|
"""
|
|
(vertex1, vertex2) = edge
|
|
if vertex1 == vertex2:
|
|
raise GraphError("Cannot add a self loop on vertex {} in an unoriented graph.".format(
|
|
str(vertex1)))
|
|
if vertex1 in self.graph_dict:
|
|
self.graph_dict[vertex1].add(vertex2)
|
|
else:
|
|
self.graph_dict[vertex1] = {vertex2}
|
|
if vertex2 in self.graph_dict:
|
|
self.graph_dict[vertex2].add(vertex1)
|
|
else:
|
|
self.graph_dict[vertex2] = {vertex1}
|
|
|
|
def print_dot(self, name: str, colors={}) -> None:
|
|
"""Print the graph."""
|
|
color_names = ['red', 'blue', 'green', 'yellow', 'cyan', 'magenta'] + \
|
|
[f"grey{i}" for i in range(0, 100, 10)]
|
|
color_shapes = ['ellipse', 'box', 'diamond', 'trapezium', 'egg',
|
|
'parallelogram', 'house', 'triangle', 'pentagon', 'hexagon',
|
|
'septagon', 'octagon']
|
|
dot = Digraph(comment='Conflict Graph')
|
|
for k in self.graph_dict:
|
|
shape = None
|
|
if not colors:
|
|
color = "red" # Graph not colored: red for everyone
|
|
elif k not in colors:
|
|
color = "grey" # Node not colored: grey
|
|
else:
|
|
n = colors[k]
|
|
if n < len(color_names):
|
|
color = color_names[colors[k]]
|
|
else:
|
|
color = "black" # Too many colors anyway, it won't be readable.
|
|
shape = color_shapes[n % len(color_shapes)]
|
|
dot.node(str(k), color=color, shape=shape)
|
|
for (v1, v2) in self.edges():
|
|
dot.edge(str(v1), str(v2), dir="none")
|
|
# print(dot.source)
|
|
dot.render(name, view=True) # print in pdf
|
|
|
|
def delete_vertex(self, vertex: Any) -> None:
|
|
"""Delete a vertex and all the adjacent edges."""
|
|
gdict = self.graph_dict
|
|
for neighbour in gdict[vertex]:
|
|
gdict[neighbour].remove(vertex)
|
|
del gdict[vertex]
|
|
|
|
def delete_edge(self, edge: Tuple[Any, Any]):
|
|
"""Delete an edge."""
|
|
(v1, v2) = edge
|
|
self.graph_dict[v1].remove(v2)
|
|
self.graph_dict[v2].remove(v1)
|
|
|
|
def color(self) -> Dict[Any, int]:
|
|
"""
|
|
Color the graph with an unlimited number of colors.
|
|
Return a dict vertex -> color, where color is an integer (0, 1, ...).
|
|
"""
|
|
coloring, _, _ = self.color_with_k_colors()
|
|
return coloring
|
|
|
|
# see algo of the course
|
|
def color_with_k_colors(self, K=None, avoidingnodes=()) -> Tuple[Dict[Any, int], bool, List]:
|
|
"""
|
|
Color with <= K colors (if K is unspecified, use unlimited colors).
|
|
|
|
Return 3 values:
|
|
|
|
- a dict vertex -> color
|
|
- a Boolean, True if the coloring succeeded
|
|
- the set of nodes actually colored
|
|
|
|
Do not color vertices belonging to avoidingnodes.
|
|
|
|
Continue even if the algo fails.
|
|
"""
|
|
if K is None:
|
|
K = len(self.graph_dict)
|
|
todo_vertices = []
|
|
is_total = True
|
|
gcopy = Graph(self.graph_dict.copy())
|
|
# suppress nodes that are not to be considered.
|
|
for node in avoidingnodes:
|
|
gcopy.delete_vertex(node)
|
|
# append nodes in the list according to their degree and node number:
|
|
while gcopy.graph_dict:
|
|
todo = list(gcopy.graph_dict)
|
|
todo.sort(key=lambda v: (len(gcopy.graph_dict[v]), str(v)))
|
|
lower = todo[0]
|
|
todo_vertices.append(lower)
|
|
gcopy.delete_vertex(lower)
|
|
# Now reverse the list: first elements are those with higher degree
|
|
# print(todo_vertices)
|
|
todo_vertices.reverse() # in place reversal
|
|
# print(todo_vertices)
|
|
coloring = {}
|
|
colored_nodes = []
|
|
# gdict will be the coloring map to return
|
|
gdict = self.graph_dict
|
|
for v in todo_vertices:
|
|
seen_neighbours = [x for x in gdict[v] if x in coloring]
|
|
choose_among = [i for i in range(K) if not (
|
|
i in [coloring[v1] for v1 in seen_neighbours])]
|
|
if choose_among:
|
|
# if the node can be colored, I choose the minimal color.
|
|
color = min(choose_among)
|
|
coloring[v] = color
|
|
colored_nodes.append(v)
|
|
else:
|
|
# if I cannot color some node, the coloring is not Total
|
|
# but I continue
|
|
is_total = False
|
|
return (coloring, is_total, colored_nodes)
|
|
|
|
|
|
class DiGraph(GeneralGraph):
|
|
"""Class for directed graphs."""
|
|
|
|
def pred(self, v: Any) -> Set:
|
|
"""Return all predecessors of the vertex `v` in the graph."""
|
|
return {src for src, dests in self.graph_dict.items() if v in dests}
|
|
|
|
def neighbourhoods(self) -> List[Tuple[Any, Set]]:
|
|
"""Return all neighbourhoods in the graph."""
|
|
return list(self.graph_dict.items())
|
|
|
|
def edges(self) -> List[Set]:
|
|
""" A static method generating the set of edges"""
|
|
edges = []
|
|
for vertex in self.graph_dict:
|
|
for neighbour in self.graph_dict[vertex]:
|
|
edges.append((vertex, neighbour))
|
|
return edges
|
|
|
|
def add_edge(self, edge: Tuple[Any, Any]) -> None:
|
|
"""
|
|
Add an edge in the graph.
|
|
edge should be a pair and not (c,c)
|
|
(we call g.add_edge((v1,v2)))
|
|
"""
|
|
(vertex1, vertex2) = edge
|
|
if vertex1 in self.graph_dict:
|
|
self.graph_dict[vertex1].add(vertex2)
|
|
else:
|
|
self.graph_dict[vertex1] = {vertex2}
|
|
if vertex2 not in self.graph_dict:
|
|
self.graph_dict[vertex2] = set()
|
|
|
|
def print_dot(self, name: str) -> None:
|
|
"""Print the graph."""
|
|
dot = Digraph(comment='Conflict Graph')
|
|
for k in self.graph_dict:
|
|
shape = None
|
|
color = "grey"
|
|
dot.node(str(k), color=color, shape=shape)
|
|
for (v1, v2) in self.edges():
|
|
dot.edge(str(v1), str(v2), dir="none")
|
|
# print(dot.source)
|
|
dot.render(name, view=True) # print in pdf
|
|
|
|
def delete_vertex(self, vertex: Any) -> None:
|
|
"""Delete a vertex and all the adjacent edges."""
|
|
for node, neighbours in self.graph_dict.items():
|
|
if vertex in neighbours:
|
|
neighbours.remove(vertex)
|
|
del self.graph_dict[vertex]
|
|
|
|
def delete_edge(self, edge: Tuple[Any, Any]) -> None:
|
|
"""Delete an edge."""
|
|
(v1, v2) = edge
|
|
self.graph_dict[v1].remove(v2)
|