123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912 |
- """Functions for finding and manipulating cliques.
- Finding the largest clique in a graph is NP-complete problem, so most of
- these algorithms have an exponential running time; for more information,
- see the Wikipedia article on the clique problem [1]_.
- .. [1] clique problem:: https://en.wikipedia.org/wiki/Clique_problem
- """
- from collections import defaultdict, deque
- from itertools import chain, combinations, islice
- import networkx as nx
- from networkx.utils import not_implemented_for
- __all__ = [
- "find_cliques",
- "find_cliques_recursive",
- "make_max_clique_graph",
- "make_clique_bipartite",
- "graph_clique_number",
- "graph_number_of_cliques",
- "node_clique_number",
- "number_of_cliques",
- "cliques_containing_node",
- "enumerate_all_cliques",
- "max_weight_clique",
- ]
- @not_implemented_for("directed")
- def enumerate_all_cliques(G):
- """Returns all cliques in an undirected graph.
- This function returns an iterator over cliques, each of which is a
- list of nodes. The iteration is ordered by cardinality of the
- cliques: first all cliques of size one, then all cliques of size
- two, etc.
- Parameters
- ----------
- G : NetworkX graph
- An undirected graph.
- Returns
- -------
- iterator
- An iterator over cliques, each of which is a list of nodes in
- `G`. The cliques are ordered according to size.
- Notes
- -----
- To obtain a list of all cliques, use
- `list(enumerate_all_cliques(G))`. However, be aware that in the
- worst-case, the length of this list can be exponential in the number
- of nodes in the graph (for example, when the graph is the complete
- graph). This function avoids storing all cliques in memory by only
- keeping current candidate node lists in memory during its search.
- The implementation is adapted from the algorithm by Zhang, et
- al. (2005) [1]_ to output all cliques discovered.
- This algorithm ignores self-loops and parallel edges, since cliques
- are not conventionally defined with such edges.
- References
- ----------
- .. [1] Yun Zhang, Abu-Khzam, F.N., Baldwin, N.E., Chesler, E.J.,
- Langston, M.A., Samatova, N.F.,
- "Genome-Scale Computational Approaches to Memory-Intensive
- Applications in Systems Biology".
- *Supercomputing*, 2005. Proceedings of the ACM/IEEE SC 2005
- Conference, pp. 12, 12--18 Nov. 2005.
- <https://doi.org/10.1109/SC.2005.29>.
- """
- index = {}
- nbrs = {}
- for u in G:
- index[u] = len(index)
- # Neighbors of u that appear after u in the iteration order of G.
- nbrs[u] = {v for v in G[u] if v not in index}
- queue = deque(([u], sorted(nbrs[u], key=index.__getitem__)) for u in G)
- # Loop invariants:
- # 1. len(base) is nondecreasing.
- # 2. (base + cnbrs) is sorted with respect to the iteration order of G.
- # 3. cnbrs is a set of common neighbors of nodes in base.
- while queue:
- base, cnbrs = map(list, queue.popleft())
- yield base
- for i, u in enumerate(cnbrs):
- # Use generators to reduce memory consumption.
- queue.append(
- (
- chain(base, [u]),
- filter(nbrs[u].__contains__, islice(cnbrs, i + 1, None)),
- )
- )
- @not_implemented_for("directed")
- def find_cliques(G, nodes=None):
- """Returns all maximal cliques in an undirected graph.
- For each node *n*, a *maximal clique for n* is a largest complete
- subgraph containing *n*. The largest maximal clique is sometimes
- called the *maximum clique*.
- This function returns an iterator over cliques, each of which is a
- list of nodes. It is an iterative implementation, so should not
- suffer from recursion depth issues.
- This function accepts a list of `nodes` and only the maximal cliques
- containing all of these `nodes` are returned. It can considerably speed up
- the running time if some specific cliques are desired.
- Parameters
- ----------
- G : NetworkX graph
- An undirected graph.
- nodes : list, optional (default=None)
- If provided, only yield *maximal cliques* containing all nodes in `nodes`.
- If `nodes` isn't a clique itself, a ValueError is raised.
- Returns
- -------
- iterator
- An iterator over maximal cliques, each of which is a list of
- nodes in `G`. If `nodes` is provided, only the maximal cliques
- containing all the nodes in `nodes` are returned. The order of
- cliques is arbitrary.
- Raises
- ------
- ValueError
- If `nodes` is not a clique.
- Examples
- --------
- >>> from pprint import pprint # For nice dict formatting
- >>> G = nx.karate_club_graph()
- >>> sum(1 for c in nx.find_cliques(G)) # The number of maximal cliques in G
- 36
- >>> max(nx.find_cliques(G), key=len) # The largest maximal clique in G
- [0, 1, 2, 3, 13]
- The size of the largest maximal clique is known as the *clique number* of
- the graph, which can be found directly with:
- >>> max(len(c) for c in nx.find_cliques(G))
- 5
- One can also compute the number of maximal cliques in `G` that contain a given
- node. The following produces a dictionary keyed by node whose
- values are the number of maximal cliques in `G` that contain the node:
- >>> pprint({n: sum(1 for c in nx.find_cliques(G) if n in c) for n in G})
- {0: 13,
- 1: 6,
- 2: 7,
- 3: 3,
- 4: 2,
- 5: 3,
- 6: 3,
- 7: 1,
- 8: 3,
- 9: 2,
- 10: 2,
- 11: 1,
- 12: 1,
- 13: 2,
- 14: 1,
- 15: 1,
- 16: 1,
- 17: 1,
- 18: 1,
- 19: 2,
- 20: 1,
- 21: 1,
- 22: 1,
- 23: 3,
- 24: 2,
- 25: 2,
- 26: 1,
- 27: 3,
- 28: 2,
- 29: 2,
- 30: 2,
- 31: 4,
- 32: 9,
- 33: 14}
- Or, similarly, the maximal cliques in `G` that contain a given node.
- For example, the 4 maximal cliques that contain node 31:
- >>> [c for c in nx.find_cliques(G) if 31 in c]
- [[0, 31], [33, 32, 31], [33, 28, 31], [24, 25, 31]]
- See Also
- --------
- find_cliques_recursive
- A recursive version of the same algorithm.
- Notes
- -----
- To obtain a list of all maximal cliques, use
- `list(find_cliques(G))`. However, be aware that in the worst-case,
- the length of this list can be exponential in the number of nodes in
- the graph. This function avoids storing all cliques in memory by
- only keeping current candidate node lists in memory during its search.
- This implementation is based on the algorithm published by Bron and
- Kerbosch (1973) [1]_, as adapted by Tomita, Tanaka and Takahashi
- (2006) [2]_ and discussed in Cazals and Karande (2008) [3]_. It
- essentially unrolls the recursion used in the references to avoid
- issues of recursion stack depth (for a recursive implementation, see
- :func:`find_cliques_recursive`).
- This algorithm ignores self-loops and parallel edges, since cliques
- are not conventionally defined with such edges.
- References
- ----------
- .. [1] Bron, C. and Kerbosch, J.
- "Algorithm 457: finding all cliques of an undirected graph".
- *Communications of the ACM* 16, 9 (Sep. 1973), 575--577.
- <http://portal.acm.org/citation.cfm?doid=362342.362367>
- .. [2] Etsuji Tomita, Akira Tanaka, Haruhisa Takahashi,
- "The worst-case time complexity for generating all maximal
- cliques and computational experiments",
- *Theoretical Computer Science*, Volume 363, Issue 1,
- Computing and Combinatorics,
- 10th Annual International Conference on
- Computing and Combinatorics (COCOON 2004), 25 October 2006, Pages 28--42
- <https://doi.org/10.1016/j.tcs.2006.06.015>
- .. [3] F. Cazals, C. Karande,
- "A note on the problem of reporting maximal cliques",
- *Theoretical Computer Science*,
- Volume 407, Issues 1--3, 6 November 2008, Pages 564--568,
- <https://doi.org/10.1016/j.tcs.2008.05.010>
- """
- if len(G) == 0:
- return
- adj = {u: {v for v in G[u] if v != u} for u in G}
- # Initialize Q with the given nodes and subg, cand with their nbrs
- Q = nodes[:] if nodes is not None else []
- cand = set(G)
- for node in Q:
- if node not in cand:
- raise ValueError(f"The given `nodes` {nodes} do not form a clique")
- cand &= adj[node]
- if not cand:
- yield Q[:]
- return
- subg = cand.copy()
- stack = []
- Q.append(None)
- u = max(subg, key=lambda u: len(cand & adj[u]))
- ext_u = cand - adj[u]
- try:
- while True:
- if ext_u:
- q = ext_u.pop()
- cand.remove(q)
- Q[-1] = q
- adj_q = adj[q]
- subg_q = subg & adj_q
- if not subg_q:
- yield Q[:]
- else:
- cand_q = cand & adj_q
- if cand_q:
- stack.append((subg, cand, ext_u))
- Q.append(None)
- subg = subg_q
- cand = cand_q
- u = max(subg, key=lambda u: len(cand & adj[u]))
- ext_u = cand - adj[u]
- else:
- Q.pop()
- subg, cand, ext_u = stack.pop()
- except IndexError:
- pass
- # TODO Should this also be not implemented for directed graphs?
- def find_cliques_recursive(G, nodes=None):
- """Returns all maximal cliques in a graph.
- For each node *v*, a *maximal clique for v* is a largest complete
- subgraph containing *v*. The largest maximal clique is sometimes
- called the *maximum clique*.
- This function returns an iterator over cliques, each of which is a
- list of nodes. It is a recursive implementation, so may suffer from
- recursion depth issues, but is included for pedagogical reasons.
- For a non-recursive implementation, see :func:`find_cliques`.
- This function accepts a list of `nodes` and only the maximal cliques
- containing all of these `nodes` are returned. It can considerably speed up
- the running time if some specific cliques are desired.
- Parameters
- ----------
- G : NetworkX graph
- nodes : list, optional (default=None)
- If provided, only yield *maximal cliques* containing all nodes in `nodes`.
- If `nodes` isn't a clique itself, a ValueError is raised.
- Returns
- -------
- iterator
- An iterator over maximal cliques, each of which is a list of
- nodes in `G`. If `nodes` is provided, only the maximal cliques
- containing all the nodes in `nodes` are yielded. The order of
- cliques is arbitrary.
- Raises
- ------
- ValueError
- If `nodes` is not a clique.
- See Also
- --------
- find_cliques
- An iterative version of the same algorithm. See docstring for examples.
- Notes
- -----
- To obtain a list of all maximal cliques, use
- `list(find_cliques_recursive(G))`. However, be aware that in the
- worst-case, the length of this list can be exponential in the number
- of nodes in the graph. This function avoids storing all cliques in memory
- by only keeping current candidate node lists in memory during its search.
- This implementation is based on the algorithm published by Bron and
- Kerbosch (1973) [1]_, as adapted by Tomita, Tanaka and Takahashi
- (2006) [2]_ and discussed in Cazals and Karande (2008) [3]_. For a
- non-recursive implementation, see :func:`find_cliques`.
- This algorithm ignores self-loops and parallel edges, since cliques
- are not conventionally defined with such edges.
- References
- ----------
- .. [1] Bron, C. and Kerbosch, J.
- "Algorithm 457: finding all cliques of an undirected graph".
- *Communications of the ACM* 16, 9 (Sep. 1973), 575--577.
- <http://portal.acm.org/citation.cfm?doid=362342.362367>
- .. [2] Etsuji Tomita, Akira Tanaka, Haruhisa Takahashi,
- "The worst-case time complexity for generating all maximal
- cliques and computational experiments",
- *Theoretical Computer Science*, Volume 363, Issue 1,
- Computing and Combinatorics,
- 10th Annual International Conference on
- Computing and Combinatorics (COCOON 2004), 25 October 2006, Pages 28--42
- <https://doi.org/10.1016/j.tcs.2006.06.015>
- .. [3] F. Cazals, C. Karande,
- "A note on the problem of reporting maximal cliques",
- *Theoretical Computer Science*,
- Volume 407, Issues 1--3, 6 November 2008, Pages 564--568,
- <https://doi.org/10.1016/j.tcs.2008.05.010>
- """
- if len(G) == 0:
- return iter([])
- adj = {u: {v for v in G[u] if v != u} for u in G}
- # Initialize Q with the given nodes and subg, cand with their nbrs
- Q = nodes[:] if nodes is not None else []
- cand_init = set(G)
- for node in Q:
- if node not in cand_init:
- raise ValueError(f"The given `nodes` {nodes} do not form a clique")
- cand_init &= adj[node]
- if not cand_init:
- return iter([Q])
- subg_init = cand_init.copy()
- def expand(subg, cand):
- u = max(subg, key=lambda u: len(cand & adj[u]))
- for q in cand - adj[u]:
- cand.remove(q)
- Q.append(q)
- adj_q = adj[q]
- subg_q = subg & adj_q
- if not subg_q:
- yield Q[:]
- else:
- cand_q = cand & adj_q
- if cand_q:
- yield from expand(subg_q, cand_q)
- Q.pop()
- return expand(subg_init, cand_init)
- def make_max_clique_graph(G, create_using=None):
- """Returns the maximal clique graph of the given graph.
- The nodes of the maximal clique graph of `G` are the cliques of
- `G` and an edge joins two cliques if the cliques are not disjoint.
- Parameters
- ----------
- G : NetworkX graph
- create_using : NetworkX graph constructor, optional (default=nx.Graph)
- Graph type to create. If graph instance, then cleared before populated.
- Returns
- -------
- NetworkX graph
- A graph whose nodes are the cliques of `G` and whose edges
- join two cliques if they are not disjoint.
- Notes
- -----
- This function behaves like the following code::
- import networkx as nx
- G = nx.make_clique_bipartite(G)
- cliques = [v for v in G.nodes() if G.nodes[v]['bipartite'] == 0]
- G = nx.bipartite.projected_graph(G, cliques)
- G = nx.relabel_nodes(G, {-v: v - 1 for v in G})
- It should be faster, though, since it skips all the intermediate
- steps.
- """
- if create_using is None:
- B = G.__class__()
- else:
- B = nx.empty_graph(0, create_using)
- cliques = list(enumerate(set(c) for c in find_cliques(G)))
- # Add a numbered node for each clique.
- B.add_nodes_from(i for i, c in cliques)
- # Join cliques by an edge if they share a node.
- clique_pairs = combinations(cliques, 2)
- B.add_edges_from((i, j) for (i, c1), (j, c2) in clique_pairs if c1 & c2)
- return B
- def make_clique_bipartite(G, fpos=None, create_using=None, name=None):
- """Returns the bipartite clique graph corresponding to `G`.
- In the returned bipartite graph, the "bottom" nodes are the nodes of
- `G` and the "top" nodes represent the maximal cliques of `G`.
- There is an edge from node *v* to clique *C* in the returned graph
- if and only if *v* is an element of *C*.
- Parameters
- ----------
- G : NetworkX graph
- An undirected graph.
- fpos : bool
- If True or not None, the returned graph will have an
- additional attribute, `pos`, a dictionary mapping node to
- position in the Euclidean plane.
- create_using : NetworkX graph constructor, optional (default=nx.Graph)
- Graph type to create. If graph instance, then cleared before populated.
- Returns
- -------
- NetworkX graph
- A bipartite graph whose "bottom" set is the nodes of the graph
- `G`, whose "top" set is the cliques of `G`, and whose edges
- join nodes of `G` to the cliques that contain them.
- The nodes of the graph `G` have the node attribute
- 'bipartite' set to 1 and the nodes representing cliques
- have the node attribute 'bipartite' set to 0, as is the
- convention for bipartite graphs in NetworkX.
- """
- B = nx.empty_graph(0, create_using)
- B.clear()
- # The "bottom" nodes in the bipartite graph are the nodes of the
- # original graph, G.
- B.add_nodes_from(G, bipartite=1)
- for i, cl in enumerate(find_cliques(G)):
- # The "top" nodes in the bipartite graph are the cliques. These
- # nodes get negative numbers as labels.
- name = -i - 1
- B.add_node(name, bipartite=0)
- B.add_edges_from((v, name) for v in cl)
- return B
- def graph_clique_number(G, cliques=None):
- """Returns the clique number of the graph.
- The *clique number* of a graph is the size of the largest clique in
- the graph.
- .. deprecated:: 3.0
- graph_clique_number is deprecated in NetworkX 3.0 and will be removed
- in v3.2. The graph clique number can be computed directly with::
- max(len(c) for c in nx.find_cliques(G))
- Parameters
- ----------
- G : NetworkX graph
- An undirected graph.
- cliques : list
- A list of cliques, each of which is itself a list of nodes. If
- not specified, the list of all cliques will be computed, as by
- :func:`find_cliques`.
- Returns
- -------
- int
- The size of the largest clique in `G`.
- Notes
- -----
- You should provide `cliques` if you have already computed the list
- of maximal cliques, in order to avoid an exponential time search for
- maximal cliques.
- """
- import warnings
- warnings.warn(
- (
- "\n\ngraph_clique_number is deprecated and will be removed.\n"
- "Use: ``max(len(c) for c in nx.find_cliques(G))`` instead."
- ),
- DeprecationWarning,
- stacklevel=2,
- )
- if len(G.nodes) < 1:
- return 0
- if cliques is None:
- cliques = find_cliques(G)
- return max([len(c) for c in cliques] or [1])
- def graph_number_of_cliques(G, cliques=None):
- """Returns the number of maximal cliques in the graph.
- .. deprecated:: 3.0
- graph_number_of_cliques is deprecated and will be removed in v3.2.
- The number of maximal cliques can be computed directly with::
- sum(1 for _ in nx.find_cliques(G))
- Parameters
- ----------
- G : NetworkX graph
- An undirected graph.
- cliques : list
- A list of cliques, each of which is itself a list of nodes. If
- not specified, the list of all cliques will be computed, as by
- :func:`find_cliques`.
- Returns
- -------
- int
- The number of maximal cliques in `G`.
- Notes
- -----
- You should provide `cliques` if you have already computed the list
- of maximal cliques, in order to avoid an exponential time search for
- maximal cliques.
- """
- import warnings
- warnings.warn(
- (
- "\n\ngraph_number_of_cliques is deprecated and will be removed.\n"
- "Use: ``sum(1 for _ in nx.find_cliques(G))`` instead."
- ),
- DeprecationWarning,
- stacklevel=2,
- )
- if cliques is None:
- cliques = list(find_cliques(G))
- return len(cliques)
- def node_clique_number(G, nodes=None, cliques=None, separate_nodes=False):
- """Returns the size of the largest maximal clique containing each given node.
- Returns a single or list depending on input nodes.
- An optional list of cliques can be input if already computed.
- Parameters
- ----------
- G : NetworkX graph
- An undirected graph.
- cliques : list, optional (default=None)
- A list of cliques, each of which is itself a list of nodes.
- If not specified, the list of all cliques will be computed
- using :func:`find_cliques`.
- Returns
- -------
- int or dict
- If `nodes` is a single node, returns the size of the
- largest maximal clique in `G` containing that node.
- Otherwise return a dict keyed by node to the size
- of the largest maximal clique containing that node.
- See Also
- --------
- find_cliques
- find_cliques yields the maximal cliques of G.
- It accepts a `nodes` argument which restricts consideration to
- maximal cliques containing all the given `nodes`.
- The search for the cliques is optimized for `nodes`.
- """
- if cliques is None:
- if nodes is not None:
- # Use ego_graph to decrease size of graph
- # check for single node
- if nodes in G:
- return max(len(c) for c in find_cliques(nx.ego_graph(G, nodes)))
- # handle multiple nodes
- return {
- n: max(len(c) for c in find_cliques(nx.ego_graph(G, n))) for n in nodes
- }
- # nodes is None--find all cliques
- cliques = list(find_cliques(G))
- # single node requested
- if nodes in G:
- return max(len(c) for c in cliques if nodes in c)
- # multiple nodes requested
- # preprocess all nodes (faster than one at a time for even 2 nodes)
- size_for_n = defaultdict(int)
- for c in cliques:
- size_of_c = len(c)
- for n in c:
- if size_for_n[n] < size_of_c:
- size_for_n[n] = size_of_c
- if nodes is None:
- return size_for_n
- return {n: size_for_n[n] for n in nodes}
- def number_of_cliques(G, nodes=None, cliques=None):
- """Returns the number of maximal cliques for each node.
- .. deprecated:: 3.0
- number_of_cliques is deprecated and will be removed in v3.2.
- Use the result of `find_cliques` directly to compute the number of
- cliques containing each node::
- {n: sum(1 for c in nx.find_cliques(G) if n in c) for n in G}
- Returns a single or list depending on input nodes.
- Optional list of cliques can be input if already computed.
- """
- import warnings
- warnings.warn(
- (
- "\n\nnumber_of_cliques is deprecated and will be removed.\n"
- "Use the result of find_cliques directly to compute the number\n"
- "of cliques containing each node:\n\n"
- " {n: sum(1 for c in nx.find_cliques(G) if n in c) for n in G}\n\n"
- ),
- DeprecationWarning,
- stacklevel=2,
- )
- if cliques is None:
- cliques = list(find_cliques(G))
- if nodes is None:
- nodes = list(G.nodes()) # none, get entire graph
- if not isinstance(nodes, list): # check for a list
- v = nodes
- # assume it is a single value
- numcliq = len([1 for c in cliques if v in c])
- else:
- numcliq = {}
- for v in nodes:
- numcliq[v] = len([1 for c in cliques if v in c])
- return numcliq
- def cliques_containing_node(G, nodes=None, cliques=None):
- """Returns a list of cliques containing the given node.
- .. deprecated:: 3.0
- cliques_containing_node is deprecated and will be removed in 3.2.
- Use the result of `find_cliques` directly to compute the cliques that
- contain each node::
- {n: [c for c in nx.find_cliques(G) if n in c] for n in G}
- Returns a single list or list of lists depending on input nodes.
- Optional list of cliques can be input if already computed.
- """
- import warnings
- warnings.warn(
- (
- "\n\ncliques_containing_node is deprecated and will be removed.\n"
- "Use the result of find_cliques directly to compute maximal cliques\n"
- "containing each node:\n\n"
- " {n: [c for c in nx.find_cliques(G) if n in c] for n in G}\n\n"
- ),
- DeprecationWarning,
- stacklevel=2,
- )
- if cliques is None:
- cliques = list(find_cliques(G))
- if nodes is None:
- nodes = list(G.nodes()) # none, get entire graph
- if not isinstance(nodes, list): # check for a list
- v = nodes
- # assume it is a single value
- vcliques = [c for c in cliques if v in c]
- else:
- vcliques = {}
- for v in nodes:
- vcliques[v] = [c for c in cliques if v in c]
- return vcliques
- class MaxWeightClique:
- """A class for the maximum weight clique algorithm.
- This class is a helper for the `max_weight_clique` function. The class
- should not normally be used directly.
- Parameters
- ----------
- G : NetworkX graph
- The undirected graph for which a maximum weight clique is sought
- weight : string or None, optional (default='weight')
- The node attribute that holds the integer value used as a weight.
- If None, then each node has weight 1.
- Attributes
- ----------
- G : NetworkX graph
- The undirected graph for which a maximum weight clique is sought
- node_weights: dict
- The weight of each node
- incumbent_nodes : list
- The nodes of the incumbent clique (the best clique found so far)
- incumbent_weight: int
- The weight of the incumbent clique
- """
- def __init__(self, G, weight):
- self.G = G
- self.incumbent_nodes = []
- self.incumbent_weight = 0
- if weight is None:
- self.node_weights = {v: 1 for v in G.nodes()}
- else:
- for v in G.nodes():
- if weight not in G.nodes[v]:
- errmsg = f"Node {v!r} does not have the requested weight field."
- raise KeyError(errmsg)
- if not isinstance(G.nodes[v][weight], int):
- errmsg = f"The {weight!r} field of node {v!r} is not an integer."
- raise ValueError(errmsg)
- self.node_weights = {v: G.nodes[v][weight] for v in G.nodes()}
- def update_incumbent_if_improved(self, C, C_weight):
- """Update the incumbent if the node set C has greater weight.
- C is assumed to be a clique.
- """
- if C_weight > self.incumbent_weight:
- self.incumbent_nodes = C[:]
- self.incumbent_weight = C_weight
- def greedily_find_independent_set(self, P):
- """Greedily find an independent set of nodes from a set of
- nodes P."""
- independent_set = []
- P = P[:]
- while P:
- v = P[0]
- independent_set.append(v)
- P = [w for w in P if v != w and not self.G.has_edge(v, w)]
- return independent_set
- def find_branching_nodes(self, P, target):
- """Find a set of nodes to branch on."""
- residual_wt = {v: self.node_weights[v] for v in P}
- total_wt = 0
- P = P[:]
- while P:
- independent_set = self.greedily_find_independent_set(P)
- min_wt_in_class = min(residual_wt[v] for v in independent_set)
- total_wt += min_wt_in_class
- if total_wt > target:
- break
- for v in independent_set:
- residual_wt[v] -= min_wt_in_class
- P = [v for v in P if residual_wt[v] != 0]
- return P
- def expand(self, C, C_weight, P):
- """Look for the best clique that contains all the nodes in C and zero or
- more of the nodes in P, backtracking if it can be shown that no such
- clique has greater weight than the incumbent.
- """
- self.update_incumbent_if_improved(C, C_weight)
- branching_nodes = self.find_branching_nodes(P, self.incumbent_weight - C_weight)
- while branching_nodes:
- v = branching_nodes.pop()
- P.remove(v)
- new_C = C + [v]
- new_C_weight = C_weight + self.node_weights[v]
- new_P = [w for w in P if self.G.has_edge(v, w)]
- self.expand(new_C, new_C_weight, new_P)
- def find_max_weight_clique(self):
- """Find a maximum weight clique."""
- # Sort nodes in reverse order of degree for speed
- nodes = sorted(self.G.nodes(), key=lambda v: self.G.degree(v), reverse=True)
- nodes = [v for v in nodes if self.node_weights[v] > 0]
- self.expand([], 0, nodes)
- @not_implemented_for("directed")
- def max_weight_clique(G, weight="weight"):
- """Find a maximum weight clique in G.
- A *clique* in a graph is a set of nodes such that every two distinct nodes
- are adjacent. The *weight* of a clique is the sum of the weights of its
- nodes. A *maximum weight clique* of graph G is a clique C in G such that
- no clique in G has weight greater than the weight of C.
- Parameters
- ----------
- G : NetworkX graph
- Undirected graph
- weight : string or None, optional (default='weight')
- The node attribute that holds the integer value used as a weight.
- If None, then each node has weight 1.
- Returns
- -------
- clique : list
- the nodes of a maximum weight clique
- weight : int
- the weight of a maximum weight clique
- Notes
- -----
- The implementation is recursive, and therefore it may run into recursion
- depth issues if G contains a clique whose number of nodes is close to the
- recursion depth limit.
- At each search node, the algorithm greedily constructs a weighted
- independent set cover of part of the graph in order to find a small set of
- nodes on which to branch. The algorithm is very similar to the algorithm
- of Tavares et al. [1]_, other than the fact that the NetworkX version does
- not use bitsets. This style of algorithm for maximum weight clique (and
- maximum weight independent set, which is the same problem but on the
- complement graph) has a decades-long history. See Algorithm B of Warren
- and Hicks [2]_ and the references in that paper.
- References
- ----------
- .. [1] Tavares, W.A., Neto, M.B.C., Rodrigues, C.D., Michelon, P.: Um
- algoritmo de branch and bound para o problema da clique máxima
- ponderada. Proceedings of XLVII SBPO 1 (2015).
- .. [2] Warrent, Jeffrey S, Hicks, Illya V.: Combinatorial Branch-and-Bound
- for the Maximum Weight Independent Set Problem. Technical Report,
- Texas A&M University (2016).
- """
- mwc = MaxWeightClique(G, weight)
- mwc.find_max_weight_clique()
- return mwc.incumbent_nodes, mwc.incumbent_weight
|