-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgraph.py
More file actions
480 lines (388 loc) · 14.2 KB
/
Copy pathgraph.py
File metadata and controls
480 lines (388 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
"""
This is a module for working with directed and undirected multigraphs.
"""
# version: 29-01-2015, Paul Bonsma
# version: 01-02-2017, Pieter Bos, Tariq Bontekoe
from typing import List, Union, Set
class GraphError(Exception):
"""
An error that occurs while manipulating a `Graph`
"""
def __init__(self, message: str):
"""
Constructor
:param message: The error message
:type message: str
"""
super(GraphError, self).__init__(message)
class Vertex(object):
"""
`Vertex` objects have a property `graph` pointing to the graph they are part of,
and an attribute `label` which can be anything: it is not used for any methods,
except for `__str__`.
"""
def __init__(self, graph: "Graph", label=None):
"""
Creates a vertex, part of `graph`, with optional label `label`.
(Labels of different vertices may be chosen the same; this does
not influence correctness of the methods, but will make the string
representation of the graph ambiguous.)
:param graph: The graph that this `Vertex` is a part of
:param label: Optional parameter to specify a label for the
"""
if label is None:
label = graph._next_label()
self._graph = graph
self.label = label
self._incidence = {}
self._children = []
def __repr__(self):
"""
A programmer-friendly representation of the vertex.
:return: The string to approximate the constructor arguments of the `Vertex'
"""
return 'Vertex(label={}, #incident={})'.format(self.label, len(self._incidence))
def __str__(self) -> str:
"""
A user-friendly representation of the vertex, that is, its label.
:return: The string representation of the label.
"""
return str(self.label)
def is_adjacent(self, other: "Vertex") -> bool:
"""
Returns True iff `self` is adjacent to `other` vertex.
:param other: The other vertex
"""
return other in self._incidence
def _add_incidence(self, edge: "Edge"):
"""
For internal use only; adds an edge to the incidence map
:param edge: The edge that is used to add the incidence
"""
other = edge.other_end(self)
if other not in self._incidence:
self._incidence[other] = set()
self._incidence[other].add(edge)
def _remove_incidence(self, edge: "Edge"):
other = edge.other_end(self)
if other in self._incidence:
del self._incidence[other]
@property
def graph(self) -> "Graph":
"""
The graph of this vertex
:return: The graph of this vertex
"""
return self._graph
@property
def incidence(self) -> List["Edge"]:
"""
Returns the list of edges incident with the vertex.
:return: The list of edges incident with the vertex
"""
result = set()
for edge_set in self._incidence.values():
result |= edge_set
return list(result)
@property
def neighbours(self) -> List["Vertex"]:
"""
Returns the list of neighbors of the vertex.
"""
return list(self._incidence.keys())
@property
def children(self) -> List["Vertex"]:
return self._children
@property
def degree(self) -> int:
"""
Returns the degree of the vertex
"""
return sum(map(len, self._incidence.values()))
def add_child(self, v: "Vertex"):
self._children.append(v)
class Edge(object):
"""
Edges have properties `tail` and `head` which point to the end vertices
(`Vertex` objects). The order of these matters when the graph is directed.
"""
def __init__(self, tail: Vertex, head: Vertex, weight=None):
"""
Creates an edge between vertices `tail` and `head`
:param tail: In case the graph is directed, this is the tail of the arrow.
:param head: In case the graph is directed, this is the head of the arrow.
:param weight: Optional weight of the vertex, which can be any type, but usually is a number.
"""
if tail.graph != head.graph:
raise GraphError("Can only add edges between vertices of the same graph")
self._tail = tail
self._head = head
self._weight = weight
def __repr__(self):
"""
A programmer-friendly representation of the edge.
:return: The string to approximate the constructor arguments of the `Edge'
"""
return 'Edge(head={}, tail={}, weight={})'.format(self.head.label, self.tail.label, self.weight)
def __str__(self) -> str:
"""
A user friendly representation of this edge
:return: A user friendly representation of this edge
"""
return '({}, {})'.format(str(self.tail), str(self.head))
@property
def tail(self) -> "Vertex":
"""
In case the graph is directed, this represents the tail of the arrow.
:return: The tail of this edge
"""
return self._tail
@property
def head(self) -> "Vertex":
"""
In case the graph is directed, this represents the head of the arrow.
:return: The head of this edge
"""
return self._head
@property
def weight(self):
"""
The weight of this edge, which can also just be used as a generic label.
:return: The weight of this edge
"""
return self._weight
def other_end(self, vertex: Vertex) -> Vertex:
"""
Given one end `vertex` of the edge, this returns
the other end vertex.
:param vertex: One end
:return: The other end
"""
if self.tail == vertex:
return self.head
elif self.head == vertex:
return self.tail
raise GraphError(
'edge.other_end(vertex): vertex must be head or tail of edge')
def incident(self, vertex: Vertex) -> bool:
"""
Returns True iff the edge is incident with the
vertex.
:param vertex: The vertex
:return: Whether the vertex is incident with the edge.
"""
return self.head == vertex or self.tail == vertex
class Graph(object):
def __init__(self, directed: bool, n: int=0, simple: bool=False):
"""
Creates a graph.
:param directed: Whether the graph should behave as a directed graph.
:param simple: Whether the graph should be a simple graph, that is, not have multi-edges or loops.
:param n: Optional, the number of vertices the graph should create immediately
"""
self._v = list()
self._e = list()
self._simple = simple
self._directed = directed
self._next_label_value = 0
self._highest_vertex = 0
for i in range(n):
self.add_vertex(Vertex(self))
def __repr__(self):
"""
A programmer-friendly representation of the Graph.
:return: The string to approximate the constructor arguments of the `Graph'
"""
return 'Graph(directed={}, simple={}, #edges={n_edges}, #vertices={n_vertices})'.format(
self._directed, self._simple, n_edges=len(self._e), n_vertices=len(self._v))
def __str__(self) -> str:
"""
A user-friendly representation of this graph
:return: A textual representation of the vertices and edges of this graph
"""
return 'V=[' + ", ".join(map(str, self._v)) + ']\nE=[' + ", ".join(map(str, self._e)) + ']'
def _next_label(self) -> int:
"""
Generates unique labels for vertices within the graph
:return: A unique label
"""
result = self._next_label_value
self._next_label_value += 1
return result
@property
def simple(self) -> bool:
"""
Whether the graph is a simple graph, that is, it does not have multi-edges or loops.
:return: Whether the graph is simple
"""
return self._simple
@property
def directed(self) -> bool:
"""
Whether the graph behaves as a directed graph
:return: Whether the graph is directed
"""
return self._directed
@property
def vertices(self) -> List["Vertex"]:
"""
:return: The `set` of vertices of the graph
"""
return list(self._v)
@property
def highest_vertex(self) -> int:
return self._highest_vertex
@property
def edges(self) -> List["Edge"]:
"""
:return: The `set` of edges of the graph
"""
return list(self._e)
def __iter__(self):
"""
:return: Returns an iterator for the vertices of the graph
"""
return iter(self._v)
def __len__(self) -> int:
"""
:return: The number of vertices of the graph
"""
return len(self._v)
def add_vertex(self, vertex: "Vertex"):
"""
Add a vertex to the graph.
:param vertex: The vertex to be added.
"""
if vertex.graph != self:
raise GraphError("A vertex must belong to the graph it is added to")
self._v.append(vertex)
def remove_vertex(self, vertex: "Vertex"):
if vertex.graph != self:
raise GraphError("A vertex must belong to the graph it is removed from")
if vertex.degree > 0:
for edge in vertex.incidence:
self.remove_edge(edge)
self._v.remove(vertex)
def add_edge(self, edge: "Edge"):
"""
Add an edge to the graph. And if necessary also the vertices.
Includes some checks in case the graph should stay simple.
:param edge: The edge to be added
"""
if self._simple:
if edge.tail == edge.head:
raise GraphError('No loops allowed in simple graphs')
if self.is_adjacent(edge.tail, edge.head):
raise GraphError('No multiedges allowed in simple graphs')
if edge.tail not in self._v:
self.add_vertex(edge.tail)
if edge.head not in self._v:
self.add_vertex(edge.head)
self._e.append(edge)
edge.head._add_incidence(edge)
edge.tail._add_incidence(edge)
def remove_edge(self, edge: Edge):
if edge.head.graph != self or edge.tail.graph != self:
raise GraphError('The edge must belong to the graph it is removed from')
edge.head._remove_incidence(edge)
edge.tail._remove_incidence(edge)
self._e.remove(edge)
def increase_highest_vertex(self):
self._highest_vertex += 1
def __add__(self, other: "Graph") -> "Graph":
"""
Make a disjoint union of two graphs.
:param other: Graph to add to `self'.
:return: New graph which is a disjoint union of `self' and `other'.
"""
I = Graph(directed=False)
vertex_dict = {}
for vertex in self.vertices:
new_vertex = Vertex(I)
vertex_dict[vertex] = new_vertex
for vertex in other.vertices:
new_vertex = Vertex(I)
vertex_dict[vertex] = new_vertex
for edge in self.edges:
new_edge = Edge(vertex_dict[edge.tail], vertex_dict[edge.head])
I.add_edge(new_edge)
for edge in other.edges:
new_edge = Edge(vertex_dict[edge.tail], vertex_dict[edge.head])
I.add_edge(new_edge)
return I
def __iadd__(self, other: Union[Edge, Vertex]) -> "Graph":
"""
Add either an `Edge` or `Vertex` with the += syntax.
:param other: The object to be added
:return: The modified graph
"""
if isinstance(other, Vertex):
self.add_vertex(other)
if isinstance(other, Edge):
self.add_edge(other)
return self
def find_edge(self, u: "Vertex", v: "Vertex") -> Set["Edge"]:
"""
Tries to find edges between two vertices.
:param u: One vertex
:param v: The other vertex
:return: The set of edges incident with both `u` and `v`
"""
result = u._incidence.get(v, set())
if not self._directed:
result |= v._incidence.get(u, set())
return set(result)
def is_adjacent(self, u: "Vertex", v: "Vertex") -> bool:
"""
Returns True iff vertices `u` and `v` are adjacent. If the graph is directed, the direction of the edges is
respected.
:param u: One vertex
:param v: The other vertex
:return: Whether the vertices are adjacent
"""
return v in u.neighbours and (not self.directed or any(e.head == v for e in u.incidence))
def highest_vertex_number(self):
labels = []
for vertex in self.vertices:
labels.append(vertex.label)
self._highest_vertex = max(labels)
def deep_copy(self):
new_graph = Graph(directed=self.directed, simple=self.simple)
new_graph._highest_vertex = self.highest_vertex
vertex_dict = {}
for vertex in self.vertices:
v = Vertex(new_graph, vertex.label)
vertex_dict[vertex] = v
new_graph.add_vertex(v)
for edge in self.edges:
tail = vertex_dict[edge.tail]
head = vertex_dict[edge.head]
new_graph.add_edge(Edge(tail, head))
return new_graph
class UnsafeGraph(Graph):
@property
def vertices(self) -> List["Vertex"]:
return self._v
@property
def edges(self) -> List["Edge"]:
return self._e
def add_vertex(self, vertex: "Vertex"):
self._v.append(vertex)
def add_edge(self, edge: "Edge"):
self._e.append(edge)
edge.head._add_incidence(edge)
edge.tail._add_incidence(edge)
def find_edge(self, u: "Vertex", v: "Vertex") -> Set["Edge"]:
left = u._incidence.get(v, None)
right = None
if not self._directed:
right = v._incidence.get(u, None)
if left is None and right is None:
return set()
if left is None:
return right
if right is None:
return left
return left | right
def is_adjacent(self, u: "Vertex", v: "Vertex") -> bool:
return v in u._incidence or (not self._directed and u in v._incidence)