Skip to content

Commit df88d06

Browse files
author
Release Manager
committed
gh-38354: add uniform generator of random proper interval graphs This PR implements the random proper interval graph generator proposed in - Toshiki Saitoh, Katsuhisa Yamanaka, Masashi Kiyomi, Ryuhei Uehara: Random Generation and Enumeration of Proper Interval Graphs. IEICE Trans. Inf. Syst. 93-D(7): 1816-1823 (2010) The time complexity of the generator is in $O(n^3)$. To check that the generator is uniform, we can use: ```py def test(n, steps=1000): from collections import Counter C = Counter() for _ in range(steps): G = graphs.RandomProperIntervalGraph(n) C[G.canonical_label().copy(immutable=True)] += 1 L = list(C.values()) import numpy print(f"different graphs = {len(L)}, avg number = {numpy.mean(L)}, standard deviation = {numpy.std(L)}") ``` and we get ```py sage: test(10, 100000) different graphs = 2542, avg number = 39.33910306845004, standard deviation = 7.640982640860865 sage: test(15, 100000) different graphs = 96384, avg number = 1.0375166002656042, standard deviation = 0.19487594733725658 sage: test(100, 100000) different graphs = 100000, avg number = 1.0, standard deviation = 0.0 ``` It seems that the generator is asymptotically uniform. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [ ] I have linked a relevant issue or discussion. - [x] I have created tests covering the changes. - [x] I have updated the documentation and checked the documentation preview. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on. For example, --> <!-- - #12345: short description why this is a dependency --> <!-- - #34567: ... --> URL: #38354 Reported by: David Coudert Reviewer(s): Frédéric Chapoton
2 parents 3b02726 + 61bd3a7 commit df88d06

File tree

3 files changed

+203
-2
lines changed

3 files changed

+203
-2
lines changed

src/doc/en/reference/references/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6300,6 +6300,11 @@ REFERENCES:
63006300
Discrete Mathematics,
63016301
Volume 63, Issues 2-3, 1987, Pages 279-295.
63026302
6303+
.. [SYKU2010] Toshiki Saitoh, Katsuhisa Yamanaka, Masashi Kiyomi, Ryuhei Uehara:
6304+
Random Generation and Enumeration of Proper Interval Graphs.
6305+
IEICE Trans. Inf. Syst. 93-D(7): 1816-1823 (2010)
6306+
:doi:`10.1587/transinf.E93.D.1816`
6307+
63036308
.. [SYYTIYTT2002] \T. Shimoyama, H. Yanami, K. Yokoyama, M. Takenaka, K. Itoh,
63046309
\J. Yajima, N. Torii, and H. Tanaka, *The block cipher SC2000*; in
63056310
FSE, (2001), pp. 312-327.

src/sage/graphs/generators/random.py

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,7 @@ def RandomHolmeKim(n, m, p, seed=None):
825825

826826
def RandomIntervalGraph(n, seed=None):
827827
r"""
828-
Returns a random interval graph.
828+
Return a random interval graph.
829829
830830
An interval graph is built from a list `(a_i,b_i)_{1\leq i \leq n}`
831831
of intervals : to each interval of the list is associated one
@@ -845,6 +845,11 @@ def RandomIntervalGraph(n, seed=None):
845845
used to create the graph are saved with the graph and can
846846
be recovered using ``get_vertex()`` or ``get_vertices()``.
847847
848+
.. SEEALSO::
849+
850+
- :meth:`sage.graphs.generators.intersection.IntervalGraph`
851+
- :meth:`sage.graphs.generators.random.RandomProperIntervalGraph`
852+
848853
INPUT:
849854
850855
- ``n`` -- integer; the number of vertices in the random graph
@@ -863,13 +868,202 @@ def RandomIntervalGraph(n, seed=None):
863868
"""
864869
if seed is not None:
865870
set_random_seed(seed)
866-
from sage.misc.prandom import random
867871
from sage.graphs.generators.intersection import IntervalGraph
868872

869873
intervals = [tuple(sorted((random(), random()))) for i in range(n)]
870874
return IntervalGraph(intervals, True)
871875

872876

