up follow livre
This commit is contained in:
parent
70a5c3465c
commit
cffb31c1ef
12198 changed files with 2562132 additions and 35 deletions
53
venv/lib/python3.13/site-packages/networkx/__init__.py
Normal file
53
venv/lib/python3.13/site-packages/networkx/__init__.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""
|
||||
NetworkX
|
||||
========
|
||||
|
||||
NetworkX is a Python package for the creation, manipulation, and study of the
|
||||
structure, dynamics, and functions of complex networks.
|
||||
|
||||
See https://networkx.org for complete documentation.
|
||||
"""
|
||||
|
||||
__version__ = "3.5"
|
||||
|
||||
|
||||
# These are imported in order as listed
|
||||
from networkx.lazy_imports import _lazy_import
|
||||
|
||||
from networkx.exception import *
|
||||
|
||||
from networkx import utils
|
||||
from networkx.utils import _clear_cache, _dispatchable
|
||||
|
||||
# load_and_call entry_points, set configs
|
||||
config = utils.backends._set_configs_from_environment()
|
||||
utils.config = utils.configs.config = config # type: ignore[attr-defined]
|
||||
|
||||
from networkx import classes
|
||||
from networkx.classes import filters
|
||||
from networkx.classes import *
|
||||
|
||||
from networkx import convert
|
||||
from networkx.convert import *
|
||||
|
||||
from networkx import convert_matrix
|
||||
from networkx.convert_matrix import *
|
||||
|
||||
from networkx import relabel
|
||||
from networkx.relabel import *
|
||||
|
||||
from networkx import generators
|
||||
from networkx.generators import *
|
||||
|
||||
from networkx import readwrite
|
||||
from networkx.readwrite import *
|
||||
|
||||
# Need to test with SciPy, when available
|
||||
from networkx import algorithms
|
||||
from networkx.algorithms import *
|
||||
|
||||
from networkx import linalg
|
||||
from networkx.linalg import *
|
||||
|
||||
from networkx import drawing
|
||||
from networkx.drawing import *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,133 @@
|
|||
from networkx.algorithms.assortativity import *
|
||||
from networkx.algorithms.asteroidal import *
|
||||
from networkx.algorithms.boundary import *
|
||||
from networkx.algorithms.broadcasting import *
|
||||
from networkx.algorithms.bridges import *
|
||||
from networkx.algorithms.chains import *
|
||||
from networkx.algorithms.centrality import *
|
||||
from networkx.algorithms.chordal import *
|
||||
from networkx.algorithms.cluster import *
|
||||
from networkx.algorithms.clique import *
|
||||
from networkx.algorithms.communicability_alg import *
|
||||
from networkx.algorithms.components import *
|
||||
from networkx.algorithms.coloring import *
|
||||
from networkx.algorithms.core import *
|
||||
from networkx.algorithms.covering import *
|
||||
from networkx.algorithms.cycles import *
|
||||
from networkx.algorithms.cuts import *
|
||||
from networkx.algorithms.d_separation import *
|
||||
from networkx.algorithms.dag import *
|
||||
from networkx.algorithms.distance_measures import *
|
||||
from networkx.algorithms.distance_regular import *
|
||||
from networkx.algorithms.dominance import *
|
||||
from networkx.algorithms.dominating import *
|
||||
from networkx.algorithms.efficiency_measures import *
|
||||
from networkx.algorithms.euler import *
|
||||
from networkx.algorithms.graphical import *
|
||||
from networkx.algorithms.hierarchy import *
|
||||
from networkx.algorithms.hybrid import *
|
||||
from networkx.algorithms.link_analysis import *
|
||||
from networkx.algorithms.link_prediction import *
|
||||
from networkx.algorithms.lowest_common_ancestors import *
|
||||
from networkx.algorithms.isolate import *
|
||||
from networkx.algorithms.matching import *
|
||||
from networkx.algorithms.minors import *
|
||||
from networkx.algorithms.mis import *
|
||||
from networkx.algorithms.moral import *
|
||||
from networkx.algorithms.non_randomness import *
|
||||
from networkx.algorithms.operators import *
|
||||
from networkx.algorithms.planarity import *
|
||||
from networkx.algorithms.planar_drawing import *
|
||||
from networkx.algorithms.polynomials import *
|
||||
from networkx.algorithms.reciprocity import *
|
||||
from networkx.algorithms.regular import *
|
||||
from networkx.algorithms.richclub import *
|
||||
from networkx.algorithms.shortest_paths import *
|
||||
from networkx.algorithms.similarity import *
|
||||
from networkx.algorithms.graph_hashing import *
|
||||
from networkx.algorithms.simple_paths import *
|
||||
from networkx.algorithms.smallworld import *
|
||||
from networkx.algorithms.smetric import *
|
||||
from networkx.algorithms.structuralholes import *
|
||||
from networkx.algorithms.sparsifiers import *
|
||||
from networkx.algorithms.summarization import *
|
||||
from networkx.algorithms.swap import *
|
||||
from networkx.algorithms.time_dependent import *
|
||||
from networkx.algorithms.traversal import *
|
||||
from networkx.algorithms.triads import *
|
||||
from networkx.algorithms.vitality import *
|
||||
from networkx.algorithms.voronoi import *
|
||||
from networkx.algorithms.walks import *
|
||||
from networkx.algorithms.wiener import *
|
||||
|
||||
# Make certain subpackages available to the user as direct imports from
|
||||
# the `networkx` namespace.
|
||||
from networkx.algorithms import approximation
|
||||
from networkx.algorithms import assortativity
|
||||
from networkx.algorithms import bipartite
|
||||
from networkx.algorithms import node_classification
|
||||
from networkx.algorithms import centrality
|
||||
from networkx.algorithms import chordal
|
||||
from networkx.algorithms import cluster
|
||||
from networkx.algorithms import clique
|
||||
from networkx.algorithms import components
|
||||
from networkx.algorithms import connectivity
|
||||
from networkx.algorithms import community
|
||||
from networkx.algorithms import coloring
|
||||
from networkx.algorithms import flow
|
||||
from networkx.algorithms import isomorphism
|
||||
from networkx.algorithms import link_analysis
|
||||
from networkx.algorithms import lowest_common_ancestors
|
||||
from networkx.algorithms import operators
|
||||
from networkx.algorithms import shortest_paths
|
||||
from networkx.algorithms import tournament
|
||||
from networkx.algorithms import traversal
|
||||
from networkx.algorithms import tree
|
||||
|
||||
# Make certain functions from some of the previous subpackages available
|
||||
# to the user as direct imports from the `networkx` namespace.
|
||||
from networkx.algorithms.bipartite import complete_bipartite_graph
|
||||
from networkx.algorithms.bipartite import is_bipartite
|
||||
from networkx.algorithms.bipartite import projected_graph
|
||||
from networkx.algorithms.connectivity import all_pairs_node_connectivity
|
||||
from networkx.algorithms.connectivity import all_node_cuts
|
||||
from networkx.algorithms.connectivity import average_node_connectivity
|
||||
from networkx.algorithms.connectivity import edge_connectivity
|
||||
from networkx.algorithms.connectivity import edge_disjoint_paths
|
||||
from networkx.algorithms.connectivity import k_components
|
||||
from networkx.algorithms.connectivity import k_edge_components
|
||||
from networkx.algorithms.connectivity import k_edge_subgraphs
|
||||
from networkx.algorithms.connectivity import k_edge_augmentation
|
||||
from networkx.algorithms.connectivity import is_k_edge_connected
|
||||
from networkx.algorithms.connectivity import minimum_edge_cut
|
||||
from networkx.algorithms.connectivity import minimum_node_cut
|
||||
from networkx.algorithms.connectivity import node_connectivity
|
||||
from networkx.algorithms.connectivity import node_disjoint_paths
|
||||
from networkx.algorithms.connectivity import stoer_wagner
|
||||
from networkx.algorithms.flow import capacity_scaling
|
||||
from networkx.algorithms.flow import cost_of_flow
|
||||
from networkx.algorithms.flow import gomory_hu_tree
|
||||
from networkx.algorithms.flow import max_flow_min_cost
|
||||
from networkx.algorithms.flow import maximum_flow
|
||||
from networkx.algorithms.flow import maximum_flow_value
|
||||
from networkx.algorithms.flow import min_cost_flow
|
||||
from networkx.algorithms.flow import min_cost_flow_cost
|
||||
from networkx.algorithms.flow import minimum_cut
|
||||
from networkx.algorithms.flow import minimum_cut_value
|
||||
from networkx.algorithms.flow import network_simplex
|
||||
from networkx.algorithms.isomorphism import could_be_isomorphic
|
||||
from networkx.algorithms.isomorphism import fast_could_be_isomorphic
|
||||
from networkx.algorithms.isomorphism import faster_could_be_isomorphic
|
||||
from networkx.algorithms.isomorphism import is_isomorphic
|
||||
from networkx.algorithms.isomorphism.vf2pp import *
|
||||
from networkx.algorithms.tree.branchings import maximum_branching
|
||||
from networkx.algorithms.tree.branchings import maximum_spanning_arborescence
|
||||
from networkx.algorithms.tree.branchings import minimum_branching
|
||||
from networkx.algorithms.tree.branchings import minimum_spanning_arborescence
|
||||
from networkx.algorithms.tree.branchings import ArborescenceIterator
|
||||
from networkx.algorithms.tree.coding import *
|
||||
from networkx.algorithms.tree.decomposition import *
|
||||
from networkx.algorithms.tree.mst import *
|
||||
from networkx.algorithms.tree.operations import *
|
||||
from networkx.algorithms.tree.recognition import *
|
||||
from networkx.algorithms.tournament import is_tournament
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,26 @@
|
|||
"""Approximations of graph properties and Heuristic methods for optimization.
|
||||
|
||||
The functions in this class are not imported into the top-level ``networkx``
|
||||
namespace so the easiest way to use them is with::
|
||||
|
||||
>>> from networkx.algorithms import approximation
|
||||
|
||||
Another option is to import the specific function with
|
||||
``from networkx.algorithms.approximation import function_name``.
|
||||
|
||||
"""
|
||||
|
||||
from networkx.algorithms.approximation.clustering_coefficient import *
|
||||
from networkx.algorithms.approximation.clique import *
|
||||
from networkx.algorithms.approximation.connectivity import *
|
||||
from networkx.algorithms.approximation.distance_measures import *
|
||||
from networkx.algorithms.approximation.dominating_set import *
|
||||
from networkx.algorithms.approximation.kcomponents import *
|
||||
from networkx.algorithms.approximation.matching import *
|
||||
from networkx.algorithms.approximation.ramsey import *
|
||||
from networkx.algorithms.approximation.steinertree import *
|
||||
from networkx.algorithms.approximation.traveling_salesman import *
|
||||
from networkx.algorithms.approximation.treewidth import *
|
||||
from networkx.algorithms.approximation.vertex_cover import *
|
||||
from networkx.algorithms.approximation.maxcut import *
|
||||
from networkx.algorithms.approximation.density import *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,259 @@
|
|||
"""Functions for computing large cliques and maximum independent sets."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import ramsey
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = [
|
||||
"clique_removal",
|
||||
"max_clique",
|
||||
"large_clique_size",
|
||||
"maximum_independent_set",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def maximum_independent_set(G):
|
||||
"""Returns an approximate maximum independent set.
|
||||
|
||||
Independent set or stable set is a set of vertices in a graph, no two of
|
||||
which are adjacent. That is, it is a set I of vertices such that for every
|
||||
two vertices in I, there is no edge connecting the two. Equivalently, each
|
||||
edge in the graph has at most one endpoint in I. The size of an independent
|
||||
set is the number of vertices it contains [1]_.
|
||||
|
||||
A maximum independent set is a largest independent set for a given graph G
|
||||
and its size is denoted $\\alpha(G)$. The problem of finding such a set is called
|
||||
the maximum independent set problem and is an NP-hard optimization problem.
|
||||
As such, it is unlikely that there exists an efficient algorithm for finding
|
||||
a maximum independent set of a graph.
|
||||
|
||||
The Independent Set algorithm is based on [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
iset : Set
|
||||
The apx-maximum independent set
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(10)
|
||||
>>> nx.approximation.maximum_independent_set(G)
|
||||
{0, 2, 4, 6, 9}
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Finds the $O(|V|/(log|V|)^2)$ apx of independent set in the worst case.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] `Wikipedia: Independent set
|
||||
<https://en.wikipedia.org/wiki/Independent_set_(graph_theory)>`_
|
||||
.. [2] Boppana, R., & Halldórsson, M. M. (1992).
|
||||
Approximating maximum independent sets by excluding subgraphs.
|
||||
BIT Numerical Mathematics, 32(2), 180–196. Springer.
|
||||
"""
|
||||
iset, _ = clique_removal(G)
|
||||
return iset
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def max_clique(G):
|
||||
r"""Find the Maximum Clique
|
||||
|
||||
Finds the $O(|V|/(log|V|)^2)$ apx of maximum clique/independent set
|
||||
in the worst case.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
clique : set
|
||||
The apx-maximum clique of the graph
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(10)
|
||||
>>> nx.approximation.max_clique(G)
|
||||
{8, 9}
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
A clique in an undirected graph G = (V, E) is a subset of the vertex set
|
||||
`C \subseteq V` such that for every two vertices in C there exists an edge
|
||||
connecting the two. This is equivalent to saying that the subgraph
|
||||
induced by C is complete (in some cases, the term clique may also refer
|
||||
to the subgraph).
|
||||
|
||||
A maximum clique is a clique of the largest possible size in a given graph.
|
||||
The clique number `\omega(G)` of a graph G is the number of
|
||||
vertices in a maximum clique in G. The intersection number of
|
||||
G is the smallest number of cliques that together cover all edges of G.
|
||||
|
||||
https://en.wikipedia.org/wiki/Maximum_clique
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Boppana, R., & Halldórsson, M. M. (1992).
|
||||
Approximating maximum independent sets by excluding subgraphs.
|
||||
BIT Numerical Mathematics, 32(2), 180–196. Springer.
|
||||
doi:10.1007/BF01994876
|
||||
"""
|
||||
# finding the maximum clique in a graph is equivalent to finding
|
||||
# the independent set in the complementary graph
|
||||
cgraph = nx.complement(G)
|
||||
iset, _ = clique_removal(cgraph)
|
||||
return iset
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def clique_removal(G):
|
||||
r"""Repeatedly remove cliques from the graph.
|
||||
|
||||
Results in a $O(|V|/(\log |V|)^2)$ approximation of maximum clique
|
||||
and independent set. Returns the largest independent set found, along
|
||||
with found maximal cliques.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
max_ind_cliques : (set, list) tuple
|
||||
2-tuple of Maximal Independent Set and list of maximal cliques (sets).
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(10)
|
||||
>>> nx.approximation.clique_removal(G)
|
||||
({0, 2, 4, 6, 9}, [{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}])
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Boppana, R., & Halldórsson, M. M. (1992).
|
||||
Approximating maximum independent sets by excluding subgraphs.
|
||||
BIT Numerical Mathematics, 32(2), 180–196. Springer.
|
||||
"""
|
||||
graph = G.copy()
|
||||
c_i, i_i = ramsey.ramsey_R2(graph)
|
||||
cliques = [c_i]
|
||||
isets = [i_i]
|
||||
while graph:
|
||||
graph.remove_nodes_from(c_i)
|
||||
c_i, i_i = ramsey.ramsey_R2(graph)
|
||||
if c_i:
|
||||
cliques.append(c_i)
|
||||
if i_i:
|
||||
isets.append(i_i)
|
||||
# Determine the largest independent set as measured by cardinality.
|
||||
maxiset = max(isets, key=len)
|
||||
return maxiset, cliques
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def large_clique_size(G):
|
||||
"""Find the size of a large clique in a graph.
|
||||
|
||||
A *clique* is a subset of nodes in which each pair of nodes is
|
||||
adjacent. This function is a heuristic for finding the size of a
|
||||
large clique in the graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
k: integer
|
||||
The size of a large clique in the graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(10)
|
||||
>>> nx.approximation.large_clique_size(G)
|
||||
2
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This implementation is from [1]_. Its worst case time complexity is
|
||||
:math:`O(n d^2)`, where *n* is the number of nodes in the graph and
|
||||
*d* is the maximum degree.
|
||||
|
||||
This function is a heuristic, which means it may work well in
|
||||
practice, but there is no rigorous mathematical guarantee on the
|
||||
ratio between the returned number and the actual largest clique size
|
||||
in the graph.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Pattabiraman, Bharath, et al.
|
||||
"Fast Algorithms for the Maximum Clique Problem on Massive Graphs
|
||||
with Applications to Overlapping Community Detection."
|
||||
*Internet Mathematics* 11.4-5 (2015): 421--448.
|
||||
<https://doi.org/10.1080/15427951.2014.986778>
|
||||
|
||||
See also
|
||||
--------
|
||||
|
||||
:func:`networkx.algorithms.approximation.clique.max_clique`
|
||||
A function that returns an approximate maximum clique with a
|
||||
guarantee on the approximation ratio.
|
||||
|
||||
:mod:`networkx.algorithms.clique`
|
||||
Functions for finding the exact maximum clique in a graph.
|
||||
|
||||
"""
|
||||
degrees = G.degree
|
||||
|
||||
def _clique_heuristic(G, U, size, best_size):
|
||||
if not U:
|
||||
return max(best_size, size)
|
||||
u = max(U, key=degrees)
|
||||
U.remove(u)
|
||||
N_prime = {v for v in G[u] if degrees[v] >= best_size}
|
||||
return _clique_heuristic(G, U & N_prime, size + 1, best_size)
|
||||
|
||||
best_size = 0
|
||||
nodes = (u for u in G if degrees[u] >= best_size)
|
||||
for u in nodes:
|
||||
neighbors = {v for v in G[u] if degrees[v] >= best_size}
|
||||
best_size = _clique_heuristic(G, neighbors, 1, best_size)
|
||||
return best_size
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for, py_random_state
|
||||
|
||||
__all__ = ["average_clustering"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@py_random_state(2)
|
||||
@nx._dispatchable(name="approximate_average_clustering")
|
||||
def average_clustering(G, trials=1000, seed=None):
|
||||
r"""Estimates the average clustering coefficient of G.
|
||||
|
||||
The local clustering of each node in `G` is the fraction of triangles
|
||||
that actually exist over all possible triangles in its neighborhood.
|
||||
The average clustering coefficient of a graph `G` is the mean of
|
||||
local clusterings.
|
||||
|
||||
This function finds an approximate average clustering coefficient
|
||||
for G by repeating `n` times (defined in `trials`) the following
|
||||
experiment: choose a node at random, choose two of its neighbors
|
||||
at random, and check if they are connected. The approximate
|
||||
coefficient is the fraction of triangles found over the number
|
||||
of trials [1]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
trials : integer
|
||||
Number of trials to perform (default 1000).
|
||||
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
c : float
|
||||
Approximated average clustering coefficient.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import approximation
|
||||
>>> G = nx.erdos_renyi_graph(10, 0.2, seed=10)
|
||||
>>> approximation.average_clustering(G, trials=1000, seed=10)
|
||||
0.214
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If G is directed.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Schank, Thomas, and Dorothea Wagner. Approximating clustering
|
||||
coefficient and transitivity. Universität Karlsruhe, Fakultät für
|
||||
Informatik, 2004.
|
||||
https://doi.org/10.5445/IR/1000001239
|
||||
|
||||
"""
|
||||
n = len(G)
|
||||
triangles = 0
|
||||
nodes = list(G)
|
||||
for i in [int(seed.random() * n) for i in range(trials)]:
|
||||
nbrs = list(G[nodes[i]])
|
||||
if len(nbrs) < 2:
|
||||
continue
|
||||
u, v = seed.sample(nbrs, 2)
|
||||
if u in G[v]:
|
||||
triangles += 1
|
||||
return triangles / trials
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
"""Fast approximation for node connectivity"""
|
||||
|
||||
import itertools
|
||||
from operator import itemgetter
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = [
|
||||
"local_node_connectivity",
|
||||
"node_connectivity",
|
||||
"all_pairs_node_connectivity",
|
||||
]
|
||||
|
||||
|
||||
@nx._dispatchable(name="approximate_local_node_connectivity")
|
||||
def local_node_connectivity(G, source, target, cutoff=None):
|
||||
"""Compute node connectivity between source and target.
|
||||
|
||||
Pairwise or local node connectivity between two distinct and nonadjacent
|
||||
nodes is the minimum number of nodes that must be removed (minimum
|
||||
separating cutset) to disconnect them. By Menger's theorem, this is equal
|
||||
to the number of node independent paths (paths that share no nodes other
|
||||
than source and target). Which is what we compute in this function.
|
||||
|
||||
This algorithm is a fast approximation that gives an strict lower
|
||||
bound on the actual number of node independent paths between two nodes [1]_.
|
||||
It works for both directed and undirected graphs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
G : NetworkX graph
|
||||
|
||||
source : node
|
||||
Starting node for node connectivity
|
||||
|
||||
target : node
|
||||
Ending node for node connectivity
|
||||
|
||||
cutoff : integer
|
||||
Maximum node connectivity to consider. If None, the minimum degree
|
||||
of source or target is used as a cutoff. Default value None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
k: integer
|
||||
pairwise node connectivity
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # Platonic octahedral graph has node connectivity 4
|
||||
>>> # for each non adjacent node pair
|
||||
>>> from networkx.algorithms import approximation as approx
|
||||
>>> G = nx.octahedral_graph()
|
||||
>>> approx.local_node_connectivity(G, 0, 5)
|
||||
4
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm [1]_ finds node independents paths between two nodes by
|
||||
computing their shortest path using BFS, marking the nodes of the path
|
||||
found as 'used' and then searching other shortest paths excluding the
|
||||
nodes marked as used until no more paths exist. It is not exact because
|
||||
a shortest path could use nodes that, if the path were longer, may belong
|
||||
to two different node independent paths. Thus it only guarantees an
|
||||
strict lower bound on node connectivity.
|
||||
|
||||
Note that the authors propose a further refinement, losing accuracy and
|
||||
gaining speed, which is not implemented yet.
|
||||
|
||||
See also
|
||||
--------
|
||||
all_pairs_node_connectivity
|
||||
node_connectivity
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
|
||||
Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
|
||||
http://eclectic.ss.uci.edu/~drwhite/working.pdf
|
||||
|
||||
"""
|
||||
if target == source:
|
||||
raise nx.NetworkXError("source and target have to be different nodes.")
|
||||
|
||||
# Maximum possible node independent paths
|
||||
if G.is_directed():
|
||||
possible = min(G.out_degree(source), G.in_degree(target))
|
||||
else:
|
||||
possible = min(G.degree(source), G.degree(target))
|
||||
|
||||
K = 0
|
||||
if not possible:
|
||||
return K
|
||||
|
||||
if cutoff is None:
|
||||
cutoff = float("inf")
|
||||
|
||||
exclude = set()
|
||||
for i in range(min(possible, cutoff)):
|
||||
try:
|
||||
path = _bidirectional_shortest_path(G, source, target, exclude)
|
||||
exclude.update(set(path))
|
||||
K += 1
|
||||
except nx.NetworkXNoPath:
|
||||
break
|
||||
|
||||
return K
|
||||
|
||||
|
||||
@nx._dispatchable(name="approximate_node_connectivity")
|
||||
def node_connectivity(G, s=None, t=None):
|
||||
r"""Returns an approximation for node connectivity for a graph or digraph G.
|
||||
|
||||
Node connectivity is equal to the minimum number of nodes that
|
||||
must be removed to disconnect G or render it trivial. By Menger's theorem,
|
||||
this is equal to the number of node independent paths (paths that
|
||||
share no nodes other than source and target).
|
||||
|
||||
If source and target nodes are provided, this function returns the
|
||||
local node connectivity: the minimum number of nodes that must be
|
||||
removed to break all paths from source to target in G.
|
||||
|
||||
This algorithm is based on a fast approximation that gives an strict lower
|
||||
bound on the actual number of node independent paths between two nodes [1]_.
|
||||
It works for both directed and undirected graphs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
s : node
|
||||
Source node. Optional. Default value: None.
|
||||
|
||||
t : node
|
||||
Target node. Optional. Default value: None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
K : integer
|
||||
Node connectivity of G, or local node connectivity if source
|
||||
and target are provided.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # Platonic octahedral graph is 4-node-connected
|
||||
>>> from networkx.algorithms import approximation as approx
|
||||
>>> G = nx.octahedral_graph()
|
||||
>>> approx.node_connectivity(G)
|
||||
4
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm [1]_ finds node independents paths between two nodes by
|
||||
computing their shortest path using BFS, marking the nodes of the path
|
||||
found as 'used' and then searching other shortest paths excluding the
|
||||
nodes marked as used until no more paths exist. It is not exact because
|
||||
a shortest path could use nodes that, if the path were longer, may belong
|
||||
to two different node independent paths. Thus it only guarantees an
|
||||
strict lower bound on node connectivity.
|
||||
|
||||
See also
|
||||
--------
|
||||
all_pairs_node_connectivity
|
||||
local_node_connectivity
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
|
||||
Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
|
||||
http://eclectic.ss.uci.edu/~drwhite/working.pdf
|
||||
|
||||
"""
|
||||
if (s is not None and t is None) or (s is None and t is not None):
|
||||
raise nx.NetworkXError("Both source and target must be specified.")
|
||||
|
||||
# Local node connectivity
|
||||
if s is not None and t is not None:
|
||||
if s not in G:
|
||||
raise nx.NetworkXError(f"node {s} not in graph")
|
||||
if t not in G:
|
||||
raise nx.NetworkXError(f"node {t} not in graph")
|
||||
return local_node_connectivity(G, s, t)
|
||||
|
||||
# Global node connectivity
|
||||
if G.is_directed():
|
||||
connected_func = nx.is_weakly_connected
|
||||
iter_func = itertools.permutations
|
||||
|
||||
def neighbors(v):
|
||||
return itertools.chain(G.predecessors(v), G.successors(v))
|
||||
|
||||
else:
|
||||
connected_func = nx.is_connected
|
||||
iter_func = itertools.combinations
|
||||
neighbors = G.neighbors
|
||||
|
||||
if not connected_func(G):
|
||||
return 0
|
||||
|
||||
# Choose a node with minimum degree
|
||||
v, minimum_degree = min(G.degree(), key=itemgetter(1))
|
||||
# Node connectivity is bounded by minimum degree
|
||||
K = minimum_degree
|
||||
# compute local node connectivity with all non-neighbors nodes
|
||||
# and store the minimum
|
||||
for w in set(G) - set(neighbors(v)) - {v}:
|
||||
K = min(K, local_node_connectivity(G, v, w, cutoff=K))
|
||||
# Same for non adjacent pairs of neighbors of v
|
||||
for x, y in iter_func(neighbors(v), 2):
|
||||
if y not in G[x] and x != y:
|
||||
K = min(K, local_node_connectivity(G, x, y, cutoff=K))
|
||||
return K
|
||||
|
||||
|
||||
@nx._dispatchable(name="approximate_all_pairs_node_connectivity")
|
||||
def all_pairs_node_connectivity(G, nbunch=None, cutoff=None):
|
||||
"""Compute node connectivity between all pairs of nodes.
|
||||
|
||||
Pairwise or local node connectivity between two distinct and nonadjacent
|
||||
nodes is the minimum number of nodes that must be removed (minimum
|
||||
separating cutset) to disconnect them. By Menger's theorem, this is equal
|
||||
to the number of node independent paths (paths that share no nodes other
|
||||
than source and target). Which is what we compute in this function.
|
||||
|
||||
This algorithm is a fast approximation that gives an strict lower
|
||||
bound on the actual number of node independent paths between two nodes [1]_.
|
||||
It works for both directed and undirected graphs.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
nbunch: container
|
||||
Container of nodes. If provided node connectivity will be computed
|
||||
only over pairs of nodes in nbunch.
|
||||
|
||||
cutoff : integer
|
||||
Maximum node connectivity to consider. If None, the minimum degree
|
||||
of source or target is used as a cutoff in each pair of nodes.
|
||||
Default value None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
K : dictionary
|
||||
Dictionary, keyed by source and target, of pairwise node connectivity
|
||||
|
||||
Examples
|
||||
--------
|
||||
A 3 node cycle with one extra node attached has connectivity 2 between all
|
||||
nodes in the cycle and connectivity 1 between the extra node and the rest:
|
||||
|
||||
>>> G = nx.cycle_graph(3)
|
||||
>>> G.add_edge(2, 3)
|
||||
>>> import pprint # for nice dictionary formatting
|
||||
>>> pprint.pprint(nx.all_pairs_node_connectivity(G))
|
||||
{0: {1: 2, 2: 2, 3: 1},
|
||||
1: {0: 2, 2: 2, 3: 1},
|
||||
2: {0: 2, 1: 2, 3: 1},
|
||||
3: {0: 1, 1: 1, 2: 1}}
|
||||
|
||||
See Also
|
||||
--------
|
||||
local_node_connectivity
|
||||
node_connectivity
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
|
||||
Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
|
||||
http://eclectic.ss.uci.edu/~drwhite/working.pdf
|
||||
"""
|
||||
if nbunch is None:
|
||||
nbunch = G
|
||||
else:
|
||||
nbunch = set(nbunch)
|
||||
|
||||
directed = G.is_directed()
|
||||
if directed:
|
||||
iter_func = itertools.permutations
|
||||
else:
|
||||
iter_func = itertools.combinations
|
||||
|
||||
all_pairs = {n: {} for n in nbunch}
|
||||
|
||||
for u, v in iter_func(nbunch, 2):
|
||||
k = local_node_connectivity(G, u, v, cutoff=cutoff)
|
||||
all_pairs[u][v] = k
|
||||
if not directed:
|
||||
all_pairs[v][u] = k
|
||||
|
||||
return all_pairs
|
||||
|
||||
|
||||
def _bidirectional_shortest_path(G, source, target, exclude):
|
||||
"""Returns shortest path between source and target ignoring nodes in the
|
||||
container 'exclude'.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
G : NetworkX graph
|
||||
|
||||
source : node
|
||||
Starting node for path
|
||||
|
||||
target : node
|
||||
Ending node for path
|
||||
|
||||
exclude: container
|
||||
Container for nodes to exclude from the search for shortest paths
|
||||
|
||||
Returns
|
||||
-------
|
||||
path: list
|
||||
Shortest path between source and target ignoring nodes in 'exclude'
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNoPath
|
||||
If there is no path or if nodes are adjacent and have only one path
|
||||
between them
|
||||
|
||||
Notes
|
||||
-----
|
||||
This function and its helper are originally from
|
||||
networkx.algorithms.shortest_paths.unweighted and are modified to
|
||||
accept the extra parameter 'exclude', which is a container for nodes
|
||||
already used in other paths that should be ignored.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
|
||||
Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
|
||||
http://eclectic.ss.uci.edu/~drwhite/working.pdf
|
||||
|
||||
"""
|
||||
# call helper to do the real work
|
||||
results = _bidirectional_pred_succ(G, source, target, exclude)
|
||||
pred, succ, w = results
|
||||
|
||||
# build path from pred+w+succ
|
||||
path = []
|
||||
# from source to w
|
||||
while w is not None:
|
||||
path.append(w)
|
||||
w = pred[w]
|
||||
path.reverse()
|
||||
# from w to target
|
||||
w = succ[path[-1]]
|
||||
while w is not None:
|
||||
path.append(w)
|
||||
w = succ[w]
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def _bidirectional_pred_succ(G, source, target, exclude):
|
||||
# does BFS from both source and target and meets in the middle
|
||||
# excludes nodes in the container "exclude" from the search
|
||||
|
||||
# handle either directed or undirected
|
||||
if G.is_directed():
|
||||
Gpred = G.predecessors
|
||||
Gsucc = G.successors
|
||||
else:
|
||||
Gpred = G.neighbors
|
||||
Gsucc = G.neighbors
|
||||
|
||||
# predecessor and successors in search
|
||||
pred = {source: None}
|
||||
succ = {target: None}
|
||||
|
||||
# initialize fringes, start with forward
|
||||
forward_fringe = [source]
|
||||
reverse_fringe = [target]
|
||||
|
||||
level = 0
|
||||
|
||||
while forward_fringe and reverse_fringe:
|
||||
# Make sure that we iterate one step forward and one step backwards
|
||||
# thus source and target will only trigger "found path" when they are
|
||||
# adjacent and then they can be safely included in the container 'exclude'
|
||||
level += 1
|
||||
if level % 2 != 0:
|
||||
this_level = forward_fringe
|
||||
forward_fringe = []
|
||||
for v in this_level:
|
||||
for w in Gsucc(v):
|
||||
if w in exclude:
|
||||
continue
|
||||
if w not in pred:
|
||||
forward_fringe.append(w)
|
||||
pred[w] = v
|
||||
if w in succ:
|
||||
return pred, succ, w # found path
|
||||
else:
|
||||
this_level = reverse_fringe
|
||||
reverse_fringe = []
|
||||
for v in this_level:
|
||||
for w in Gpred(v):
|
||||
if w in exclude:
|
||||
continue
|
||||
if w not in succ:
|
||||
succ[w] = v
|
||||
reverse_fringe.append(w)
|
||||
if w in pred:
|
||||
return pred, succ, w # found path
|
||||
|
||||
raise nx.NetworkXNoPath(f"No path between {source} and {target}.")
|
||||
|
|
@ -0,0 +1,396 @@
|
|||
"""Fast algorithms for the densest subgraph problem"""
|
||||
|
||||
import math
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["densest_subgraph"]
|
||||
|
||||
|
||||
def _greedy_plus_plus(G, iterations):
|
||||
if G.number_of_edges() == 0:
|
||||
return 0.0, set()
|
||||
if iterations < 1:
|
||||
raise ValueError(
|
||||
f"The number of iterations must be an integer >= 1. Provided: {iterations}"
|
||||
)
|
||||
|
||||
loads = dict.fromkeys(G.nodes, 0) # Load vector for Greedy++.
|
||||
best_density = 0.0 # Highest density encountered.
|
||||
best_subgraph = set() # Nodes of the best subgraph found.
|
||||
|
||||
for _ in range(iterations):
|
||||
# Initialize heap for fast access to minimum weighted degree.
|
||||
heap = nx.utils.BinaryHeap()
|
||||
|
||||
# Compute initial weighted degrees and add nodes to the heap.
|
||||
for node, degree in G.degree:
|
||||
heap.insert(node, loads[node] + degree)
|
||||
# Set up tracking for current graph state.
|
||||
remaining_nodes = set(G.nodes)
|
||||
num_edges = G.number_of_edges()
|
||||
current_degrees = dict(G.degree)
|
||||
|
||||
while remaining_nodes:
|
||||
num_nodes = len(remaining_nodes)
|
||||
|
||||
# Current density of the (implicit) graph
|
||||
current_density = num_edges / num_nodes
|
||||
|
||||
# Update the best density.
|
||||
if current_density > best_density:
|
||||
best_density = current_density
|
||||
best_subgraph = set(remaining_nodes)
|
||||
|
||||
# Pop the node with the smallest weighted degree.
|
||||
node, _ = heap.pop()
|
||||
if node not in remaining_nodes:
|
||||
continue # Skip nodes already removed.
|
||||
|
||||
# Update the load of the popped node.
|
||||
loads[node] += current_degrees[node]
|
||||
|
||||
# Update neighbors' degrees and the heap.
|
||||
for neighbor in G.neighbors(node):
|
||||
if neighbor in remaining_nodes:
|
||||
current_degrees[neighbor] -= 1
|
||||
num_edges -= 1
|
||||
heap.insert(neighbor, loads[neighbor] + current_degrees[neighbor])
|
||||
|
||||
# Remove the node from the remaining nodes.
|
||||
remaining_nodes.remove(node)
|
||||
|
||||
return best_density, best_subgraph
|
||||
|
||||
|
||||
def _fractional_peeling(G, b, x, node_to_idx, edge_to_idx):
|
||||
"""
|
||||
Optimized fractional peeling using NumPy arrays.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : networkx.Graph
|
||||
The input graph.
|
||||
b : numpy.ndarray
|
||||
Induced load vector.
|
||||
x : numpy.ndarray
|
||||
Fractional edge values.
|
||||
node_to_idx : dict
|
||||
Mapping from node to index.
|
||||
edge_to_idx : dict
|
||||
Mapping from edge to index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
best_density : float
|
||||
The best density found.
|
||||
best_subgraph : set
|
||||
The subset of nodes defining the densest subgraph.
|
||||
"""
|
||||
heap = nx.utils.BinaryHeap()
|
||||
|
||||
remaining_nodes = set(G.nodes)
|
||||
|
||||
# Initialize heap with b values
|
||||
for idx in remaining_nodes:
|
||||
heap.insert(idx, b[idx])
|
||||
|
||||
num_edges = G.number_of_edges()
|
||||
|
||||
best_density = 0.0
|
||||
best_subgraph = set()
|
||||
|
||||
while remaining_nodes:
|
||||
num_nodes = len(remaining_nodes)
|
||||
current_density = num_edges / num_nodes
|
||||
|
||||
if current_density > best_density:
|
||||
best_density = current_density
|
||||
best_subgraph = set(remaining_nodes)
|
||||
|
||||
# Pop the node with the smallest b
|
||||
node, _ = heap.pop()
|
||||
while node not in remaining_nodes:
|
||||
node, _ = heap.pop() # Clean the heap from stale values
|
||||
|
||||
# Update neighbors b values by subtracting fractional x value
|
||||
for neighbor in G.neighbors(node):
|
||||
if neighbor in remaining_nodes:
|
||||
neighbor_idx = node_to_idx[neighbor]
|
||||
# Take off fractional value
|
||||
b[neighbor_idx] -= x[edge_to_idx[(neighbor, node)]]
|
||||
num_edges -= 1
|
||||
heap.insert(neighbor, b[neighbor_idx])
|
||||
|
||||
remaining_nodes.remove(node) # peel off node
|
||||
|
||||
return best_density, best_subgraph
|
||||
|
||||
|
||||
def _fista(G, iterations):
|
||||
if G.number_of_edges() == 0:
|
||||
return 0.0, set()
|
||||
if iterations < 1:
|
||||
raise ValueError(
|
||||
f"The number of iterations must be an integer >= 1. Provided: {iterations}"
|
||||
)
|
||||
import numpy as np
|
||||
|
||||
# 1. Node Mapping: Assign a unique index to each node and edge
|
||||
node_to_idx = {node: idx for idx, node in enumerate(G)}
|
||||
num_nodes = G.number_of_nodes()
|
||||
num_undirected_edges = G.number_of_edges()
|
||||
|
||||
# 2. Edge Mapping: Assign a unique index to each bidirectional edge
|
||||
bidirectional_edges = [(u, v) for u, v in G.edges] + [(v, u) for u, v in G.edges]
|
||||
edge_to_idx = {edge: idx for idx, edge in enumerate(bidirectional_edges)}
|
||||
|
||||
num_edges = len(bidirectional_edges)
|
||||
|
||||
# 3. Reverse Edge Mapping: Map each (bidirectional) edge to its reverse edge index
|
||||
reverse_edge_idx = np.empty(num_edges, dtype=np.int32)
|
||||
for idx in range(num_undirected_edges):
|
||||
reverse_edge_idx[idx] = num_undirected_edges + idx
|
||||
for idx in range(num_undirected_edges, 2 * num_undirected_edges):
|
||||
reverse_edge_idx[idx] = idx - num_undirected_edges
|
||||
|
||||
# 4. Initialize Variables as NumPy Arrays
|
||||
x = np.full(num_edges, 0.5, dtype=np.float32)
|
||||
y = x.copy()
|
||||
z = np.zeros(num_edges, dtype=np.float32)
|
||||
b = np.zeros(num_nodes, dtype=np.float32) # Induced load vector
|
||||
tk = 1.0 # Momentum term
|
||||
|
||||
# 5. Precompute Edge Source Indices
|
||||
edge_src_indices = np.array(
|
||||
[node_to_idx[u] for u, _ in bidirectional_edges], dtype=np.int32
|
||||
)
|
||||
|
||||
# 6. Compute Learning Rate
|
||||
max_degree = max(deg for _, deg in G.degree)
|
||||
# 0.9 for floating point errs when max_degree is very large
|
||||
learning_rate = 0.9 / max_degree
|
||||
|
||||
# 7. Iterative Updates
|
||||
for _ in range(iterations):
|
||||
# 7a. Update b: sum y over outgoing edges for each node
|
||||
b[:] = 0.0 # Reset b to zero
|
||||
np.add.at(b, edge_src_indices, y) # b_u = \sum_{v : (u,v) \in E(G)} y_{uv}
|
||||
|
||||
# 7b. Compute z, z_{uv} = y_{uv} - 2 * learning_rate * b_u
|
||||
z = y - 2.0 * learning_rate * b[edge_src_indices]
|
||||
|
||||
# 7c. Update Momentum Term
|
||||
tknew = (1.0 + math.sqrt(1 + 4.0 * tk**2)) / 2.0
|
||||
|
||||
# 7d. Update x in a vectorized manner, x_{uv} = (z_{uv} - z_{vu} + 1.0) / 2.0
|
||||
new_xuv = (z - z[reverse_edge_idx] + 1.0) / 2.0
|
||||
clamped_x = np.clip(new_xuv, 0.0, 1.0) # Clamp x_{uv} between 0 and 1
|
||||
|
||||
# Update y using the FISTA update formula (similar to gradient descent)
|
||||
y = (
|
||||
clamped_x
|
||||
+ ((tk - 1.0) / tknew) * (clamped_x - x)
|
||||
+ (tk / tknew) * (clamped_x - y)
|
||||
)
|
||||
|
||||
# Update x
|
||||
x = clamped_x
|
||||
|
||||
# Update tk, the momemntum term
|
||||
tk = tknew
|
||||
|
||||
# Rebalance the b values! Otherwise performance is a bit suboptimal.
|
||||
b[:] = 0.0
|
||||
np.add.at(b, edge_src_indices, x) # b_u = \sum_{v : (u,v) \in E(G)} x_{uv}
|
||||
|
||||
# Extract the actual (approximate) dense subgraph.
|
||||
return _fractional_peeling(G, b, x, node_to_idx, edge_to_idx)
|
||||
|
||||
|
||||
ALGORITHMS = {"greedy++": _greedy_plus_plus, "fista": _fista}
|
||||
|
||||
|
||||
@nx.utils.not_implemented_for("directed")
|
||||
@nx.utils.not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def densest_subgraph(G, iterations=1, *, method="fista"):
|
||||
r"""Returns an approximate densest subgraph for a graph `G`.
|
||||
|
||||
This function runs an iterative algorithm to find the densest subgraph,
|
||||
and returns both the density and the subgraph. For a discussion on the
|
||||
notion of density used and the different algorithms available on
|
||||
networkx, please see the Notes section below.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph.
|
||||
|
||||
iterations : int, optional (default=1)
|
||||
Number of iterations to use for the iterative algorithm. Can be
|
||||
specified positionally or as a keyword argument.
|
||||
|
||||
method : string, optional (default='fista')
|
||||
The algorithm to use to approximate the densest subgraph. Supported
|
||||
options: 'greedy++' by Boob et al. [2]_ and 'fista' by Harb et al. [3]_.
|
||||
Must be specified as a keyword argument. Other inputs produce a
|
||||
ValueError.
|
||||
|
||||
Returns
|
||||
-------
|
||||
d : float
|
||||
The density of the approximate subgraph found.
|
||||
|
||||
S : set
|
||||
The subset of nodes defining the approximate densest subgraph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.star_graph(4)
|
||||
>>> nx.approximation.densest_subgraph(G, iterations=1)
|
||||
(0.8, {0, 1, 2, 3, 4})
|
||||
|
||||
Notes
|
||||
-----
|
||||
**Problem Definition:**
|
||||
The densest subgraph problem (DSG) asks to find the subgraph
|
||||
$S \subseteq V(G)$ with maximum density. For a subset of the nodes of
|
||||
$G$, $S \subseteq V(G)$, define $E(S) = \{ (u,v) : (u,v)\in E(G),
|
||||
u\in S, v\in S \}$ as the set of edges with both endpoints in $S$.
|
||||
The density of $S$ is defined as $|E(S)|/|S|$, the ratio between the
|
||||
edges in the subgraph $G[S]$ and the number of nodes in that subgraph.
|
||||
Note that this is different from the standard graph theoretic definition
|
||||
of density, defined as $\frac{2|E(S)|}{|S|(|S|-1)}$, for historical
|
||||
reasons.
|
||||
|
||||
**Exact Algorithms:**
|
||||
The densest subgraph problem is polynomial time solvable using maximum
|
||||
flow, commonly referred to as Goldberg's algorithm. However, the
|
||||
algorithm is quite involved. It first binary searches on the optimal
|
||||
density, $d^\ast$. For a guess of the density $d$, it sets up a flow
|
||||
network $G'$ with size $O(m)$. The maximum flow solution either
|
||||
informs the algorithm that no subgraph with density $d$ exists, or it
|
||||
provides a subgraph with density at least $d$. However, this is
|
||||
inherently bottlenecked by the maximum flow algorithm. For example, [2]_
|
||||
notes that Goldberg’s algorithm was not feasible on many large graphs
|
||||
even though they used a highly optimized maximum flow library.
|
||||
|
||||
**Charikar's Greedy Peeling:**
|
||||
While exact solution algorithms are quite involved, there are several
|
||||
known approximation algorithms for the densest subgraph problem.
|
||||
|
||||
Charikar [1]_ described a very simple 1/2-approximation algorithm for DSG
|
||||
known as the greedy "peeling" algorithm. The algorithm creates an
|
||||
ordering of the nodes as follows. The first node $v_1$ is the one with
|
||||
the smallest degree in $G$ (ties broken arbitrarily). It selects
|
||||
$v_2$ to be the smallest degree node in $G \setminus v_1$. Letting
|
||||
$G_i$ be the graph after removing $v_1, ..., v_i$ (with $G_0=G$),
|
||||
the algorithm returns the graph among $G_0, ..., G_n$ with the highest
|
||||
density.
|
||||
|
||||
**Greedy++:**
|
||||
Boob et al. [2]_ generalized this algorithm into Greedy++, an iterative
|
||||
algorithm that runs several rounds of "peeling". In fact, Greedy++ with 1
|
||||
iteration is precisely Charikar's algorithm. The algorithm converges to a
|
||||
$(1-\epsilon)$ approximate densest subgraph in $O(\Delta(G)\log
|
||||
n/\epsilon^2)$ iterations, where $\Delta(G)$ is the maximum degree,
|
||||
and $n$ is the number of nodes in $G$. The algorithm also has other
|
||||
desirable properties as shown by [4]_ and [5]_.
|
||||
|
||||
**FISTA Algorithm:**
|
||||
Harb et al. [3]_ gave a faster and more scalable algorithm using ideas
|
||||
from quadratic programming for the densest subgraph, which is based on a
|
||||
fast iterative shrinkage-thresholding algorithm (FISTA) algorithm. It is
|
||||
known that computing the densest subgraph can be formulated as the
|
||||
following convex optimization problem:
|
||||
|
||||
Minimize $\sum_{u \in V(G)} b_u^2$
|
||||
|
||||
Subject to:
|
||||
|
||||
$b_u = \sum_{v: \{u,v\} \in E(G)} x_{uv}$ for all $u \in V(G)$
|
||||
|
||||
$x_{uv} + x_{vu} = 1.0$ for all $\{u,v\} \in E(G)$
|
||||
|
||||
$x_{uv} \geq 0, x_{vu} \geq 0$ for all $\{u,v\} \in E(G)$
|
||||
|
||||
Here, $x_{uv}$ represents the fraction of edge $\{u,v\}$ assigned to
|
||||
$u$, and $x_{vu}$ to $v$.
|
||||
|
||||
The FISTA algorithm efficiently solves this convex program using gradient
|
||||
descent with projections. For a learning rate $\alpha$, the algorithm
|
||||
does:
|
||||
|
||||
1. **Initialization**: Set $x^{(0)}_{uv} = x^{(0)}_{vu} = 0.5$ for all
|
||||
edges as a feasible solution.
|
||||
|
||||
2. **Gradient Update**: For iteration $k\geq 1$, set
|
||||
$x^{(k+1)}_{uv} = x^{(k)}_{uv} - 2 \alpha \sum_{v: \{u,v\} \in E(G)}
|
||||
x^{(k)}_{uv}$. However, now $x^{(k+1)}_{uv}$ might be infeasible!
|
||||
To ensure feasibility, we project $x^{(k+1)}_{uv}$.
|
||||
|
||||
3. **Projection to the Feasible Set**: Compute
|
||||
$b^{(k+1)}_u = \sum_{v: \{u,v\} \in E(G)} x^{(k)}_{uv}$ for all
|
||||
nodes $u$. Define $z^{(k+1)}_{uv} = x^{(k+1)}_{uv} - 2 \alpha
|
||||
b^{(k+1)}_u$. Update $x^{(k+1)}_{uv} =
|
||||
CLAMP((z^{(k+1)}_{uv} - z^{(k+1)}_{vu} + 1.0) / 2.0)$, where
|
||||
$CLAMP(x) = \max(0, \min(1, x))$.
|
||||
|
||||
With a learning rate of $\alpha=1/\Delta(G)$, where $\Delta(G)$ is
|
||||
the maximum degree, the algorithm converges to the optimum solution of
|
||||
the convex program.
|
||||
|
||||
**Fractional Peeling:**
|
||||
To obtain a **discrete** subgraph, we use fractional peeling, an
|
||||
adaptation of the standard peeling algorithm which peels the minimum
|
||||
degree vertex in each iteration, and returns the densest subgraph found
|
||||
along the way. Here, we instead peel the vertex with the smallest
|
||||
induced load $b_u$:
|
||||
|
||||
1. Compute $b_u$ and $x_{uv}$.
|
||||
|
||||
2. Iteratively remove the vertex with the smallest $b_u$, updating its
|
||||
neighbors' load by $x_{vu}$.
|
||||
|
||||
Fractional peeling transforms the approximately optimal fractional
|
||||
values $b_u, x_{uv}$ into a discrete subgraph. Unlike traditional
|
||||
peeling, which removes the lowest-degree node, this method accounts for
|
||||
fractional edge contributions from the convex program.
|
||||
|
||||
This approach is both scalable and theoretically sound, ensuring a quick
|
||||
approximation of the densest subgraph while leveraging fractional load
|
||||
balancing.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Charikar, Moses. "Greedy approximation algorithms for finding dense
|
||||
components in a graph." In International workshop on approximation
|
||||
algorithms for combinatorial optimization, pp. 84-95. Berlin, Heidelberg:
|
||||
Springer Berlin Heidelberg, 2000.
|
||||
|
||||
.. [2] Boob, Digvijay, Yu Gao, Richard Peng, Saurabh Sawlani, Charalampos
|
||||
Tsourakakis, Di Wang, and Junxing Wang. "Flowless: Extracting densest
|
||||
subgraphs without flow computations." In Proceedings of The Web Conference
|
||||
2020, pp. 573-583. 2020.
|
||||
|
||||
.. [3] Harb, Elfarouk, Kent Quanrud, and Chandra Chekuri. "Faster and scalable
|
||||
algorithms for densest subgraph and decomposition." Advances in Neural
|
||||
Information Processing Systems 35 (2022): 26966-26979.
|
||||
|
||||
.. [4] Harb, Elfarouk, Kent Quanrud, and Chandra Chekuri. "Convergence to
|
||||
lexicographically optimal base in a (contra) polymatroid and applications
|
||||
to densest subgraph and tree packing." arXiv preprint arXiv:2305.02987
|
||||
(2023).
|
||||
|
||||
.. [5] Chekuri, Chandra, Kent Quanrud, and Manuel R. Torres. "Densest
|
||||
subgraph: Supermodularity, iterative peeling, and flow." In Proceedings of
|
||||
the 2022 Annual ACM-SIAM Symposium on Discrete Algorithms (SODA), pp.
|
||||
1531-1555. Society for Industrial and Applied Mathematics, 2022.
|
||||
"""
|
||||
try:
|
||||
algo = ALGORITHMS[method]
|
||||
except KeyError as e:
|
||||
raise ValueError(f"{method} is not a valid choice for an algorithm.") from e
|
||||
|
||||
return algo(G, iterations)
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
"""Distance measures approximated metrics."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils.decorators import py_random_state
|
||||
|
||||
__all__ = ["diameter"]
|
||||
|
||||
|
||||
@py_random_state(1)
|
||||
@nx._dispatchable(name="approximate_diameter")
|
||||
def diameter(G, seed=None):
|
||||
"""Returns a lower bound on the diameter of the graph G.
|
||||
|
||||
The function computes a lower bound on the diameter (i.e., the maximum eccentricity)
|
||||
of a directed or undirected graph G. The procedure used varies depending on the graph
|
||||
being directed or not.
|
||||
|
||||
If G is an `undirected` graph, then the function uses the `2-sweep` algorithm [1]_.
|
||||
The main idea is to pick the farthest node from a random node and return its eccentricity.
|
||||
|
||||
Otherwise, if G is a `directed` graph, the function uses the `2-dSweep` algorithm [2]_,
|
||||
The procedure starts by selecting a random source node $s$ from which it performs a
|
||||
forward and a backward BFS. Let $a_1$ and $a_2$ be the farthest nodes in the forward and
|
||||
backward cases, respectively. Then, it computes the backward eccentricity of $a_1$ using
|
||||
a backward BFS and the forward eccentricity of $a_2$ using a forward BFS.
|
||||
Finally, it returns the best lower bound between the two.
|
||||
|
||||
In both cases, the time complexity is linear with respect to the size of G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
d : integer
|
||||
Lower Bound on the Diameter of G
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(10) # undirected graph
|
||||
>>> nx.diameter(G)
|
||||
9
|
||||
>>> G = nx.cycle_graph(3, create_using=nx.DiGraph) # directed graph
|
||||
>>> nx.diameter(G)
|
||||
2
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If the graph is empty or
|
||||
If the graph is undirected and not connected or
|
||||
If the graph is directed and not strongly connected.
|
||||
|
||||
See Also
|
||||
--------
|
||||
networkx.algorithms.distance_measures.diameter
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Magnien, Clémence, Matthieu Latapy, and Michel Habib.
|
||||
*Fast computation of empirically tight bounds for the diameter of massive graphs.*
|
||||
Journal of Experimental Algorithmics (JEA), 2009.
|
||||
https://arxiv.org/pdf/0904.2728.pdf
|
||||
.. [2] Crescenzi, Pierluigi, Roberto Grossi, Leonardo Lanzi, and Andrea Marino.
|
||||
*On computing the diameter of real-world directed (weighted) graphs.*
|
||||
International Symposium on Experimental Algorithms. Springer, Berlin, Heidelberg, 2012.
|
||||
https://courses.cs.ut.ee/MTAT.03.238/2014_fall/uploads/Main/diameter.pdf
|
||||
"""
|
||||
# if G is empty
|
||||
if not G:
|
||||
raise nx.NetworkXError("Expected non-empty NetworkX graph!")
|
||||
# if there's only a node
|
||||
if G.number_of_nodes() == 1:
|
||||
return 0
|
||||
# if G is directed
|
||||
if G.is_directed():
|
||||
return _two_sweep_directed(G, seed)
|
||||
# else if G is undirected
|
||||
return _two_sweep_undirected(G, seed)
|
||||
|
||||
|
||||
def _two_sweep_undirected(G, seed):
|
||||
"""Helper function for finding a lower bound on the diameter
|
||||
for undirected Graphs.
|
||||
|
||||
The idea is to pick the farthest node from a random node
|
||||
and return its eccentricity.
|
||||
|
||||
``G`` is a NetworkX undirected graph.
|
||||
|
||||
.. note::
|
||||
|
||||
``seed`` is a random.Random or numpy.random.RandomState instance
|
||||
"""
|
||||
# select a random source node
|
||||
source = seed.choice(list(G))
|
||||
# get the distances to the other nodes
|
||||
distances = nx.shortest_path_length(G, source)
|
||||
# if some nodes have not been visited, then the graph is not connected
|
||||
if len(distances) != len(G):
|
||||
raise nx.NetworkXError("Graph not connected.")
|
||||
# take a node that is (one of) the farthest nodes from the source
|
||||
*_, node = distances
|
||||
# return the eccentricity of the node
|
||||
return nx.eccentricity(G, node)
|
||||
|
||||
|
||||
def _two_sweep_directed(G, seed):
|
||||
"""Helper function for finding a lower bound on the diameter
|
||||
for directed Graphs.
|
||||
|
||||
It implements 2-dSweep, the directed version of the 2-sweep algorithm.
|
||||
The algorithm follows the following steps.
|
||||
1. Select a source node $s$ at random.
|
||||
2. Perform a forward BFS from $s$ to select a node $a_1$ at the maximum
|
||||
distance from the source, and compute $LB_1$, the backward eccentricity of $a_1$.
|
||||
3. Perform a backward BFS from $s$ to select a node $a_2$ at the maximum
|
||||
distance from the source, and compute $LB_2$, the forward eccentricity of $a_2$.
|
||||
4. Return the maximum between $LB_1$ and $LB_2$.
|
||||
|
||||
``G`` is a NetworkX directed graph.
|
||||
|
||||
.. note::
|
||||
|
||||
``seed`` is a random.Random or numpy.random.RandomState instance
|
||||
"""
|
||||
# get a new digraph G' with the edges reversed in the opposite direction
|
||||
G_reversed = G.reverse()
|
||||
# select a random source node
|
||||
source = seed.choice(list(G))
|
||||
# compute forward distances from source
|
||||
forward_distances = nx.shortest_path_length(G, source)
|
||||
# compute backward distances from source
|
||||
backward_distances = nx.shortest_path_length(G_reversed, source)
|
||||
# if either the source can't reach every node or not every node
|
||||
# can reach the source, then the graph is not strongly connected
|
||||
n = len(G)
|
||||
if len(forward_distances) != n or len(backward_distances) != n:
|
||||
raise nx.NetworkXError("DiGraph not strongly connected.")
|
||||
# take a node a_1 at the maximum distance from the source in G
|
||||
*_, a_1 = forward_distances
|
||||
# take a node a_2 at the maximum distance from the source in G_reversed
|
||||
*_, a_2 = backward_distances
|
||||
# return the max between the backward eccentricity of a_1 and the forward eccentricity of a_2
|
||||
return max(nx.eccentricity(G_reversed, a_1), nx.eccentricity(G, a_2))
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
"""Functions for finding node and edge dominating sets.
|
||||
|
||||
A `dominating set`_ for an undirected graph *G* with vertex set *V*
|
||||
and edge set *E* is a subset *D* of *V* such that every vertex not in
|
||||
*D* is adjacent to at least one member of *D*. An `edge dominating set`_
|
||||
is a subset *F* of *E* such that every edge not in *F* is
|
||||
incident to an endpoint of at least one edge in *F*.
|
||||
|
||||
.. _dominating set: https://en.wikipedia.org/wiki/Dominating_set
|
||||
.. _edge dominating set: https://en.wikipedia.org/wiki/Edge_dominating_set
|
||||
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
|
||||
from ...utils import not_implemented_for
|
||||
from ..matching import maximal_matching
|
||||
|
||||
__all__ = ["min_weighted_dominating_set", "min_edge_dominating_set"]
|
||||
|
||||
|
||||
# TODO Why doesn't this algorithm work for directed graphs?
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(node_attrs="weight")
|
||||
def min_weighted_dominating_set(G, weight=None):
|
||||
r"""Returns a dominating set that approximates the minimum weight node
|
||||
dominating set.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph.
|
||||
|
||||
weight : string
|
||||
The node attribute storing the weight of an node. If provided,
|
||||
the node attribute with this key must be a number for each
|
||||
node. If not provided, each node is assumed to have weight one.
|
||||
|
||||
Returns
|
||||
-------
|
||||
min_weight_dominating_set : set
|
||||
A set of nodes, the sum of whose weights is no more than `(\log
|
||||
w(V)) w(V^*)`, where `w(V)` denotes the sum of the weights of
|
||||
each node in the graph and `w(V^*)` denotes the sum of the
|
||||
weights of each node in the minimum weight dominating set.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.Graph([(0, 1), (0, 4), (1, 4), (1, 2), (2, 3), (3, 4), (2, 5)])
|
||||
>>> nx.approximation.min_weighted_dominating_set(G)
|
||||
{1, 2, 4}
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If G is directed.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm computes an approximate minimum weighted dominating
|
||||
set for the graph `G`. The returned solution has weight `(\log
|
||||
w(V)) w(V^*)`, where `w(V)` denotes the sum of the weights of each
|
||||
node in the graph and `w(V^*)` denotes the sum of the weights of
|
||||
each node in the minimum weight dominating set for the graph.
|
||||
|
||||
This implementation of the algorithm runs in $O(m)$ time, where $m$
|
||||
is the number of edges in the graph.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Vazirani, Vijay V.
|
||||
*Approximation Algorithms*.
|
||||
Springer Science & Business Media, 2001.
|
||||
|
||||
"""
|
||||
# The unique dominating set for the null graph is the empty set.
|
||||
if len(G) == 0:
|
||||
return set()
|
||||
|
||||
# This is the dominating set that will eventually be returned.
|
||||
dom_set = set()
|
||||
|
||||
def _cost(node_and_neighborhood):
|
||||
"""Returns the cost-effectiveness of greedily choosing the given
|
||||
node.
|
||||
|
||||
`node_and_neighborhood` is a two-tuple comprising a node and its
|
||||
closed neighborhood.
|
||||
|
||||
"""
|
||||
v, neighborhood = node_and_neighborhood
|
||||
return G.nodes[v].get(weight, 1) / len(neighborhood - dom_set)
|
||||
|
||||
# This is a set of all vertices not already covered by the
|
||||
# dominating set.
|
||||
vertices = set(G)
|
||||
# This is a dictionary mapping each node to the closed neighborhood
|
||||
# of that node.
|
||||
neighborhoods = {v: {v} | set(G[v]) for v in G}
|
||||
|
||||
# Continue until all vertices are adjacent to some node in the
|
||||
# dominating set.
|
||||
while vertices:
|
||||
# Find the most cost-effective node to add, along with its
|
||||
# closed neighborhood.
|
||||
dom_node, min_set = min(neighborhoods.items(), key=_cost)
|
||||
# Add the node to the dominating set and reduce the remaining
|
||||
# set of nodes to cover.
|
||||
dom_set.add(dom_node)
|
||||
del neighborhoods[dom_node]
|
||||
vertices -= min_set
|
||||
|
||||
return dom_set
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def min_edge_dominating_set(G):
|
||||
r"""Returns minimum cardinality edge dominating set.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
min_edge_dominating_set : set
|
||||
Returns a set of dominating edges whose size is no more than 2 * OPT.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.petersen_graph()
|
||||
>>> nx.approximation.min_edge_dominating_set(G)
|
||||
{(0, 1), (4, 9), (6, 8), (5, 7), (2, 3)}
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If the input graph `G` is empty.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm computes an approximate solution to the edge dominating set
|
||||
problem. The result is no more than 2 * OPT in terms of size of the set.
|
||||
Runtime of the algorithm is $O(|E|)$.
|
||||
"""
|
||||
if not G:
|
||||
raise ValueError("Expected non-empty NetworkX graph!")
|
||||
return maximal_matching(G)
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
"""Fast approximation for k-component structure"""
|
||||
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping
|
||||
from functools import cached_property
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.approximation import local_node_connectivity
|
||||
from networkx.exception import NetworkXError
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = ["k_components"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(name="approximate_k_components")
|
||||
def k_components(G, min_density=0.95):
|
||||
r"""Returns the approximate k-component structure of a graph G.
|
||||
|
||||
A `k`-component is a maximal subgraph of a graph G that has, at least,
|
||||
node connectivity `k`: we need to remove at least `k` nodes to break it
|
||||
into more components. `k`-components have an inherent hierarchical
|
||||
structure because they are nested in terms of connectivity: a connected
|
||||
graph can contain several 2-components, each of which can contain
|
||||
one or more 3-components, and so forth.
|
||||
|
||||
This implementation is based on the fast heuristics to approximate
|
||||
the `k`-component structure of a graph [1]_. Which, in turn, it is based on
|
||||
a fast approximation algorithm for finding good lower bounds of the number
|
||||
of node independent paths between two nodes [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
min_density : Float
|
||||
Density relaxation threshold. Default value 0.95
|
||||
|
||||
Returns
|
||||
-------
|
||||
k_components : dict
|
||||
Dictionary with connectivity level `k` as key and a list of
|
||||
sets of nodes that form a k-component of level `k` as values.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If G is directed.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # Petersen graph has 10 nodes and it is triconnected, thus all
|
||||
>>> # nodes are in a single component on all three connectivity levels
|
||||
>>> from networkx.algorithms import approximation as apxa
|
||||
>>> G = nx.petersen_graph()
|
||||
>>> k_components = apxa.k_components(G)
|
||||
|
||||
Notes
|
||||
-----
|
||||
The logic of the approximation algorithm for computing the `k`-component
|
||||
structure [1]_ is based on repeatedly applying simple and fast algorithms
|
||||
for `k`-cores and biconnected components in order to narrow down the
|
||||
number of pairs of nodes over which we have to compute White and Newman's
|
||||
approximation algorithm for finding node independent paths [2]_. More
|
||||
formally, this algorithm is based on Whitney's theorem, which states
|
||||
an inclusion relation among node connectivity, edge connectivity, and
|
||||
minimum degree for any graph G. This theorem implies that every
|
||||
`k`-component is nested inside a `k`-edge-component, which in turn,
|
||||
is contained in a `k`-core. Thus, this algorithm computes node independent
|
||||
paths among pairs of nodes in each biconnected part of each `k`-core,
|
||||
and repeats this procedure for each `k` from 3 to the maximal core number
|
||||
of a node in the input graph.
|
||||
|
||||
Because, in practice, many nodes of the core of level `k` inside a
|
||||
bicomponent actually are part of a component of level k, the auxiliary
|
||||
graph needed for the algorithm is likely to be very dense. Thus, we use
|
||||
a complement graph data structure (see `AntiGraph`) to save memory.
|
||||
AntiGraph only stores information of the edges that are *not* present
|
||||
in the actual auxiliary graph. When applying algorithms to this
|
||||
complement graph data structure, it behaves as if it were the dense
|
||||
version.
|
||||
|
||||
See also
|
||||
--------
|
||||
k_components
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Torrents, J. and F. Ferraro (2015) Structural Cohesion:
|
||||
Visualization and Heuristics for Fast Computation.
|
||||
https://arxiv.org/pdf/1503.04476v1
|
||||
|
||||
.. [2] White, Douglas R., and Mark Newman (2001) A Fast Algorithm for
|
||||
Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
|
||||
https://www.santafe.edu/research/results/working-papers/fast-approximation-algorithms-for-finding-node-ind
|
||||
|
||||
.. [3] Moody, J. and D. White (2003). Social cohesion and embeddedness:
|
||||
A hierarchical conception of social groups.
|
||||
American Sociological Review 68(1), 103--28.
|
||||
https://doi.org/10.2307/3088904
|
||||
|
||||
"""
|
||||
# Dictionary with connectivity level (k) as keys and a list of
|
||||
# sets of nodes that form a k-component as values
|
||||
k_components = defaultdict(list)
|
||||
# make a few functions local for speed
|
||||
node_connectivity = local_node_connectivity
|
||||
k_core = nx.k_core
|
||||
core_number = nx.core_number
|
||||
biconnected_components = nx.biconnected_components
|
||||
combinations = itertools.combinations
|
||||
# Exact solution for k = {1,2}
|
||||
# There is a linear time algorithm for triconnectivity, if we had an
|
||||
# implementation available we could start from k = 4.
|
||||
for component in nx.connected_components(G):
|
||||
# isolated nodes have connectivity 0
|
||||
comp = set(component)
|
||||
if len(comp) > 1:
|
||||
k_components[1].append(comp)
|
||||
for bicomponent in nx.biconnected_components(G):
|
||||
# avoid considering dyads as bicomponents
|
||||
bicomp = set(bicomponent)
|
||||
if len(bicomp) > 2:
|
||||
k_components[2].append(bicomp)
|
||||
# There is no k-component of k > maximum core number
|
||||
# \kappa(G) <= \lambda(G) <= \delta(G)
|
||||
g_cnumber = core_number(G)
|
||||
max_core = max(g_cnumber.values())
|
||||
for k in range(3, max_core + 1):
|
||||
C = k_core(G, k, core_number=g_cnumber)
|
||||
for nodes in biconnected_components(C):
|
||||
# Build a subgraph SG induced by the nodes that are part of
|
||||
# each biconnected component of the k-core subgraph C.
|
||||
if len(nodes) < k:
|
||||
continue
|
||||
SG = G.subgraph(nodes)
|
||||
# Build auxiliary graph
|
||||
H = _AntiGraph()
|
||||
H.add_nodes_from(SG.nodes())
|
||||
for u, v in combinations(SG, 2):
|
||||
K = node_connectivity(SG, u, v, cutoff=k)
|
||||
if k > K:
|
||||
H.add_edge(u, v)
|
||||
for h_nodes in biconnected_components(H):
|
||||
if len(h_nodes) <= k:
|
||||
continue
|
||||
SH = H.subgraph(h_nodes)
|
||||
for Gc in _cliques_heuristic(SG, SH, k, min_density):
|
||||
for k_nodes in biconnected_components(Gc):
|
||||
Gk = nx.k_core(SG.subgraph(k_nodes), k)
|
||||
if len(Gk) <= k:
|
||||
continue
|
||||
k_components[k].append(set(Gk))
|
||||
return k_components
|
||||
|
||||
|
||||
def _cliques_heuristic(G, H, k, min_density):
|
||||
h_cnumber = nx.core_number(H)
|
||||
for i, c_value in enumerate(sorted(set(h_cnumber.values()), reverse=True)):
|
||||
cands = {n for n, c in h_cnumber.items() if c == c_value}
|
||||
# Skip checking for overlap for the highest core value
|
||||
if i == 0:
|
||||
overlap = False
|
||||
else:
|
||||
overlap = set.intersection(
|
||||
*[{x for x in H[n] if x not in cands} for n in cands]
|
||||
)
|
||||
if overlap and len(overlap) < k:
|
||||
SH = H.subgraph(cands | overlap)
|
||||
else:
|
||||
SH = H.subgraph(cands)
|
||||
sh_cnumber = nx.core_number(SH)
|
||||
SG = nx.k_core(G.subgraph(SH), k)
|
||||
while not (_same(sh_cnumber) and nx.density(SH) >= min_density):
|
||||
# This subgraph must be writable => .copy()
|
||||
SH = H.subgraph(SG).copy()
|
||||
if len(SH) <= k:
|
||||
break
|
||||
sh_cnumber = nx.core_number(SH)
|
||||
sh_deg = dict(SH.degree())
|
||||
min_deg = min(sh_deg.values())
|
||||
SH.remove_nodes_from(n for n, d in sh_deg.items() if d == min_deg)
|
||||
SG = nx.k_core(G.subgraph(SH), k)
|
||||
else:
|
||||
yield SG
|
||||
|
||||
|
||||
def _same(measure, tol=0):
|
||||
vals = set(measure.values())
|
||||
if (max(vals) - min(vals)) <= tol:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class _AntiGraph(nx.Graph):
|
||||
"""
|
||||
Class for complement graphs.
|
||||
|
||||
The main goal is to be able to work with big and dense graphs with
|
||||
a low memory footprint.
|
||||
|
||||
In this class you add the edges that *do not exist* in the dense graph,
|
||||
the report methods of the class return the neighbors, the edges and
|
||||
the degree as if it was the dense graph. Thus it's possible to use
|
||||
an instance of this class with some of NetworkX functions. In this
|
||||
case we only use k-core, connected_components, and biconnected_components.
|
||||
"""
|
||||
|
||||
all_edge_dict = {"weight": 1}
|
||||
|
||||
def single_edge_dict(self):
|
||||
return self.all_edge_dict
|
||||
|
||||
edge_attr_dict_factory = single_edge_dict # type: ignore[assignment]
|
||||
|
||||
def __getitem__(self, n):
|
||||
"""Returns a dict of neighbors of node n in the dense graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : node
|
||||
A node in the graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
adj_dict : dictionary
|
||||
The adjacency dictionary for nodes connected to n.
|
||||
|
||||
"""
|
||||
all_edge_dict = self.all_edge_dict
|
||||
return dict.fromkeys(set(self._adj) - set(self._adj[n]) - {n}, all_edge_dict)
|
||||
|
||||
def neighbors(self, n):
|
||||
"""Returns an iterator over all neighbors of node n in the
|
||||
dense graph.
|
||||
"""
|
||||
try:
|
||||
return iter(set(self._adj) - set(self._adj[n]) - {n})
|
||||
except KeyError as err:
|
||||
raise NetworkXError(f"The node {n} is not in the graph.") from err
|
||||
|
||||
class AntiAtlasView(Mapping):
|
||||
"""An adjacency inner dict for AntiGraph"""
|
||||
|
||||
def __init__(self, graph, node):
|
||||
self._graph = graph
|
||||
self._atlas = graph._adj[node]
|
||||
self._node = node
|
||||
|
||||
def __len__(self):
|
||||
return len(self._graph) - len(self._atlas) - 1
|
||||
|
||||
def __iter__(self):
|
||||
return (n for n in self._graph if n not in self._atlas and n != self._node)
|
||||
|
||||
def __getitem__(self, nbr):
|
||||
nbrs = set(self._graph._adj) - set(self._atlas) - {self._node}
|
||||
if nbr in nbrs:
|
||||
return self._graph.all_edge_dict
|
||||
raise KeyError(nbr)
|
||||
|
||||
class AntiAdjacencyView(AntiAtlasView):
|
||||
"""An adjacency outer dict for AntiGraph"""
|
||||
|
||||
def __init__(self, graph):
|
||||
self._graph = graph
|
||||
self._atlas = graph._adj
|
||||
|
||||
def __len__(self):
|
||||
return len(self._atlas)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._graph)
|
||||
|
||||
def __getitem__(self, node):
|
||||
if node not in self._graph:
|
||||
raise KeyError(node)
|
||||
return self._graph.AntiAtlasView(self._graph, node)
|
||||
|
||||
@cached_property
|
||||
def adj(self):
|
||||
return self.AntiAdjacencyView(self)
|
||||
|
||||
def subgraph(self, nodes):
|
||||
"""This subgraph method returns a full AntiGraph. Not a View"""
|
||||
nodes = set(nodes)
|
||||
G = _AntiGraph()
|
||||
G.add_nodes_from(nodes)
|
||||
for n in G:
|
||||
Gnbrs = G.adjlist_inner_dict_factory()
|
||||
G._adj[n] = Gnbrs
|
||||
for nbr, d in self._adj[n].items():
|
||||
if nbr in G._adj:
|
||||
Gnbrs[nbr] = d
|
||||
G._adj[nbr][n] = d
|
||||
G.graph = self.graph
|
||||
return G
|
||||
|
||||
class AntiDegreeView(nx.reportviews.DegreeView):
|
||||
def __iter__(self):
|
||||
all_nodes = set(self._succ)
|
||||
for n in self._nodes:
|
||||
nbrs = all_nodes - set(self._succ[n]) - {n}
|
||||
yield (n, len(nbrs))
|
||||
|
||||
def __getitem__(self, n):
|
||||
nbrs = set(self._succ) - set(self._succ[n]) - {n}
|
||||
# AntiGraph is a ThinGraph so all edges have weight 1
|
||||
return len(nbrs) + (n in nbrs)
|
||||
|
||||
@cached_property
|
||||
def degree(self):
|
||||
"""Returns an iterator for (node, degree) and degree for single node.
|
||||
|
||||
The node degree is the number of edges adjacent to the node.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nbunch : iterable container, optional (default=all nodes)
|
||||
A container of nodes. The container will be iterated
|
||||
through once.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
The edge attribute that holds the numerical value used
|
||||
as a weight. If None, then each edge has weight 1.
|
||||
The degree is the sum of the edge weights adjacent to the node.
|
||||
|
||||
Returns
|
||||
-------
|
||||
deg:
|
||||
Degree of the node, if a single node is passed as argument.
|
||||
nd_iter : an iterator
|
||||
The iterator returns two-tuples of (node, degree).
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> G.degree(0) # node 0 with degree 1
|
||||
1
|
||||
>>> list(G.degree([0, 1]))
|
||||
[(0, 1), (1, 2)]
|
||||
|
||||
"""
|
||||
return self.AntiDegreeView(self)
|
||||
|
||||
def adjacency(self):
|
||||
"""Returns an iterator of (node, adjacency set) tuples for all nodes
|
||||
in the dense graph.
|
||||
|
||||
This is the fastest way to look at every edge.
|
||||
For directed graphs, only outgoing adjacencies are included.
|
||||
|
||||
Returns
|
||||
-------
|
||||
adj_iter : iterator
|
||||
An iterator of (node, adjacency set) for all nodes in
|
||||
the graph.
|
||||
|
||||
"""
|
||||
for n in self._adj:
|
||||
yield (n, set(self._adj) - set(self._adj[n]) - {n})
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
"""
|
||||
**************
|
||||
Graph Matching
|
||||
**************
|
||||
|
||||
Given a graph G = (V,E), a matching M in G is a set of pairwise non-adjacent
|
||||
edges; that is, no two edges share a common vertex.
|
||||
|
||||
`Wikipedia: Matching <https://en.wikipedia.org/wiki/Matching_(graph_theory)>`_
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["min_maximal_matching"]
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def min_maximal_matching(G):
|
||||
r"""Returns the minimum maximal matching of G. That is, out of all maximal
|
||||
matchings of the graph G, the smallest is returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
min_maximal_matching : set
|
||||
Returns a set of edges such that no two edges share a common endpoint
|
||||
and every edge not in the set shares some common endpoint in the set.
|
||||
Cardinality will be 2*OPT in the worst case.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm computes an approximate solution for the minimum maximal
|
||||
cardinality matching problem. The solution is no more than 2 * OPT in size.
|
||||
Runtime is $O(|E|)$.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Vazirani, Vijay Approximation Algorithms (2001)
|
||||
"""
|
||||
return nx.maximal_matching(G)
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import networkx as nx
|
||||
from networkx.utils.decorators import not_implemented_for, py_random_state
|
||||
|
||||
__all__ = ["randomized_partitioning", "one_exchange"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@py_random_state(1)
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def randomized_partitioning(G, seed=None, p=0.5, weight=None):
|
||||
"""Compute a random partitioning of the graph nodes and its cut value.
|
||||
|
||||
A partitioning is calculated by observing each node
|
||||
and deciding to add it to the partition with probability `p`,
|
||||
returning a random cut and its corresponding value (the
|
||||
sum of weights of edges connecting different partitions).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
p : scalar
|
||||
Probability for each node to be part of the first partition.
|
||||
Should be in [0,1]
|
||||
|
||||
weight : object
|
||||
Edge attribute key to use as weight. If not specified, edges
|
||||
have weight one.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cut_size : scalar
|
||||
Value of the minimum cut.
|
||||
|
||||
partition : pair of node sets
|
||||
A partitioning of the nodes that defines a minimum cut.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.complete_graph(5)
|
||||
>>> cut_size, partition = nx.approximation.randomized_partitioning(G, seed=1)
|
||||
>>> cut_size
|
||||
6
|
||||
>>> partition
|
||||
({0, 3, 4}, {1, 2})
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
"""
|
||||
cut = {node for node in G.nodes() if seed.random() < p}
|
||||
cut_size = nx.algorithms.cut_size(G, cut, weight=weight)
|
||||
partition = (cut, G.nodes - cut)
|
||||
return cut_size, partition
|
||||
|
||||
|
||||
def _swap_node_partition(cut, node):
|
||||
return cut - {node} if node in cut else cut.union({node})
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@py_random_state(2)
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def one_exchange(G, initial_cut=None, seed=None, weight=None):
|
||||
"""Compute a partitioning of the graphs nodes and the corresponding cut value.
|
||||
|
||||
Use a greedy one exchange strategy to find a locally maximal cut
|
||||
and its value, it works by finding the best node (one that gives
|
||||
the highest gain to the cut value) to add to the current cut
|
||||
and repeats this process until no improvement can be made.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : networkx Graph
|
||||
Graph to find a maximum cut for.
|
||||
|
||||
initial_cut : set
|
||||
Cut to use as a starting point. If not supplied the algorithm
|
||||
starts with an empty cut.
|
||||
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
weight : object
|
||||
Edge attribute key to use as weight. If not specified, edges
|
||||
have weight one.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cut_value : scalar
|
||||
Value of the maximum cut.
|
||||
|
||||
partition : pair of node sets
|
||||
A partitioning of the nodes that defines a maximum cut.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.complete_graph(5)
|
||||
>>> curr_cut_size, partition = nx.approximation.one_exchange(G, seed=1)
|
||||
>>> curr_cut_size
|
||||
6
|
||||
>>> partition
|
||||
({0, 2}, {1, 3, 4})
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
"""
|
||||
if initial_cut is None:
|
||||
initial_cut = set()
|
||||
cut = set(initial_cut)
|
||||
current_cut_size = nx.algorithms.cut_size(G, cut, weight=weight)
|
||||
while True:
|
||||
nodes = list(G.nodes())
|
||||
# Shuffling the nodes ensures random tie-breaks in the following call to max
|
||||
seed.shuffle(nodes)
|
||||
best_node_to_swap = max(
|
||||
nodes,
|
||||
key=lambda v: nx.algorithms.cut_size(
|
||||
G, _swap_node_partition(cut, v), weight=weight
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
potential_cut = _swap_node_partition(cut, best_node_to_swap)
|
||||
potential_cut_size = nx.algorithms.cut_size(G, potential_cut, weight=weight)
|
||||
|
||||
if potential_cut_size > current_cut_size:
|
||||
cut = potential_cut
|
||||
current_cut_size = potential_cut_size
|
||||
else:
|
||||
break
|
||||
|
||||
partition = (cut, G.nodes - cut)
|
||||
return current_cut_size, partition
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
"""
|
||||
Ramsey numbers.
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
from ...utils import arbitrary_element
|
||||
|
||||
__all__ = ["ramsey_R2"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def ramsey_R2(G):
|
||||
r"""Compute the largest clique and largest independent set in `G`.
|
||||
|
||||
This can be used to estimate bounds for the 2-color
|
||||
Ramsey number `R(2;s,t)` for `G`.
|
||||
|
||||
This is a recursive implementation which could run into trouble
|
||||
for large recursions. Note that self-loop edges are ignored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
Undirected graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
max_pair : (set, set) tuple
|
||||
Maximum clique, Maximum independent set.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If the graph is directed or is a multigraph.
|
||||
"""
|
||||
if not G:
|
||||
return set(), set()
|
||||
|
||||
node = arbitrary_element(G)
|
||||
nbrs = (nbr for nbr in nx.all_neighbors(G, node) if nbr != node)
|
||||
nnbrs = nx.non_neighbors(G, node)
|
||||
c_1, i_1 = ramsey_R2(G.subgraph(nbrs).copy())
|
||||
c_2, i_2 = ramsey_R2(G.subgraph(nnbrs).copy())
|
||||
|
||||
c_1.add(node)
|
||||
i_2.add(node)
|
||||
# Choose the larger of the two cliques and the larger of the two
|
||||
# independent sets, according to cardinality.
|
||||
return max(c_1, c_2, key=len), max(i_1, i_2, key=len)
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
from itertools import chain
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for, pairwise
|
||||
|
||||
__all__ = ["metric_closure", "steiner_tree"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(edge_attrs="weight", returns_graph=True)
|
||||
def metric_closure(G, weight="weight"):
|
||||
"""Return the metric closure of a graph.
|
||||
|
||||
The metric closure of a graph *G* is the complete graph in which each edge
|
||||
is weighted by the shortest path distance between the nodes in *G* .
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
NetworkX graph
|
||||
Metric closure of the graph `G`.
|
||||
|
||||
"""
|
||||
M = nx.Graph()
|
||||
|
||||
Gnodes = set(G)
|
||||
|
||||
# check for connected graph while processing first node
|
||||
all_paths_iter = nx.all_pairs_dijkstra(G, weight=weight)
|
||||
u, (distance, path) = next(all_paths_iter)
|
||||
if len(G) != len(distance):
|
||||
msg = "G is not a connected graph. metric_closure is not defined."
|
||||
raise nx.NetworkXError(msg)
|
||||
Gnodes.remove(u)
|
||||
for v in Gnodes:
|
||||
M.add_edge(u, v, distance=distance[v], path=path[v])
|
||||
|
||||
# first node done -- now process the rest
|
||||
for u, (distance, path) in all_paths_iter:
|
||||
Gnodes.remove(u)
|
||||
for v in Gnodes:
|
||||
M.add_edge(u, v, distance=distance[v], path=path[v])
|
||||
|
||||
return M
|
||||
|
||||
|
||||
def _mehlhorn_steiner_tree(G, terminal_nodes, weight):
|
||||
paths = nx.multi_source_dijkstra_path(G, terminal_nodes)
|
||||
|
||||
d_1 = {}
|
||||
s = {}
|
||||
for v in G.nodes():
|
||||
s[v] = paths[v][0]
|
||||
d_1[(v, s[v])] = len(paths[v]) - 1
|
||||
|
||||
# G1-G4 names match those from the Mehlhorn 1988 paper.
|
||||
G_1_prime = nx.Graph()
|
||||
for u, v, data in G.edges(data=True):
|
||||
su, sv = s[u], s[v]
|
||||
weight_here = d_1[(u, su)] + data.get(weight, 1) + d_1[(v, sv)]
|
||||
if not G_1_prime.has_edge(su, sv):
|
||||
G_1_prime.add_edge(su, sv, weight=weight_here)
|
||||
else:
|
||||
new_weight = min(weight_here, G_1_prime[su][sv]["weight"])
|
||||
G_1_prime.add_edge(su, sv, weight=new_weight)
|
||||
|
||||
G_2 = nx.minimum_spanning_edges(G_1_prime, data=True)
|
||||
|
||||
G_3 = nx.Graph()
|
||||
for u, v, d in G_2:
|
||||
path = nx.shortest_path(G, u, v, weight)
|
||||
for n1, n2 in pairwise(path):
|
||||
G_3.add_edge(n1, n2)
|
||||
|
||||
G_3_mst = list(nx.minimum_spanning_edges(G_3, data=False))
|
||||
if G.is_multigraph():
|
||||
G_3_mst = (
|
||||
(u, v, min(G[u][v], key=lambda k: G[u][v][k][weight])) for u, v in G_3_mst
|
||||
)
|
||||
G_4 = G.edge_subgraph(G_3_mst).copy()
|
||||
_remove_nonterminal_leaves(G_4, terminal_nodes)
|
||||
return G_4.edges()
|
||||
|
||||
|
||||
def _kou_steiner_tree(G, terminal_nodes, weight):
|
||||
# Compute the metric closure only for terminal nodes
|
||||
# Create a complete graph H from the metric edges
|
||||
H = nx.Graph()
|
||||
unvisited_terminals = set(terminal_nodes)
|
||||
|
||||
# check for connected graph while processing first node
|
||||
u = unvisited_terminals.pop()
|
||||
distances, paths = nx.single_source_dijkstra(G, source=u, weight=weight)
|
||||
if len(G) != len(distances):
|
||||
msg = "G is not a connected graph."
|
||||
raise nx.NetworkXError(msg)
|
||||
for v in unvisited_terminals:
|
||||
H.add_edge(u, v, distance=distances[v], path=paths[v])
|
||||
|
||||
# first node done -- now process the rest
|
||||
for u in unvisited_terminals.copy():
|
||||
distances, paths = nx.single_source_dijkstra(G, source=u, weight=weight)
|
||||
unvisited_terminals.remove(u)
|
||||
for v in unvisited_terminals:
|
||||
H.add_edge(u, v, distance=distances[v], path=paths[v])
|
||||
|
||||
# Use the 'distance' attribute of each edge provided by H.
|
||||
mst_edges = nx.minimum_spanning_edges(H, weight="distance", data=True)
|
||||
|
||||
# Create an iterator over each edge in each shortest path; repeats are okay
|
||||
mst_all_edges = chain.from_iterable(pairwise(d["path"]) for u, v, d in mst_edges)
|
||||
if G.is_multigraph():
|
||||
mst_all_edges = (
|
||||
(u, v, min(G[u][v], key=lambda k: G[u][v][k][weight]))
|
||||
for u, v in mst_all_edges
|
||||
)
|
||||
|
||||
# Find the MST again, over this new set of edges
|
||||
G_S = G.edge_subgraph(mst_all_edges)
|
||||
T_S = nx.minimum_spanning_edges(G_S, weight="weight", data=False)
|
||||
|
||||
# Leaf nodes that are not terminal might still remain; remove them here
|
||||
T_H = G.edge_subgraph(T_S).copy()
|
||||
_remove_nonterminal_leaves(T_H, terminal_nodes)
|
||||
|
||||
return T_H.edges()
|
||||
|
||||
|
||||
def _remove_nonterminal_leaves(G, terminals):
|
||||
terminal_set = set(terminals)
|
||||
leaves = {n for n in G if len(set(G[n]) - {n}) == 1}
|
||||
nonterminal_leaves = leaves - terminal_set
|
||||
|
||||
while nonterminal_leaves:
|
||||
# Removing a node may create new non-terminal leaves, so we limit
|
||||
# search for candidate non-terminal nodes to neighbors of current
|
||||
# non-terminal nodes
|
||||
candidate_leaves = set.union(*(set(G[n]) for n in nonterminal_leaves))
|
||||
candidate_leaves -= nonterminal_leaves | terminal_set
|
||||
# Remove current set of non-terminal nodes
|
||||
G.remove_nodes_from(nonterminal_leaves)
|
||||
# Find any new non-terminal nodes from the set of candidates
|
||||
leaves = {n for n in candidate_leaves if len(set(G[n]) - {n}) == 1}
|
||||
nonterminal_leaves = leaves - terminal_set
|
||||
|
||||
|
||||
ALGORITHMS = {
|
||||
"kou": _kou_steiner_tree,
|
||||
"mehlhorn": _mehlhorn_steiner_tree,
|
||||
}
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
|
||||
def steiner_tree(G, terminal_nodes, weight="weight", method=None):
|
||||
r"""Return an approximation to the minimum Steiner tree of a graph.
|
||||
|
||||
The minimum Steiner tree of `G` w.r.t a set of `terminal_nodes` (also *S*)
|
||||
is a tree within `G` that spans those nodes and has minimum size (sum of
|
||||
edge weights) among all such trees.
|
||||
|
||||
The approximation algorithm is specified with the `method` keyword
|
||||
argument. All three available algorithms produce a tree whose weight is
|
||||
within a ``(2 - (2 / l))`` factor of the weight of the optimal Steiner tree,
|
||||
where ``l`` is the minimum number of leaf nodes across all possible Steiner
|
||||
trees.
|
||||
|
||||
* ``"kou"`` [2]_ (runtime $O(|S| |V|^2)$) computes the minimum spanning tree of
|
||||
the subgraph of the metric closure of *G* induced by the terminal nodes,
|
||||
where the metric closure of *G* is the complete graph in which each edge is
|
||||
weighted by the shortest path distance between the nodes in *G*.
|
||||
|
||||
* ``"mehlhorn"`` [3]_ (runtime $O(|E|+|V|\log|V|)$) modifies Kou et al.'s
|
||||
algorithm, beginning by finding the closest terminal node for each
|
||||
non-terminal. This data is used to create a complete graph containing only
|
||||
the terminal nodes, in which edge is weighted with the shortest path
|
||||
distance between them. The algorithm then proceeds in the same way as Kou
|
||||
et al..
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
terminal_nodes : list
|
||||
A list of terminal nodes for which minimum steiner tree is
|
||||
to be found.
|
||||
|
||||
weight : string (default = 'weight')
|
||||
Use the edge attribute specified by this string as the edge weight.
|
||||
Any edge attribute not present defaults to 1.
|
||||
|
||||
method : string, optional (default = 'mehlhorn')
|
||||
The algorithm to use to approximate the Steiner tree.
|
||||
Supported options: 'kou', 'mehlhorn'.
|
||||
Other inputs produce a ValueError.
|
||||
|
||||
Returns
|
||||
-------
|
||||
NetworkX graph
|
||||
Approximation to the minimum steiner tree of `G` induced by
|
||||
`terminal_nodes` .
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If `G` is directed.
|
||||
|
||||
ValueError
|
||||
If the specified `method` is not supported.
|
||||
|
||||
Notes
|
||||
-----
|
||||
For multigraphs, the edge between two nodes with minimum weight is the
|
||||
edge put into the Steiner tree.
|
||||
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Steiner_tree_problem on Wikipedia.
|
||||
https://en.wikipedia.org/wiki/Steiner_tree_problem
|
||||
.. [2] Kou, L., G. Markowsky, and L. Berman. 1981.
|
||||
‘A Fast Algorithm for Steiner Trees’.
|
||||
Acta Informatica 15 (2): 141–45.
|
||||
https://doi.org/10.1007/BF00288961.
|
||||
.. [3] Mehlhorn, Kurt. 1988.
|
||||
‘A Faster Approximation Algorithm for the Steiner Problem in Graphs’.
|
||||
Information Processing Letters 27 (3): 125–28.
|
||||
https://doi.org/10.1016/0020-0190(88)90066-X.
|
||||
"""
|
||||
if method is None:
|
||||
method = "mehlhorn"
|
||||
|
||||
try:
|
||||
algo = ALGORITHMS[method]
|
||||
except KeyError as e:
|
||||
raise ValueError(f"{method} is not a valid choice for an algorithm.") from e
|
||||
|
||||
edges = algo(G, terminal_nodes, weight)
|
||||
# For multigraph we should add the minimal weight edge keys
|
||||
if G.is_multigraph():
|
||||
edges = (
|
||||
(u, v, min(G[u][v], key=lambda k: G[u][v][k][weight])) for u, v in edges
|
||||
)
|
||||
T = G.edge_subgraph(edges)
|
||||
return T
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue