Skip to content

Commit ce5b249

Browse files
author
Release Manager
committed
gh-35956: Fix several issues in find_hamiltonian Part of #35902. The method `find_hamiltonian` had multiple issues: it was not robust to vertices with incomparable labels, it could loop forever if a vertex has degree 1, a segfault was possible on small graphs, it was not properly working for digraphs, etc. We fix multiple issues and the method should now be safe. ### 📝 Checklist - [x] The title is concise, informative, and self-explanatory. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. - [x] I have created tests covering the changes. - [ ] I have updated the documentation accordingly. URL: #35956 Reported by: David Coudert Reviewer(s): Dima Pasechnik
2 parents 4279c4e + fb35dc4 commit ce5b249

File tree

1 file changed

+98
-43
lines changed

1 file changed

+98
-43
lines changed

src/sage/graphs/generic_graph_pyx.pyx

Lines changed: 98 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ from sage.libs.gmp.mpz cimport *
3535
from sage.misc.prandom import random
3636
from sage.graphs.base.static_sparse_graph cimport short_digraph
3737
from sage.graphs.base.static_sparse_graph cimport init_short_digraph
38+
from sage.graphs.base.static_sparse_graph cimport init_reverse
3839
from sage.graphs.base.static_sparse_graph cimport free_short_digraph
3940
from sage.graphs.base.static_sparse_graph cimport out_degree, has_edge
4041

