Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/graphs/tutte_polynomial.py
8815 views
1
r"""
2
Tutte polynomial
3
4
This module implements a deletion-contraction algorithm for computing
5
the Tutte polynomial as described in the paper [Gordon10]_.
6
7
.. csv-table::
8
:class: contentstable
9
:widths: 30, 70
10
:delim: |
11
12
:func:`tutte_polynomial` | Computes the Tutte polynomial of the input graph
13
14
Authors:
15
16
- Mike Hansen (06-2013), Implemented the algorithm.
17
- Jernej Azarija (06-2013), Tweaked the code, added documentation
18
19
Definition
20
-----------
21
22
Given a graph `G`, with `n` vertices and `m` edges and `k(G)`
23
connected components we define the Tutte polynomial of `G` as
24
25
.. MATH::
26
27
\sum_H (x-1) ^{k(H) - c} (y-1)^{k(H) - |E(H)|-n}
28
29
where the sum ranges over all induced subgraphs `H` of `G`.
30
31
REFERENCES:
32
33
.. [Gordon10] Computing Tutte Polynomials. Gary Haggard, David
34
J. Pearce and Gordon Royle. In ACM Transactions on Mathematical
35
Software, Volume 37(3), article 24, 2010. Preprint:
36
http://homepages.ecs.vuw.ac.nz/~djp/files/TOMS10.pdf
37
38
Functions
39
---------
40
"""
41
42
from contextlib import contextmanager
43
from sage.misc.lazy_attribute import lazy_attribute
44
from sage.graphs.graph import Graph
45
from sage.misc.misc_c import prod
46
from sage.rings.integer_ring import ZZ
47
from sage.misc.decorators import sage_wraps
48
49
######################
50
# Graph Modification #
51
######################
52
53
54
@contextmanager
55
def removed_multiedge(G, edge, multiplicity):
56
r"""
57
A context manager which removes an edge with multiplicity from the
58
graph `G` and restores it upon exiting.
59
60
EXAMPLES::
61
62
sage: from sage.graphs.tutte_polynomial import removed_multiedge
63
sage: G = Graph(multiedges=True)
64
sage: G.add_edges([(0,1,'a'),(0,1,'b')])
65
sage: G.edges()
66
[(0, 1, 'a'), (0, 1, 'b')]
67
sage: with removed_multiedge(G,(0,1),2) as Y:
68
....: G.edges()
69
[]
70
sage: G.edges()
71
[(0, 1, None), (0, 1, None)]
72
"""
73
for i in range(multiplicity):
74
G.delete_edge(edge)
75
try:
76
yield
77
finally:
78
for i in range(multiplicity):
79
G.add_edge(edge)
80
81
82
@contextmanager
83
def removed_edge(G, edge):
84
r"""
85
A context manager which removes an edge from the graph `G` and
86
restores it upon exiting.
87
88
EXAMPLES::
89
90
sage: from sage.graphs.tutte_polynomial import removed_edge
91
sage: G = Graph()
92
sage: G.add_edge(0,1)
93
sage: G.edges()
94
[(0, 1, None)]
95
sage: with removed_edge(G,(0,1)) as Y:
96
....: G.edges(); G.vertices()
97
[]
98
[0, 1]
99
sage: G.edges()
100
[(0, 1, None)]
101
"""
102
G.delete_edge(edge)
103
try:
104
yield
105
finally:
106
G.add_edge(edge)
107
108
109
@contextmanager
110
def contracted_edge(G, edge):
111
r"""
112
Delete the first vertex in the edge, and make all the edges that
113
went from it go to the second vertex.
114
115
EXAMPLES::
116
117
sage: from sage.graphs.tutte_polynomial import contracted_edge
118
sage: G = Graph(multiedges=True)
119
sage: G.add_edges([(0,1,'a'),(1,2,'b'),(0,3,'c')])
120
sage: G.edges()
121
[(0, 1, 'a'), (0, 3, 'c'), (1, 2, 'b')]
122
sage: with contracted_edge(G,(0,1)) as Y:
123
....: G.edges(); G.vertices()
124
[(1, 2, 'b'), (1, 3, 'c')]
125
[1, 2, 3]
126
sage: G.edges()
127
[(0, 1, 'a'), (0, 3, 'c'), (1, 2, 'b')]
128
"""
129
v1, v2 = edge[:2]
130
131
v1_edges = G.edges_incident(v1)
132
G.delete_vertex(v1)
133
added_edges = []
134
135
for start, end, label in v1_edges:
136
other_vertex = start if start != v1 else end
137
edge = (other_vertex, v2, label)
138
G.add_edge(edge)
139
added_edges.append(edge)
140
141
try:
142
yield
143
finally:
144
for edge in added_edges:
145
G.delete_edge(edge)
146
for edge in v1_edges:
147
G.add_edge(edge)
148
149
150
@contextmanager
151
def removed_loops(G):
152
r"""
153
A context manager which removes all the loops in the graph `G`.
154
It yields a list of the the loops, and restores the loops upon
155
exiting.
156
157
EXAMPLES::
158
159
sage: from sage.graphs.tutte_polynomial import removed_loops
160
sage: G = Graph(multiedges=True, loops=True)
161
sage: G.add_edges([(0,1,'a'),(1,2,'b'),(0,0,'c')])
162
sage: G.edges()
163
[(0, 0, 'c'), (0, 1, 'a'), (1, 2, 'b')]
164
sage: with removed_loops(G) as Y:
165
....: G.edges(); G.vertices(); Y
166
[(0, 1, 'a'), (1, 2, 'b')]
167
[0, 1, 2]
168
[(0, 0, 'c')]
169
sage: G.edges()
170
[(0, 0, 'c'), (0, 1, 'a'), (1, 2, 'b')]
171
"""
172
loops = G.loops()
173
for edge in loops:
174
G.delete_edge(edge)
175
try:
176
yield loops
177
finally:
178
for edge in loops:
179
G.add_edge(edge)
180
181
182
def underlying_graph(G):
183
r"""
184
Given a graph `G` with multi-edges, returns a graph where all the
185
multi-edges are replaced with a single edge.
186
187
EXAMPLES::
188
189
sage: from sage.graphs.tutte_polynomial import underlying_graph
190
sage: G = Graph(multiedges=True)
191
sage: G.add_edges([(0,1,'a'),(0,1,'b')])
192
sage: G.edges()
193
[(0, 1, 'a'), (0, 1, 'b')]
194
sage: underlying_graph(G).edges()
195
[(0, 1, None)]
196
"""
197
g = Graph()
198
g.allow_loops(True)
199
for edge in set(G.edges(labels=False)):
200
g.add_edge(edge)
201
return g
202
203
204
def edge_multiplicities(G):
205
r"""
206
Returns the a dictionary of multiplicities of the edges in the
207
graph `G`.
208
209
EXAMPLES::
210
211
sage: from sage.graphs.tutte_polynomial import edge_multiplicities
212
sage: G = Graph({1: [2,2,3], 2: [2], 3: [4,4], 4: [2,2,2]})
213
sage: sorted(edge_multiplicities(G).iteritems())
214
[((1, 2), 2), ((1, 3), 1), ((2, 2), 1), ((2, 4), 3), ((3, 4), 2)]
215
"""
216
d = {}
217
for edge in G.edges(labels=False):
218
d[edge] = d.setdefault(edge, 0) + 1
219
return d
220
221
########
222
# Ears #
223
########
224
225
226
class Ear(object):
227
r"""
228
An ear is a sequence of vertices
229
230
Here is the definition from [Gordon10]_:
231
232
An ear in a graph is a path `v_1 - v_2 - \dots - v_n - v_{n+1}`
233
where `d(v_1) > 2`, `d(v_{n+1}) > 2` and
234
`d(v_2) = d(v_3) = \dots = d(v_n) = 2`.
235
236
A cycle is viewed as a special ear where `v_1 = v_{n+1}` and the
237
restriction on the degree of this vertex is lifted.
238
239
INPUT:
240
"""
241
def __init__(self, graph, end_points, interior, is_cycle):
242
"""
243
EXAMPLES::
244
245
sage: G = graphs.PathGraph(4)
246
sage: G.add_edges([(0,4),(0,5),(3,6),(3,7)])
247
sage: from sage.graphs.tutte_polynomial import Ear
248
sage: E = Ear(G,[0,3],[1,2],False)
249
"""
250
self.end_points = end_points
251
self.interior = interior
252
self.is_cycle = is_cycle
253
self.graph = graph
254
255
@property
256
def s(self):
257
"""
258
Returns the number of distinct edges in this ear.
259
260
EXAMPLES::
261
262
sage: G = graphs.PathGraph(4)
263
sage: G.add_edges([(0,4),(0,5),(3,6),(3,7)])
264
sage: from sage.graphs.tutte_polynomial import Ear
265
sage: E = Ear(G,[0,3],[1,2],False)
266
sage: E.s
267
3
268
"""
269
return len(self.interior) + 1
270
271
@property
272
def vertices(self):
273
"""
274
Returns the vertices of this ear.
275
276
EXAMPLES::
277
278
sage: G = graphs.PathGraph(4)
279
sage: G.add_edges([(0,4),(0,5),(3,6),(3,7)])
280
sage: from sage.graphs.tutte_polynomial import Ear
281
sage: E = Ear(G,[0,3],[1,2],False)
282
sage: E.vertices
283
[0, 1, 2, 3]
284
"""
285
return sorted(self.end_points + self.interior)
286
287
@lazy_attribute
288
def edges(self):
289
"""
290
Returns the edges in this ear.
291
292
EXAMPLES::
293
294
sage: G = graphs.PathGraph(4)
295
sage: G.add_edges([(0,4),(0,5),(3,6),(3,7)])
296
sage: from sage.graphs.tutte_polynomial import Ear
297
sage: E = Ear(G,[0,3],[1,2],False)
298
sage: E.edges
299
[(0, 1), (1, 2), (2, 3)]
300
"""
301
return self.graph.edges_incident(vertices=self.interior, labels=False)
302
303
@staticmethod
304
def find_ear(g):
305
"""
306
Finds the first ear in a graph.
307
308
EXAMPLES::
309
310
sage: G = graphs.PathGraph(4)
311
sage: G.add_edges([(0,4),(0,5),(3,6),(3,7)])
312
sage: from sage.graphs.tutte_polynomial import Ear
313
sage: E = Ear.find_ear(G)
314
sage: E.s
315
3
316
sage: E.edges
317
[(0, 1), (1, 2), (2, 3)]
318
sage: E.vertices
319
[0, 1, 2, 3]
320
"""
321
degree_two_vertices = [v for v, degree
322
in g.degree_iterator(labels=True)
323
if degree == 2]
324
subgraph = g.subgraph(degree_two_vertices)
325
for component in subgraph.connected_components():
326
edges = g.edges_incident(vertices=component, labels=False)
327
all_vertices = list(sorted(set(sum(edges, tuple()))))
328
if len(all_vertices) < 3:
329
continue
330
end_points = [v for v in all_vertices if v not in component]
331
if not end_points:
332
end_points = [component[0]]
333
334
ear_is_cycle = end_points[0] == end_points[-1]
335
336
if ear_is_cycle:
337
for e in end_points:
338
if e in component:
339
component.remove(e)
340
341
return Ear(g, end_points, component, ear_is_cycle)
342
343
@contextmanager
344
def removed_from(self, G):
345
r"""
346
A context manager which removes the ear from the graph `G`.
347
348
EXAMPLES::
349
350
sage: G = graphs.PathGraph(4)
351
sage: G.add_edges([(0,4),(0,5),(3,6),(3,7)])
352
sage: len(G.edges())
353
7
354
sage: from sage.graphs.tutte_polynomial import Ear
355
sage: E = Ear.find_ear(G)
356
sage: with E.removed_from(G) as Y:
357
....: G.edges()
358
[(0, 4, None), (0, 5, None), (3, 6, None), (3, 7, None)]
359
sage: len(G.edges())
360
7
361
"""
362
deleted_edges = []
363
for edge in G.edges_incident(vertices=self.interior, labels=False):
364
G.delete_edge(edge)
365
deleted_edges.append(edge)
366
for v in self.interior:
367
G.delete_vertex(v)
368
369
try:
370
yield
371
finally:
372
for edge in deleted_edges:
373
G.add_edge(edge)
374
375
##################
376
# Edge Selection #
377
##################
378
379
380
class EdgeSelection(object):
381
pass
382
383
384
class VertexOrder(EdgeSelection):
385
def __init__(self, order):
386
"""
387
EXAMPLES::
388
389
sage: from sage.graphs.tutte_polynomial import VertexOrder
390
sage: A = VertexOrder([4,6,3,2,1,7])
391
sage: A.order
392
[4, 6, 3, 2, 1, 7]
393
sage: A.inverse_order
394
{1: 4, 2: 3, 3: 2, 4: 0, 6: 1, 7: 5}
395
"""
396
self.order = list(order)
397
self.inverse_order = dict(map(reversed, enumerate(order)))
398
399
def __call__(self, graph):
400
"""
401
EXAMPLES::
402
403
sage: from sage.graphs.tutte_polynomial import VertexOrder
404
sage: A = VertexOrder([4,0,3,2,1,5])
405
sage: G = graphs.PathGraph(6)
406
sage: A(G)
407
(3, 4)
408
"""
409
for v in self.order:
410
edges = graph.edges_incident([v], labels=False)
411
if edges:
412
edges.sort(key=lambda x: self.inverse_order[x[0] if x[0] != v else x[1]])
413
return edges[0]
414
raise RuntimeError("no edges left to select")
415
416
417
class MinimizeSingleDegree(EdgeSelection):
418
def __call__(self, graph):
419
"""
420
EXAMPLES::
421
422
sage: from sage.graphs.tutte_polynomial import MinimizeSingleDegree
423
sage: G = graphs.PathGraph(6)
424
sage: MinimizeSingleDegree()(G)
425
(0, 1)
426
"""
427
degrees = list(graph.degree_iterator(labels=True))
428
degrees.sort(key=lambda x: x[1]) # Sort by degree
429
for v, degree in degrees:
430
for e in graph.edges_incident([v], labels=False):
431
return e
432
raise RuntimeError("no edges left to select")
433
434
435
class MinimizeDegree(EdgeSelection):
436
def __call__(self, graph):
437
"""
438
EXAMPLES::
439
440
sage: from sage.graphs.tutte_polynomial import MinimizeDegree
441
sage: G = graphs.PathGraph(6)
442
sage: MinimizeDegree()(G)
443
(0, 1)
444
"""
445
degrees = dict(graph.degree_iterator(labels=True))
446
edges = graph.edges(labels=False)
447
edges.sort(key=lambda x: degrees[x[0]]+degrees[x[1]]) # Sort by degree
448
for e in edges:
449
return e
450
raise RuntimeError("no edges left to select")
451
452
453
class MaximizeDegree(EdgeSelection):
454
def __call__(self, graph):
455
"""
456
EXAMPLES::
457
458
sage: from sage.graphs.tutte_polynomial import MaximizeDegree
459
sage: G = graphs.PathGraph(6)
460
sage: MaximizeDegree()(G)
461
(3, 4)
462
"""
463
degrees = dict(graph.degree_iterator(labels=True))
464
edges = graph.edges(labels=False)
465
edges.sort(key=lambda x: degrees[x[0]]+degrees[x[1]]) # Sort by degree
466
for e in reversed(edges):
467
return e
468
raise RuntimeError("no edges left to select")
469
470
###########
471
# Caching #
472
###########
473
474
475
def _cache_key(G):
476
"""
477
Return the key used to cache the result for the graph G
478
479
This is used by the decorator :func:`_cached`.
480
481
EXAMPLES::
482
483
sage: from sage.graphs.tutte_polynomial import _cache_key
484
sage: G = graphs.PetersenGraph()
485
sage: _cache_key(G)
486
((0, 7), (0, 8), (0, 9), (1, 4), (1, 6), (1, 9), (2, 3), (2, 6), (2, 8), (3, 5), (3, 9), (4, 5), (4, 8), (5, 7), (6, 7))
487
"""
488
return tuple(sorted(G.canonical_label().edges(labels=False)))
489
490
491
def _cached(func):
492
"""
493
Wrapper used to cache results of the function `func`
494
495
This uses the function :func:`_cache_key`.
496
497
EXAMPLES::
498
499
sage: from sage.graphs.tutte_polynomial import tutte_polynomial
500
sage: G = graphs.PetersenGraph()
501
sage: T = tutte_polynomial(G) #indirect doctest
502
sage: tutte_polynomial(G)(1,1) #indirect doctest
503
2000
504
"""
505
@sage_wraps(func)
506
def wrapper(G, *args, **kwds):
507
cache = kwds.setdefault('cache', {})
508
key = _cache_key(G)
509
if key in cache:
510
return cache[key]
511
result = func(G, *args, **kwds)
512
cache[key] = result
513
return result
514
wrapper.original_func = func
515
return wrapper
516
517
####################
518
# Tutte Polynomial #
519
####################
520
521
@_cached
522
def tutte_polynomial(G, edge_selector=None, cache=None):
523
r"""
524
Return the Tutte polynomial of the graph `G`.
525
526
INPUT:
527
528
- ``edge_selector`` (optional; method) this argument allows the user
529
to specify his own heuristic for selecting edges used in the deletion
530
contraction recurrence
531
532
- ``cache`` -- (optional; dict) a dictionary to cache the Tutte
533
polynomials generated in the recursive process. One will be
534
created automatically if not provided.
535
536
EXAMPLES:
537
538
The Tutte polynomial of any tree of order `n` is `x^{n-1}`::
539
540
sage: all(T.tutte_polynomial() == x**9 for T in graphs.trees(10))
541
True
542
543
The Tutte polynomial of the Petersen graph is::
544
545
sage: P = graphs.PetersenGraph()
546
sage: P.tutte_polynomial()
547
x^9 + 6*x^8 + 21*x^7 + 56*x^6 + 12*x^5*y + y^6 + 114*x^5 + 70*x^4*y
548
+ 30*x^3*y^2 + 15*x^2*y^3 + 10*x*y^4 + 9*y^5 + 170*x^4 + 170*x^3*y
549
+ 105*x^2*y^2 + 65*x*y^3 + 35*y^4 + 180*x^3 + 240*x^2*y + 171*x*y^2
550
+ 75*y^3 + 120*x^2 + 168*x*y + 84*y^2 + 36*x + 36*y
551
552
The Tutte polynomial of `G` evaluated at (1,1) is the number of
553
spanning trees of `G`::
554
555
sage: G = graphs.RandomGNP(10,0.6)
556
sage: G.tutte_polynomial()(1,1) == G.spanning_trees_count()
557
True
558
559
Given that `T(x,y)` is the Tutte polynomial of a graph `G` with
560
`n` vertices and `c` connected components, then `(-1)^{n-c} x^k
561
T(1-x,0)` is the chromatic polynomial of `G`. ::
562
563
sage: G = graphs.OctahedralGraph()
564
sage: T = G.tutte_polynomial()
565
sage: R = PolynomialRing(ZZ, 'x')
566
sage: R((-1)^5*x*T(1-x,0)).factor()
567
(x - 2) * (x - 1) * x * (x^3 - 9*x^2 + 29*x - 32)
568
sage: G.chromatic_polynomial().factor()
569
(x - 2) * (x - 1) * x * (x^3 - 9*x^2 + 29*x - 32)
570
571
TESTS:
572
573
Providing an external cache::
574
575
sage: cache = {}
576
sage: _ = graphs.RandomGNP(7,.5).tutte_polynomial(cache=cache)
577
sage: len(cache) > 0
578
True
579
"""
580
R = ZZ['x, y']
581
if G.num_edges() == 0:
582
return R.one()
583
584
G = G.relabel(inplace=False) # making sure the vertices are integers
585
G.allow_loops(True)
586
G.allow_multiple_edges(True)
587
588
if edge_selector is None:
589
edge_selector = MinimizeSingleDegree()
590
x, y = R.gens()
591
return _tutte_polynomial_internal(G, x, y, edge_selector, cache=cache)
592
593
@_cached
594
def _tutte_polynomial_internal(G, x, y, edge_selector, cache=None):
595
"""
596
Does the recursive computation of the Tutte polynomial.
597
598
INPUT:
599
600
- ``G`` -- the graph
601
- ``x,y`` -- the variables `x,y` respectively
602
- ``edge_selector`` -- the heuristic for selecting edges used in the
603
deletion contraction recurrence
604
605
TESTS::
606
607
sage: P = graphs.CycleGraph(5)
608
sage: P.tutte_polynomial() # indirect doctest
609
x^4 + x^3 + x^2 + x + y
610
"""
611
if G.num_edges() == 0:
612
return x.parent().one()
613
614
def recursive_tp(graph=None):
615
"""
616
The recursive call -- used so that we do not have to specify
617
the same arguments everywhere.
618
"""
619
if graph is None:
620
graph = G
621
return _tutte_polynomial_internal(graph, x, y, edge_selector, cache=cache)
622
623
#Remove loops
624
with removed_loops(G) as loops:
625
if loops:
626
return y**len(loops) * recursive_tp()
627
628
uG = underlying_graph(G)
629
em = edge_multiplicities(G)
630
d = em.values()
631
632
def yy(start, end):
633
return sum(y**i for i in range(start, end+1))
634
635
#Lemma 1
636
if G.is_forest():
637
return prod(x + yy(1, d_i-1) for d_i in d)
638
639
#Handle disconnected components
640
if not G.is_connected():
641
return prod([recursive_tp(G.subgraph(block))
642
for block in G.connected_components()])
643
644
#Theorem 1: from Haggard, Pearce, Royle 2008
645
blocks, cut_vertices = G.blocks_and_cut_vertices()
646
if len(blocks) > 1:
647
return prod([recursive_tp(G.subgraph(block)) for block in blocks])
648
649
components = G.connected_components_number()
650
edge = edge_selector(G)
651
652
with removed_edge(G, edge):
653
if G.connected_components_number() > components:
654
with contracted_edge(G, edge):
655
return x*recursive_tp()
656
657
##################################
658
# We are in the biconnected case #
659
##################################
660
661
# Theorem 4: from Haggard, Pearce, and Royle Note that the formula
662
# at http://homepages.ecs.vuw.ac.nz/~djp/files/TOMS10.pdf is
663
# slightly incorrect. The initial sum should only go to n-2
664
# instead of n (allowing for the last part of the recursion).
665
# Additionally, the first operand of the final product should be
666
# (x+y^{1...(d_n+d_{n-1}-1)}) instead of just (x+y^(d_n+d_{n-1}-1)
667
if uG.num_verts() == uG.num_edges(): # G is a multi-cycle
668
n = len(d)
669
result = 0
670
for i in range(n - 2):
671
term = (prod((x + yy(1, d_j-1)) for d_j in d[i+1:]) *
672
prod((yy(0, d_k-1)) for d_k in d[:i]))
673
result += term
674
#The last part of the recursion
675
result += (x + yy(1, d[-1] + d[-2] - 1))*prod(yy(0, d_i-1)
676
for d_i in d[:-2])
677
return result
678
679
# Theorem 3 from Haggard, Pearce, and Royle, adapted to multi-eaars
680
ear = Ear.find_ear(uG)
681
if ear is not None:
682
if (ear.is_cycle and ear.vertices == G.vertices()):
683
#The graph is an ear (cycle) We should never be in this
684
#case since we check for multi-cycles above
685
return y + sum(x**i for i in range(1, ear.s))
686
else:
687
with ear.removed_from(G):
688
#result = sum(x^i for i in range(ear.s)) #single ear case
689
result = sum((prod(x + yy(1, em[e]-1) for e in ear.edges[i+1:])
690
* prod(yy(0, em[e]-1) for e in ear.edges[:i]))
691
for i in range(len(ear.edges)))
692
result *= recursive_tp()
693
694
with contracted_edge(G, [ear.end_points[0],
695
ear.end_points[-1]]):
696
result += prod(yy(0, em[e]-1)
697
for e in ear.edges)*recursive_tp()
698
699
return result
700
701
#Theorem 2
702
if len(em) == 1: # the graph is just a multiedge
703
return x + sum(y**i for i in range(1, em[edge]))
704
else:
705
with removed_multiedge(G, edge, em[edge]):
706
result = recursive_tp()
707
with contracted_edge(G, edge):
708
result += sum(y**i for i in range(em[edge]))*recursive_tp()
709
return result
710
711
712