graphical.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. """Test sequences for graphiness.
  2. """
  3. import heapq
  4. import networkx as nx
  5. __all__ = [
  6. "is_graphical",
  7. "is_multigraphical",
  8. "is_pseudographical",
  9. "is_digraphical",
  10. "is_valid_degree_sequence_erdos_gallai",
  11. "is_valid_degree_sequence_havel_hakimi",
  12. ]
  13. def is_graphical(sequence, method="eg"):
  14. """Returns True if sequence is a valid degree sequence.
  15. A degree sequence is valid if some graph can realize it.
  16. Parameters
  17. ----------
  18. sequence : list or iterable container
  19. A sequence of integer node degrees
  20. method : "eg" | "hh" (default: 'eg')
  21. The method used to validate the degree sequence.
  22. "eg" corresponds to the Erdős-Gallai algorithm
  23. [EG1960]_, [choudum1986]_, and
  24. "hh" to the Havel-Hakimi algorithm
  25. [havel1955]_, [hakimi1962]_, [CL1996]_.
  26. Returns
  27. -------
  28. valid : bool
  29. True if the sequence is a valid degree sequence and False if not.
  30. Examples
  31. --------
  32. >>> G = nx.path_graph(4)
  33. >>> sequence = (d for n, d in G.degree())
  34. >>> nx.is_graphical(sequence)
  35. True
  36. References
  37. ----------
  38. .. [EG1960] Erdős and Gallai, Mat. Lapok 11 264, 1960.
  39. .. [choudum1986] S.A. Choudum. "A simple proof of the Erdős-Gallai theorem on
  40. graph sequences." Bulletin of the Australian Mathematical Society, 33,
  41. pp 67-70, 1986. https://doi.org/10.1017/S0004972700002872
  42. .. [havel1955] Havel, V. "A Remark on the Existence of Finite Graphs"
  43. Casopis Pest. Mat. 80, 477-480, 1955.
  44. .. [hakimi1962] Hakimi, S. "On the Realizability of a Set of Integers as
  45. Degrees of the Vertices of a Graph." SIAM J. Appl. Math. 10, 496-506, 1962.
  46. .. [CL1996] G. Chartrand and L. Lesniak, "Graphs and Digraphs",
  47. Chapman and Hall/CRC, 1996.
  48. """
  49. if method == "eg":
  50. valid = is_valid_degree_sequence_erdos_gallai(list(sequence))
  51. elif method == "hh":
  52. valid = is_valid_degree_sequence_havel_hakimi(list(sequence))
  53. else:
  54. msg = "`method` must be 'eg' or 'hh'"
  55. raise nx.NetworkXException(msg)
  56. return valid
  57. def _basic_graphical_tests(deg_sequence):
  58. # Sort and perform some simple tests on the sequence
  59. deg_sequence = nx.utils.make_list_of_ints(deg_sequence)
  60. p = len(deg_sequence)
  61. num_degs = [0] * p
  62. dmax, dmin, dsum, n = 0, p, 0, 0
  63. for d in deg_sequence:
  64. # Reject if degree is negative or larger than the sequence length
  65. if d < 0 or d >= p:
  66. raise nx.NetworkXUnfeasible
  67. # Process only the non-zero integers
  68. elif d > 0:
  69. dmax, dmin, dsum, n = max(dmax, d), min(dmin, d), dsum + d, n + 1
  70. num_degs[d] += 1
  71. # Reject sequence if it has odd sum or is oversaturated
  72. if dsum % 2 or dsum > n * (n - 1):
  73. raise nx.NetworkXUnfeasible
  74. return dmax, dmin, dsum, n, num_degs
  75. def is_valid_degree_sequence_havel_hakimi(deg_sequence):
  76. r"""Returns True if deg_sequence can be realized by a simple graph.
  77. The validation proceeds using the Havel-Hakimi theorem
  78. [havel1955]_, [hakimi1962]_, [CL1996]_.
  79. Worst-case run time is $O(s)$ where $s$ is the sum of the sequence.
  80. Parameters
  81. ----------
  82. deg_sequence : list
  83. A list of integers where each element specifies the degree of a node
  84. in a graph.
  85. Returns
  86. -------
  87. valid : bool
  88. True if deg_sequence is graphical and False if not.
  89. Notes
  90. -----
  91. The ZZ condition says that for the sequence d if
  92. .. math::
  93. |d| >= \frac{(\max(d) + \min(d) + 1)^2}{4*\min(d)}
  94. then d is graphical. This was shown in Theorem 6 in [1]_.
  95. References
  96. ----------
  97. .. [1] I.E. Zverovich and V.E. Zverovich. "Contributions to the theory
  98. of graphic sequences", Discrete Mathematics, 105, pp. 292-303 (1992).
  99. .. [havel1955] Havel, V. "A Remark on the Existence of Finite Graphs"
  100. Casopis Pest. Mat. 80, 477-480, 1955.
  101. .. [hakimi1962] Hakimi, S. "On the Realizability of a Set of Integers as
  102. Degrees of the Vertices of a Graph." SIAM J. Appl. Math. 10, 496-506, 1962.
  103. .. [CL1996] G. Chartrand and L. Lesniak, "Graphs and Digraphs",
  104. Chapman and Hall/CRC, 1996.
  105. """
  106. try:
  107. dmax, dmin, dsum, n, num_degs = _basic_graphical_tests(deg_sequence)
  108. except nx.NetworkXUnfeasible:
  109. return False
  110. # Accept if sequence has no non-zero degrees or passes the ZZ condition
  111. if n == 0 or 4 * dmin * n >= (dmax + dmin + 1) * (dmax + dmin + 1):
  112. return True
  113. modstubs = [0] * (dmax + 1)
  114. # Successively reduce degree sequence by removing the maximum degree
  115. while n > 0:
  116. # Retrieve the maximum degree in the sequence
  117. while num_degs[dmax] == 0:
  118. dmax -= 1
  119. # If there are not enough stubs to connect to, then the sequence is
  120. # not graphical
  121. if dmax > n - 1:
  122. return False
  123. # Remove largest stub in list
  124. num_degs[dmax], n = num_degs[dmax] - 1, n - 1
  125. # Reduce the next dmax largest stubs
  126. mslen = 0
  127. k = dmax
  128. for i in range(dmax):
  129. while num_degs[k] == 0:
  130. k -= 1
  131. num_degs[k], n = num_degs[k] - 1, n - 1
  132. if k > 1:
  133. modstubs[mslen] = k - 1
  134. mslen += 1
  135. # Add back to the list any non-zero stubs that were removed
  136. for i in range(mslen):
  137. stub = modstubs[i]
  138. num_degs[stub], n = num_degs[stub] + 1, n + 1
  139. return True
  140. def is_valid_degree_sequence_erdos_gallai(deg_sequence):
  141. r"""Returns True if deg_sequence can be realized by a simple graph.
  142. The validation is done using the Erdős-Gallai theorem [EG1960]_.
  143. Parameters
  144. ----------
  145. deg_sequence : list
  146. A list of integers
  147. Returns
  148. -------
  149. valid : bool
  150. True if deg_sequence is graphical and False if not.
  151. Notes
  152. -----
  153. This implementation uses an equivalent form of the Erdős-Gallai criterion.
  154. Worst-case run time is $O(n)$ where $n$ is the length of the sequence.
  155. Specifically, a sequence d is graphical if and only if the
  156. sum of the sequence is even and for all strong indices k in the sequence,
  157. .. math::
  158. \sum_{i=1}^{k} d_i \leq k(k-1) + \sum_{j=k+1}^{n} \min(d_i,k)
  159. = k(n-1) - ( k \sum_{j=0}^{k-1} n_j - \sum_{j=0}^{k-1} j n_j )
  160. A strong index k is any index where d_k >= k and the value n_j is the
  161. number of occurrences of j in d. The maximal strong index is called the
  162. Durfee index.
  163. This particular rearrangement comes from the proof of Theorem 3 in [2]_.
  164. The ZZ condition says that for the sequence d if
  165. .. math::
  166. |d| >= \frac{(\max(d) + \min(d) + 1)^2}{4*\min(d)}
  167. then d is graphical. This was shown in Theorem 6 in [2]_.
  168. References
  169. ----------
  170. .. [1] A. Tripathi and S. Vijay. "A note on a theorem of Erdős & Gallai",
  171. Discrete Mathematics, 265, pp. 417-420 (2003).
  172. .. [2] I.E. Zverovich and V.E. Zverovich. "Contributions to the theory
  173. of graphic sequences", Discrete Mathematics, 105, pp. 292-303 (1992).
  174. .. [EG1960] Erdős and Gallai, Mat. Lapok 11 264, 1960.
  175. """
  176. try:
  177. dmax, dmin, dsum, n, num_degs = _basic_graphical_tests(deg_sequence)
  178. except nx.NetworkXUnfeasible:
  179. return False
  180. # Accept if sequence has no non-zero degrees or passes the ZZ condition
  181. if n == 0 or 4 * dmin * n >= (dmax + dmin + 1) * (dmax + dmin + 1):
  182. return True
  183. # Perform the EG checks using the reformulation of Zverovich and Zverovich
  184. k, sum_deg, sum_nj, sum_jnj = 0, 0, 0, 0
  185. for dk in range(dmax, dmin - 1, -1):
  186. if dk < k + 1: # Check if already past Durfee index
  187. return True
  188. if num_degs[dk] > 0:
  189. run_size = num_degs[dk] # Process a run of identical-valued degrees
  190. if dk < k + run_size: # Check if end of run is past Durfee index
  191. run_size = dk - k # Adjust back to Durfee index
  192. sum_deg += run_size * dk
  193. for v in range(run_size):
  194. sum_nj += num_degs[k + v]
  195. sum_jnj += (k + v) * num_degs[k + v]
  196. k += run_size
  197. if sum_deg > k * (n - 1) - k * sum_nj + sum_jnj:
  198. return False
  199. return True
  200. def is_multigraphical(sequence):
  201. """Returns True if some multigraph can realize the sequence.
  202. Parameters
  203. ----------
  204. sequence : list
  205. A list of integers
  206. Returns
  207. -------
  208. valid : bool
  209. True if deg_sequence is a multigraphic degree sequence and False if not.
  210. Notes
  211. -----
  212. The worst-case run time is $O(n)$ where $n$ is the length of the sequence.
  213. References
  214. ----------
  215. .. [1] S. L. Hakimi. "On the realizability of a set of integers as
  216. degrees of the vertices of a linear graph", J. SIAM, 10, pp. 496-506
  217. (1962).
  218. """
  219. try:
  220. deg_sequence = nx.utils.make_list_of_ints(sequence)
  221. except nx.NetworkXError:
  222. return False
  223. dsum, dmax = 0, 0
  224. for d in deg_sequence:
  225. if d < 0:
  226. return False
  227. dsum, dmax = dsum + d, max(dmax, d)
  228. if dsum % 2 or dsum < 2 * dmax:
  229. return False
  230. return True
  231. def is_pseudographical(sequence):
  232. """Returns True if some pseudograph can realize the sequence.
  233. Every nonnegative integer sequence with an even sum is pseudographical
  234. (see [1]_).
  235. Parameters
  236. ----------
  237. sequence : list or iterable container
  238. A sequence of integer node degrees
  239. Returns
  240. -------
  241. valid : bool
  242. True if the sequence is a pseudographic degree sequence and False if not.
  243. Notes
  244. -----
  245. The worst-case run time is $O(n)$ where n is the length of the sequence.
  246. References
  247. ----------
  248. .. [1] F. Boesch and F. Harary. "Line removal algorithms for graphs
  249. and their degree lists", IEEE Trans. Circuits and Systems, CAS-23(12),
  250. pp. 778-782 (1976).
  251. """
  252. try:
  253. deg_sequence = nx.utils.make_list_of_ints(sequence)
  254. except nx.NetworkXError:
  255. return False
  256. return sum(deg_sequence) % 2 == 0 and min(deg_sequence) >= 0
  257. def is_digraphical(in_sequence, out_sequence):
  258. r"""Returns True if some directed graph can realize the in- and out-degree
  259. sequences.
  260. Parameters
  261. ----------
  262. in_sequence : list or iterable container
  263. A sequence of integer node in-degrees
  264. out_sequence : list or iterable container
  265. A sequence of integer node out-degrees
  266. Returns
  267. -------
  268. valid : bool
  269. True if in and out-sequences are digraphic False if not.
  270. Notes
  271. -----
  272. This algorithm is from Kleitman and Wang [1]_.
  273. The worst case runtime is $O(s \times \log n)$ where $s$ and $n$ are the
  274. sum and length of the sequences respectively.
  275. References
  276. ----------
  277. .. [1] D.J. Kleitman and D.L. Wang
  278. Algorithms for Constructing Graphs and Digraphs with Given Valences
  279. and Factors, Discrete Mathematics, 6(1), pp. 79-88 (1973)
  280. """
  281. try:
  282. in_deg_sequence = nx.utils.make_list_of_ints(in_sequence)
  283. out_deg_sequence = nx.utils.make_list_of_ints(out_sequence)
  284. except nx.NetworkXError:
  285. return False
  286. # Process the sequences and form two heaps to store degree pairs with
  287. # either zero or non-zero out degrees
  288. sumin, sumout, nin, nout = 0, 0, len(in_deg_sequence), len(out_deg_sequence)
  289. maxn = max(nin, nout)
  290. maxin = 0
  291. if maxn == 0:
  292. return True
  293. stubheap, zeroheap = [], []
  294. for n in range(maxn):
  295. in_deg, out_deg = 0, 0
  296. if n < nout:
  297. out_deg = out_deg_sequence[n]
  298. if n < nin:
  299. in_deg = in_deg_sequence[n]
  300. if in_deg < 0 or out_deg < 0:
  301. return False
  302. sumin, sumout, maxin = sumin + in_deg, sumout + out_deg, max(maxin, in_deg)
  303. if in_deg > 0:
  304. stubheap.append((-1 * out_deg, -1 * in_deg))
  305. elif out_deg > 0:
  306. zeroheap.append(-1 * out_deg)
  307. if sumin != sumout:
  308. return False
  309. heapq.heapify(stubheap)
  310. heapq.heapify(zeroheap)
  311. modstubs = [(0, 0)] * (maxin + 1)
  312. # Successively reduce degree sequence by removing the maximum out degree
  313. while stubheap:
  314. # Take the first value in the sequence with non-zero in degree
  315. (freeout, freein) = heapq.heappop(stubheap)
  316. freein *= -1
  317. if freein > len(stubheap) + len(zeroheap):
  318. return False
  319. # Attach out stubs to the nodes with the most in stubs
  320. mslen = 0
  321. for i in range(freein):
  322. if zeroheap and (not stubheap or stubheap[0][0] > zeroheap[0]):
  323. stubout = heapq.heappop(zeroheap)
  324. stubin = 0
  325. else:
  326. (stubout, stubin) = heapq.heappop(stubheap)
  327. if stubout == 0:
  328. return False
  329. # Check if target is now totally connected
  330. if stubout + 1 < 0 or stubin < 0:
  331. modstubs[mslen] = (stubout + 1, stubin)
  332. mslen += 1
  333. # Add back the nodes to the heap that still have available stubs
  334. for i in range(mslen):
  335. stub = modstubs[i]
  336. if stub[1] < 0:
  337. heapq.heappush(stubheap, stub)
  338. else:
  339. heapq.heappush(zeroheap, stub[0])
  340. if freeout < 0:
  341. heapq.heappush(zeroheap, freeout)
  342. return True