755 lines
25 KiB
Python
755 lines
25 KiB
Python
|
"""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",
|
||
|
"node_clique_number",
|
||
|
"number_of_cliques",
|
||
|
"enumerate_all_cliques",
|
||
|
"max_weight_clique",
|
||
|
]
|
||
|
|
||
|
|
||
|
@not_implemented_for("directed")
|
||
|
@nx._dispatchable
|
||
|
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")
|
||
|
@nx._dispatchable
|
||
|
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?
|
||
|
@nx._dispatchable
|
||
|
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)
|
||
|
|
||
|
|
||
|
@nx._dispatchable(returns_graph=True)
|
||
|
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
|
||
|
|
||
|
|
||
|
@nx._dispatchable(returns_graph=True)
|
||
|
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
|
||
|
|
||
|
|
||
|
@nx._dispatchable
|
||
|
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.
|
||
|
|
||
|
Returns a single or list depending on input nodes.
|
||
|
Optional list of cliques can be input if already computed.
|
||
|
"""
|
||
|
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
|
||
|
|
||
|
|
||
|
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")
|
||
|
@nx._dispatchable(node_attrs="weight")
|
||
|
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] Warren, 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
|