877+
def RandomProperIntervalGraph(n, seed=None):
878+
r"""
879+
Return a random proper interval graph.
880+
881+
An interval graph is built from a list `(a_i,b_i)_{1\leq i \leq n}` of
882+
intervals : to each interval of the list is associated one vertex, two
883+
vertices being adjacent if the two corresponding (closed) intervals
884+
intersect. An interval graph is proper if no interval of the list properly
885+
contains another interval.
886+
Observe that proper interval graphs coincide with unit interval graphs.
887+
See the :wikipedia:`Interval_graph` for more details.
888+
889+
This method implements the random proper interval graph generator proposed
890+
in [SYKU2010]_ which outputs graphs with uniform probability. The time
891+
complexity of this generator is in `O(n^3)`.
892+
893+
.. NOTE::
894+
895+
The vertices are named 0, 1, 2, and so on. The intervals
896+
used to create the graph are saved with the graph and can
897+
be recovered using ``get_vertex()`` or ``get_vertices()``.
898+
899+
.. SEEALSO::
900+
901+
- :meth:`sage.graphs.generators.intersection.IntervalGraph`
902+
- :meth:`sage.graphs.generators.random.RandomIntervalGraph`
903+
904+
INPUT:
905+
906+
- ``n`` -- positive integer; the number of vertices of the graph
907+
908+
- ``seed`` -- a ``random.Random`` seed or a Python ``int`` for the random
909+
number generator (default: ``None``)
910+
911+
EXAMPLES::
912+
913+
sage: from sage.graphs.generators.random import RandomProperIntervalGraph
914+
sage: G = RandomProperIntervalGraph(10)
915+
sage: G.is_interval()
916+
True
917+
918+
TESTS::
919+
920+
sage: from sage.graphs.generators.random import RandomProperIntervalGraph
921+
sage: RandomProperIntervalGraph(0)
922+
Graph on 0 vertices
923+
sage: RandomProperIntervalGraph(1)
924+
Graph on 1 vertex
925+
sage: RandomProperIntervalGraph(-1)
926+
Traceback (most recent call last):
927+
...
928+
ValueError: parameter n must be >= 0
929+
"""
930+
if seed is not None:
931+
set_random_seed(seed)
932+
if n < 0:
933+
raise ValueError('parameter n must be >= 0')
934+
if not n:
935+
return Graph()
936+
937+
from sage.graphs.generators.intersection import IntervalGraph
938+
939+
if n == 1:
940+
return IntervalGraph([[0, 1]])
941+
942+
from sage.combinat.combinat import catalan_number
943+
from sage.functions.other import binomial
944+
945+
# let np = n' = n - 1
946+
np = n - 1
947+
948+
# Choose case 1 with probability C(n') / (C(n') + binomial(n', n' // 2))
949+
cnp = catalan_number(np)
950+
if random() < cnp / (cnp + binomial(np, np // 2)):
951+
# Case 1: Generate a balanced nonnegative string (that can be
952+
# reversible) of length 2n' as follows. We generate the sequence of '['
953+
# and ']' from left to right. Assume we have already chosen k symbols
954+
# x_1x_2...x_k, with k < 2n'. The next symbol x_{k+1} is '[' with
955+
# probability (h_x(k) + 2) (r - h_x(k) + 1) / (2 (r + 1) (h_x(k) + 1))
956+
# where r = 2n' - k - 1 and
957+
# h_x(k) = 0 if k == 0, h_x(k - 1) + 1 if x_i == 0 else h_x(k - 1) - 1.
958+
#
959+
# Since the i-th interval starts at the i-th symbol [ and ends at the
960+
# i-th symbol ], we directly build the intervals
961+
intervals = [[0, 2*n] for _ in range(n)]
962+
L = 1 # next starting interval
963+
R = 0 # next ending interval
964+
hx = [0]
965+
r = 2 * np - 1
966+
for k in range(2 * np):
967+
# Choose symbol x_{k+1}
968+
if random() < ((hx[k] + 2) * (r - hx[k] + 1)) / (2 * (r + 1) * (hx[k] + 1)):
969+
# We have choosen symbol [, so we start an interval
970+
hx.append(hx[k] + 1)
971+
intervals[L][0] = k + 1
972+
L += 1
973+
else:
974+
# We have choosen symbol ], so we end an interval
975+
hx.append(hx[k] - 1)
976+
intervals[R][1] = k + 1
977+
R += 1
978+
r -= 1
979+
# Add the last symbol, ], to get a sequence of length 2*n
980+
intervals[R][1] = k + 2
981+
982+
# Finally return the interval graph
983+
return IntervalGraph(intervals)
984+
985+
# Otherwise, generate a balanced nonnegative reversible string of length
986+
# 2n'. This case happens with small probability and is way more complex.
987+
# The string is of the form x_1x_2...x_ny_n..y_2y_1, where y_i is ] if x_i
988+
# is [, and [ otherwise.
989+
990+
from sage.misc.cachefunc import cached_function
991+
992+
@cached_function
993+
def compute_C(n, h):
994+
"""
995+
Return C(n, h) as defined below.
996+
997+
Recall that the Catalan number is C(n) = binomial(2n, n) / (n + 1)
998+
and let C(n, h) = 0 if h > n. The following equations hold for each
999+
integers i and k with 0 <= i <= k.
1000+
1001+
1. C(2k, 2i + 1) = 0, C(2k + 1, 2i) = 0,
1002+
2. C(2k, 0) = C(k), C(k, k) = 1, and
1003+
3. C(k, i) = C(k - 1, i - 1) + C(k - 1, i + 1).
1004+
"""
1005+
if h > n:
1006+
return 0
1007+
if n % 2 != h % 2:
1008+
# C(2k, 2i + 1) = 0 and C(2k + 1, 2i) = 0
1009+
# i.e., if n and h have different parity
1010+
return 0
1011+
if n == h:
1012+
return 1
1013+
if not h and not n % 2:
1014+
# C(2k, 0) = C(k)
1015+
return catalan_number(n // 2)
1016+
# Otherwise, C(k, i) = C(k - 1, i - 1) + C(k - 1, i + 1)
1017+
return compute_C(n - 1, h - 1) + compute_C(n - 1, h + 1)
1018+
1019+
# We first fill an array hx of length n, backward, and then use it to choose
1020+
# the symbols x_1x_2...x_n (and so symbols y_n...y_2y_1).
1021+
hx = [0] * n
1022+
hx[1] = 1
1023+
# Set hx[np] = h with probability C(np, h) / binomial(np, np // 2)
1024+
number = randint(0, binomial(np, np // 2))
1025+
total = 0
1026+
for h in range(np + 1):
1027+
total += compute_C(np, h)
1028+
if number < total:
1029+
break
1030+
hx[np] = h
1031+
1032+
x = [']']
1033+
y = ['[']
1034+
for i in range(np - 1, 0, -1):
1035+
# Choose symbol x_i
1036+
if random() < (hx[i + 1] + 2) * (i - hx[i + 1] + 1) / (2 * (i + 1) * (hx[i + 1] + 1)):
1037+
hx[i] = hx[i + 1] + 1
1038+
x.append(']')
1039+
y.append('[')
1040+
else:
1041+
hx[i] = hx[i + 1] - 1
1042+
x.append('[')
1043+
y.append(']')
1044+
x.append('[')
1045+
x.reverse()
1046+
y.append(']')
1047+
x.extend(y)
1048+
1049+
# We now turn the sequence of symbols to proper intervals.
1050+
# The i-th intervals starts from the index of the i-th symbol [ in
1051+
# symbols and ends at the position of the i-th symbol ].
1052+
intervals = [[0, 2 * n] for _ in range(n)]
1053+
L = 0 # next starting interval
1054+
R = 0 # next ending interval
1055+
for pos, symbol in enumerate(x):
1056+
if symbol == '[':
1057+
intervals[L][0] = pos
1058+
L += 1
1059+
else:
1060+
intervals[R][1] = pos
1061+
R += 1
1062+
1063+
# We finally return the resulting interval graph
1064+
return IntervalGraph(intervals)
1065+
1066+
8731067
# Random Chordal Graphs
8741068

8751069
def growing_subtrees(T, k):

src/sage/graphs/graph_generators.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ def wrap_name(x):
367367
"RandomPartialKTree",
368368
"RandomLobster",
369369
"RandomNewmanWattsStrogatz",
370+
"RandomProperIntervalGraph",
370371
"RandomRegular",
371372
"RandomShell",
372373
"RandomToleranceGraph",
@@ -2768,6 +2769,7 @@ def quadrangulations(self, order, minimum_degree=None, minimum_connectivity=None
27682769
RandomIntervalGraph = staticmethod(random.RandomIntervalGraph)
27692770
RandomLobster = staticmethod(random.RandomLobster)
27702771
RandomNewmanWattsStrogatz = staticmethod(random.RandomNewmanWattsStrogatz)
2772+
RandomProperIntervalGraph = staticmethod(random.RandomProperIntervalGraph)
27712773
RandomRegular = staticmethod(random.RandomRegular)
27722774
RandomShell = staticmethod(random.RandomShell)
27732775
RandomKTree = staticmethod(random.RandomKTree)

0 commit comments

Comments
 (0)