tournament.py 11 KB

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