graphical.py 15 KB

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