@@ -1257,11 +1258,29 @@ cpdef tuple find_hamiltonian(G, long max_iter=100000, long reset_bound=30000,
12571258
Finally, an example on a graph which does not have a Hamiltonian
12581259
path::
12591260
1260-
sage: G=graphs.HyperStarGraph(5,2)
1261-
sage: fh(G,find_path=False)
1262-
(False, ['00110', '10100', '01100', '11000', '01010', '10010', '00011', '10001', '00101'])
1263-
sage: fh(G,find_path=True)
1264-
(False, ['01001', '10001', '00101', '10100', '00110', '10010', '01010', '11000', '01100'])
1261+
sage: G = graphs.HyperStarGraph(5, 2)
1262+
sage: G.order()
1263+
10
1264+
sage: b, P = fh(G,find_path=False)
1265+
sage: b, len(P)
1266+
(False, 9)
1267+
sage: b, P = fh(G,find_path=True)
1268+
sage: b, len(P)
1269+
(False, 9)
1270+
1271+
The method can also be used for directed graphs::
1272+
1273+
sage: G = DiGraph([(0, 1), (1, 2), (2, 3)])
1274+
sage: fh(G)
1275+
(False, [0, 1, 2, 3])
1276+
sage: G = G.reverse()
1277+
sage: fh(G)
1278+
(False, [3, 2, 1, 0])
1279+
sage: G = DiGraph()
1280+
sage: G.add_cycle([0, 1, 2, 3, 4, 5])
1281+
sage: b, P = fh(G)
1282+
sage: b, len(P)
1283+
(True, 6)
12651284
12661285
TESTS:
12671286
@@ -1309,30 +1328,38 @@ cpdef tuple find_hamiltonian(G, long max_iter=100000, long reset_bound=30000,
13091328
sage: fh(G, find_path=True)
13101329
(False, [0, 1, 2, 3])
13111330
1331+
Check that the method is robust to incomparable vertices::
1332+
1333+
sage: G = Graph([(1, 'a'), ('a', 2), (2, 3), (3, 1)])
1334+
sage: b, C = fh(G, find_path=False)
1335+
sage: b, len(C)
1336+
(True, 4)
13121337
"""
1338+
G._scream_if_not_simple()
1339+
13131340
from sage.misc.prandom import randint
13141341
cdef int n = G.order()
13151342

13161343
# Easy cases
1317-
if not n:
1318-
return False, []
1319-
if n == 1:
1320-
return False, G.vertices(sort=False)
1344+
if n < 2:
1345+
return False, list(G)
13211346

13221347
# To clean the output when find_path is None or a number
13231348
find_path = (find_path > 0)
13241349

13251350
if G.is_clique(induced=False):
13261351
# We have an hamiltonian path since n >= 2, but we have an hamiltonian
13271352
# cycle only if n >= 3
1328-
return find_path or n >= 3, G.vertices(sort=True)
1353+
return find_path or n >= 3, list(G)
13291354

13301355
cdef list best_path, p
13311356
if not G.is_connected():
13321357
# The (Di)Graph has no hamiltonian path or cycle. We search for the
13331358
# longest path in its connected components.
13341359
best_path = []
13351360
for H in G.connected_components_subgraphs():
1361+
if H.order() <= len(best_path):
1362+
continue
13361363
_, p = find_hamiltonian(H, max_iter=max_iter, reset_bound=reset_bound,
13371364
backtrack_bound=backtrack_bound, find_path=True)
13381365
if len(p) > len(best_path):
@@ -1341,20 +1368,24 @@ cpdef tuple find_hamiltonian(G, long max_iter=100000, long reset_bound=30000,
13411368

13421369
# Misc variables used below
13431370
cdef int i, j
1344-
cdef int n_available
1371+
cdef bint directed = G.is_directed()
13451372

13461373
# Initialize the path.
13471374
cdef MemoryAllocator mem = MemoryAllocator()
13481375
cdef int *path = <int *>mem.allocarray(n, sizeof(int))
1349-
memset(path, -1, n * sizeof(int))
13501376

13511377
# Initialize the membership array
13521378
cdef bint *member = <bint *>mem.allocarray(n, sizeof(int))
13531379
memset(member, 0, n * sizeof(int))
13541380

13551381
# static copy of the graph for more efficient operations
1382+
cdef list int_to_vertex = list(G)
13561383
cdef short_digraph sd
1357-
init_short_digraph(sd, G)
1384+
init_short_digraph(sd, G, edge_labelled=False, vertex_list=int_to_vertex)
1385+
cdef short_digraph rev_sd
1386+
cdef bint reverse = False
1387+
if directed:
1388+
init_reverse(rev_sd, sd)
13581389

13591390
# A list to store the available vertices at each step
13601391
cdef list available_vertices = []
@@ -1364,7 +1395,7 @@ cpdef tuple find_hamiltonian(G, long max_iter=100000, long reset_bound=30000,
13641395
cdef int u = randint(0, n - 1)
13651396
while not out_degree(sd, u):
13661397
u = randint(0, n - 1)
1367-
# Then we pick at random a neighbor of u
1398+
# Then we pick at random a neighbor of u
13681399
cdef int x = randint(0, out_degree(sd, u) - 1)
13691400
cdef int v = sd.neighbors[u][x]
13701401
# This will be the first edge in the path
@@ -1382,34 +1413,35 @@ cpdef tuple find_hamiltonian(G, long max_iter=100000, long reset_bound=30000,
13821413

13831414
# Initialize a path to contain the longest path
13841415
cdef int *longest_path = <int *>mem.allocarray(n, sizeof(int))
1385-
memset(longest_path, -1, n * sizeof(int))
13861416
for i in range(length):
13871417
longest_path[i] = path[i]
13881418

1389-
# Initialize a temporary path for flipping
1390-
cdef int *temp_path = <int *>mem.allocarray(n, sizeof(int))
1391-
memset(temp_path, -1, n * sizeof(int))
1392-
13931419
cdef bint longer = False
1394-
cdef bint good = True
1420+
cdef bint longest_reversed = False
13951421
cdef bint flag
13961422

13971423
while not done:
13981424
counter = counter + 1
13991425
if counter % 10 == 0:
14001426
# Reverse the path
1401-
14021427
for i in range(length//2):
14031428
t = path[i]
14041429
path[i] = path[length - i - 1]
14051430
path[length - i - 1] = t
14061431

1432+
if directed:
1433+
# We now work on the reverse graph
1434+
reverse = not reverse
1435+
14071436
if counter > reset_bound:
14081437
bigcount = bigcount + 1
14091438
counter = 1
14101439

14111440
# Time to reset the procedure
14121441
memset(member, 0, n * sizeof(int))
1442+
if directed and reverse:
1443+
# We restore the original orientation
1444+
reverse = False
14131445

14141446
# First we pick a random vertex u of (out-)degree at least one
14151447
u = randint(0, n - 1)
@@ -1425,37 +1457,44 @@ cpdef tuple find_hamiltonian(G, long max_iter=100000, long reset_bound=30000,
14251457
member[u] = True
14261458
member[v] = True
14271459

1428-
if counter % backtrack_bound == 0:
1460+
if length > 5 and counter % backtrack_bound == 0:
14291461
for i in range(5):
14301462
member[path[length - i - 1]] = False
14311463
length = length - 5
14321464
longer = False
14331465

1466+
# We search for a possible extension of the path
14341467
available_vertices = []
14351468
u = path[length - 1]
1436-
for i in range(out_degree(sd, u)):
1437-
v = sd.neighbors[u][i]
1438-
if not member[v]:
1439-
available_vertices.append(v)
1469+
if directed and reverse:
1470+
for i in range(out_degree(rev_sd, u)):
1471+
v = rev_sd.neighbors[u][i]
1472+
if not member[v]:
1473+
available_vertices.append(v)
1474+
else:
1475+
for i in range(out_degree(sd, u)):
1476+
v = sd.neighbors[u][i]
1477+
if not member[v]:
1478+
available_vertices.append(v)
14401479

1441-
n_available = len(available_vertices)
1442-
if n_available > 0:
1480+
if available_vertices:
14431481
longer = True
1444-
x = randint(0, n_available - 1)
1445-
path[length] = available_vertices[x]
1482+
x = randint(0, len(available_vertices) - 1)
1483+
v = available_vertices[x]
1484+
path[length] = v
14461485
length = length + 1
1447-
member[available_vertices[x]] = True
1486+
member[v] = True
14481487

14491488
if not longer and length > longest:
1450-
1489+
# Store the current best solution
14511490
for i in range(length):
14521491
longest_path[i] = path[i]
14531492

14541493
longest = length
1494+
longest_reversed = reverse
14551495

1456-
if not longer:
1457-
1458-
memset(temp_path, -1, n * sizeof(int))
1496+
if not directed and not longer and out_degree(sd, path[length - 1]) > 1:
1497+
# We revert a cycle to change the extremity of the path
14591498
degree = out_degree(sd, path[length - 1])
14601499
while True:
14611500
x = randint(0, degree - 1)
@@ -1475,37 +1514,53 @@ cpdef tuple find_hamiltonian(G, long max_iter=100000, long reset_bound=30000,
14751514
j += 1
14761515
if path[i] == u:
14771516
flag = True
1517+
14781518
if length == n:
14791519
if find_path:
14801520
done = True
1521+
elif directed and reverse:
1522+
done = has_edge(rev_sd, path[0], path[n - 1]) != NULL
14811523
else:
14821524
done = has_edge(sd, path[n - 1], path[0]) != NULL
14831525

14841526
if bigcount * reset_bound > max_iter:
1485-
verts = G.vertices(sort=True)
1486-
output = [verts[longest_path[i]] for i in range(longest)]
1527+
output = [int_to_vertex[longest_path[i]] for i in range(longest)]
14871528
free_short_digraph(sd)
1529+
if directed:
1530+
free_short_digraph(rev_sd)
1531+
if longest_reversed:
1532+
return (False, output[::-1])
14881533
return (False, output)
14891534
# #
14901535
# # Output test
14911536
# #
14921537

1538+
if directed and reverse:
1539+
# We revert the path to work on sd
1540+
for i in range(length//2):
1541+
t = path[i]
1542+
path[i] = path[length - i - 1]
1543+
path[length - i - 1] = t
1544+
14931545
# Test adjacencies
1546+
cdef bint good = True
14941547
for i in range(n - 1):
14951548
u = path[i]
14961549
v = path[i + 1]
1497-
# Graph is simple, so both arcs are present
14981550
if has_edge(sd, u, v) == NULL:
14991551
good = False
15001552
break
15011553
if good is False:
1502-
raise RuntimeError('vertices %d and %d are consecutive in the cycle but are not adjacent' % (u, v))
1503-
if not find_path and has_edge(sd, path[0], path[n - 1]) == NULL:
1504-
raise RuntimeError('vertices %d and %d are not adjacent' % (path[0], path[n - 1]))
1554+
raise RuntimeError(f"vertices {int_to_vertex[u]} and {int_to_vertex[v]}"
1555+
" are consecutive in the cycle but are not adjacent")
1556+
if not find_path and has_edge(sd, path[n - 1], path[0]) == NULL:
1557+
raise RuntimeError(f"vertices {int_to_vertex[path[n - 1]]} and "
1558+
f"{int_to_vertex[path[0]]} are not adjacent")
15051559

1506-
verts = G.vertices(sort=True)
1507-
output = [verts[path[i]] for i in range(length)]
1560+
output = [int_to_vertex[path[i]] for i in range(length)]
15081561
free_short_digraph(sd)
1562+
if directed:
1563+
free_short_digraph(rev_sd)
15091564

15101565
return (True, output)
15111566

0 commit comments

Comments
 (0)