tournament.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. """Functions concerning tournament graphs.
  2. A `tournament graph`_ is a complete oriented graph. In other words, it
  3. is a directed graph in which there is exactly one directed edge joining
  4. each pair of distinct nodes. For each function in this module that
  5. accepts a graph as input, you must provide a tournament graph. The
  6. responsibility is on the caller to ensure that the graph is a tournament
  7. graph.
  8. To access the functions in this module, you must access them through the
  9. :mod:`networkx.algorithms.tournament` module::
  10. >>> from networkx.algorithms import tournament
  11. >>> G = nx.DiGraph([(0, 1), (1, 2), (2, 0)])
  12. >>> tournament.is_tournament(G)
  13. True
  14. .. _tournament graph: https://en.wikipedia.org/wiki/Tournament_%28graph_theory%29
  15. """
  16. from itertools import combinations
  17. import networkx as nx
  18. from networkx.algorithms.simple_paths import is_simple_path as is_path
  19. from networkx.utils import arbitrary_element, not_implemented_for, py_random_state
  20. __all__ = [
  21. "hamiltonian_path",
  22. "is_reachable",
  23. "is_strongly_connected",
  24. "is_tournament",
  25. "random_tournament",
  26. "score_sequence",
  27. ]
  28. def index_satisfying(iterable, condition):
  29. """Returns the index of the first element in `iterable` that
  30. satisfies the given condition.
  31. If no such element is found (that is, when the iterable is
  32. exhausted), this returns the length of the iterable (that is, one
  33. greater than the last index of the iterable).
  34. `iterable` must not be empty. If `iterable` is empty, this
  35. function raises :exc:`ValueError`.
  36. """
  37. # Pre-condition: iterable must not be empty.
  38. for i, x in enumerate(iterable):
  39. if condition(x):
  40. return i
  41. # If we reach the end of the iterable without finding an element
  42. # that satisfies the condition, return the length of the iterable,
  43. # which is one greater than the index of its last element. If the
  44. # iterable was empty, `i` will not be defined, so we raise an
  45. # exception.
  46. try:
  47. return i + 1
  48. except NameError as err:
  49. raise ValueError("iterable must be non-empty") from err
  50. @nx._dispatch
  51. @not_implemented_for("undirected")
  52. @not_implemented_for("multigraph")
  53. def is_tournament(G):
  54. """Returns True if and only if `G` is a tournament.
  55. A tournament is a directed graph, with neither self-loops nor
  56. multi-edges, in which there is exactly one directed edge joining
  57. each pair of distinct nodes.
  58. Parameters
  59. ----------
  60. G : NetworkX graph
  61. A directed graph representing a tournament.
  62. Returns
  63. -------
  64. bool
  65. Whether the given graph is a tournament graph.
  66. Examples
  67. --------
  68. >>> from networkx.algorithms import tournament
  69. >>> G = nx.DiGraph([(0, 1), (1, 2), (2, 0)])
  70. >>> tournament.is_tournament(G)
  71. True
  72. Notes
  73. -----
  74. Some definitions require a self-loop on each node, but that is not
  75. the convention used here.
  76. """
  77. # In a tournament, there is exactly one directed edge joining each pair.
  78. return (
  79. all((v in G[u]) ^ (u in G[v]) for u, v in combinations(G, 2))
  80. and nx.number_of_selfloops(G) == 0
  81. )
  82. @not_implemented_for("undirected")
  83. @not_implemented_for("multigraph")
  84. def hamiltonian_path(G):
  85. """Returns a Hamiltonian path in the given tournament graph.
  86. Each tournament has a Hamiltonian path. If furthermore, the
  87. tournament is strongly connected, then the returned Hamiltonian path
  88. is a Hamiltonian cycle (by joining the endpoints of the path).
  89. Parameters
  90. ----------
  91. G : NetworkX graph
  92. A directed graph representing a tournament.
  93. Returns
  94. -------
  95. path : list
  96. A list of nodes which form a Hamiltonian path in `G`.
  97. Examples
  98. --------
  99. >>> from networkx.algorithms import tournament
  100. >>> G = nx.DiGraph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)])
  101. >>> tournament.hamiltonian_path(G)
  102. [0, 1, 2, 3]
  103. Notes
  104. -----
  105. This is a recursive implementation with an asymptotic running time
  106. of $O(n^2)$, ignoring multiplicative polylogarithmic factors, where
  107. $n$ is the number of nodes in the graph.
  108. """
  109. if len(G) == 0:
  110. return []
  111. if len(G) == 1:
  112. return [arbitrary_element(G)]
  113. v = arbitrary_element(G)
  114. hampath = hamiltonian_path(G.subgraph(set(G) - {v}))
  115. # Get the index of the first node in the path that does *not* have
  116. # an edge to `v`, then insert `v` before that node.
  117. index = index_satisfying(hampath, lambda u: v not in G[u])
  118. hampath.insert(index, v)
  119. return hampath
  120. @py_random_state(1)
  121. def random_tournament(n, seed=None):
  122. r"""Returns a random tournament graph on `n` nodes.
  123. Parameters
  124. ----------
  125. n : int
  126. The number of nodes in the returned graph.
  127. seed : integer, random_state, or None (default)
  128. Indicator of random number generation state.
  129. See :ref:`Randomness<randomness>`.
  130. Returns
  131. -------
  132. G : DiGraph
  133. A tournament on `n` nodes, with exactly one directed edge joining
  134. each pair of distinct nodes.
  135. Notes
  136. -----
  137. This algorithm adds, for each pair of distinct nodes, an edge with
  138. uniformly random orientation. In other words, `\binom{n}{2}` flips
  139. of an unbiased coin decide the orientations of the edges in the
  140. graph.
  141. """
  142. # Flip an unbiased coin for each pair of distinct nodes.
  143. coins = (seed.random() for i in range((n * (n - 1)) // 2))
  144. pairs = combinations(range(n), 2)
  145. edges = ((u, v) if r < 0.5 else (v, u) for (u, v), r in zip(pairs, coins))
  146. return nx.DiGraph(edges)
  147. @nx._dispatch
  148. @not_implemented_for("undirected")
  149. @not_implemented_for("multigraph")
  150. def score_sequence(G):
  151. """Returns the score sequence for the given tournament graph.
  152. The score sequence is the sorted list of the out-degrees of the
  153. nodes of the graph.
  154. Parameters
  155. ----------
  156. G : NetworkX graph
  157. A directed graph representing a tournament.
  158. Returns
  159. -------
  160. list
  161. A sorted list of the out-degrees of the nodes of `G`.
  162. Examples
  163. --------
  164. >>> from networkx.algorithms import tournament
  165. >>> G = nx.DiGraph([(1, 0), (1, 3), (0, 2), (0, 3), (2, 1), (3, 2)])
  166. >>> tournament.score_sequence(G)
  167. [1, 1, 2, 2]
  168. """
  169. return sorted(d for v, d in G.out_degree())
  170. @nx._dispatch
  171. @not_implemented_for("undirected")
  172. @not_implemented_for("multigraph")
  173. def tournament_matrix(G):
  174. r"""Returns the tournament matrix for the given tournament graph.
  175. This function requires SciPy.
  176. The *tournament matrix* of a tournament graph with edge set *E* is
  177. the matrix *T* defined by
  178. .. math::
  179. T_{i j} =
  180. \begin{cases}
  181. +1 & \text{if } (i, j) \in E \\
  182. -1 & \text{if } (j, i) \in E \\
  183. 0 & \text{if } i == j.
  184. \end{cases}
  185. An equivalent definition is `T = A - A^T`, where *A* is the
  186. adjacency matrix of the graph `G`.
  187. Parameters
  188. ----------
  189. G : NetworkX graph
  190. A directed graph representing a tournament.
  191. Returns
  192. -------
  193. SciPy sparse array
  194. The tournament matrix of the tournament graph `G`.
  195. Raises
  196. ------
  197. ImportError
  198. If SciPy is not available.
  199. """
  200. A = nx.adjacency_matrix(G)
  201. return A - A.T
  202. @not_implemented_for("undirected")
  203. @not_implemented_for("multigraph")
  204. def is_reachable(G, s, t):
  205. """Decides whether there is a path from `s` to `t` in the
  206. tournament.
  207. This function is more theoretically efficient than the reachability
  208. checks than the shortest path algorithms in
  209. :mod:`networkx.algorithms.shortest_paths`.
  210. The given graph **must** be a tournament, otherwise this function's
  211. behavior is undefined.
  212. Parameters
  213. ----------
  214. G : NetworkX graph
  215. A directed graph representing a tournament.
  216. s : node
  217. A node in the graph.
  218. t : node
  219. A node in the graph.
  220. Returns
  221. -------
  222. bool
  223. Whether there is a path from `s` to `t` in `G`.
  224. Examples
  225. --------
  226. >>> from networkx.algorithms import tournament
  227. >>> G = nx.DiGraph([(1, 0), (1, 3), (1, 2), (2, 3), (2, 0), (3, 0)])
  228. >>> tournament.is_reachable(G, 1, 3)
  229. True
  230. >>> tournament.is_reachable(G, 3, 2)
  231. False
  232. Notes
  233. -----
  234. Although this function is more theoretically efficient than the
  235. generic shortest path functions, a speedup requires the use of
  236. parallelism. Though it may in the future, the current implementation
  237. does not use parallelism, thus you may not see much of a speedup.
  238. This algorithm comes from [1].
  239. References
  240. ----------
  241. .. [1] Tantau, Till.
  242. "A note on the complexity of the reachability problem for
  243. tournaments."
  244. *Electronic Colloquium on Computational Complexity*. 2001.
  245. <http://eccc.hpi-web.de/report/2001/092/>
  246. """
  247. def two_neighborhood(G, v):
  248. """Returns the set of nodes at distance at most two from `v`.
  249. `G` must be a graph and `v` a node in that graph.
  250. The returned set includes the nodes at distance zero (that is,
  251. the node `v` itself), the nodes at distance one (that is, the
  252. out-neighbors of `v`), and the nodes at distance two.
  253. """
  254. # TODO This is trivially parallelizable.
  255. return {
  256. x for x in G if x == v or x in G[v] or any(is_path(G, [v, z, x]) for z in G)
  257. }
  258. def is_closed(G, nodes):
  259. """Decides whether the given set of nodes is closed.
  260. A set *S* of nodes is *closed* if for each node *u* in the graph
  261. not in *S* and for each node *v* in *S*, there is an edge from
  262. *u* to *v*.
  263. """
  264. # TODO This is trivially parallelizable.
  265. return all(v in G[u] for u in set(G) - nodes for v in nodes)
  266. # TODO This is trivially parallelizable.
  267. neighborhoods = [two_neighborhood(G, v) for v in G]
  268. return all(not (is_closed(G, S) and s in S and t not in S) for S in neighborhoods)
  269. @not_implemented_for("undirected")
  270. @not_implemented_for("multigraph")
  271. def is_strongly_connected(G):
  272. """Decides whether the given tournament is strongly connected.
  273. This function is more theoretically efficient than the
  274. :func:`~networkx.algorithms.components.is_strongly_connected`
  275. function.
  276. The given graph **must** be a tournament, otherwise this function's
  277. behavior is undefined.
  278. Parameters
  279. ----------
  280. G : NetworkX graph
  281. A directed graph representing a tournament.
  282. Returns
  283. -------
  284. bool
  285. Whether the tournament is strongly connected.
  286. Examples
  287. --------
  288. >>> from networkx.algorithms import tournament
  289. >>> G = nx.DiGraph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3), (3, 0)])
  290. >>> tournament.is_strongly_connected(G)
  291. True
  292. >>> G.remove_edge(1, 3)
  293. >>> tournament.is_strongly_connected(G)
  294. False
  295. Notes
  296. -----
  297. Although this function is more theoretically efficient than the
  298. generic strong connectivity function, a speedup requires the use of
  299. parallelism. Though it may in the future, the current implementation
  300. does not use parallelism, thus you may not see much of a speedup.
  301. This algorithm comes from [1].
  302. References
  303. ----------
  304. .. [1] Tantau, Till.
  305. "A note on the complexity of the reachability problem for
  306. tournaments."
  307. *Electronic Colloquium on Computational Complexity*. 2001.
  308. <http://eccc.hpi-web.de/report/2001/092/>
  309. """
  310. # TODO This is trivially parallelizable.
  311. return all(is_reachable(G, u, v) for u in G for v in G)