363 lines
13 KiB
Python
363 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
||
# Copyright (C) 2014 by
|
||
# Christian Olsson <chro@itu.dk>
|
||
# Jan Aagaard Meier <jmei@itu.dk>
|
||
# Henrik Haugbølle <hhau@itu.dk>
|
||
# Arya McCarthy <admccarthy@smu.edu>
|
||
# All rights reserved.
|
||
# BSD license.
|
||
"""
|
||
Greedy graph coloring using various strategies.
|
||
"""
|
||
from collections import defaultdict, deque
|
||
import itertools
|
||
|
||
import networkx as nx
|
||
from networkx.utils import arbitrary_element
|
||
from networkx.utils import py_random_state
|
||
from . import greedy_coloring_with_interchange as _interchange
|
||
|
||
__all__ = ['greedy_color', 'strategy_connected_sequential',
|
||
'strategy_connected_sequential_bfs',
|
||
'strategy_connected_sequential_dfs', 'strategy_independent_set',
|
||
'strategy_largest_first', 'strategy_random_sequential',
|
||
'strategy_saturation_largest_first', 'strategy_smallest_last']
|
||
|
||
|
||
def strategy_largest_first(G, colors):
|
||
"""Returns a list of the nodes of ``G`` in decreasing order by
|
||
degree.
|
||
|
||
``G`` is a NetworkX graph. ``colors`` is ignored.
|
||
|
||
"""
|
||
return sorted(G, key=G.degree, reverse=True)
|
||
|
||
|
||
@py_random_state(2)
|
||
def strategy_random_sequential(G, colors, seed=None):
|
||
"""Returns a random permutation of the nodes of ``G`` as a list.
|
||
|
||
``G`` is a NetworkX graph. ``colors`` is ignored.
|
||
|
||
seed : integer, random_state, or None (default)
|
||
Indicator of random number generation state.
|
||
See :ref:`Randomness<randomness>`.
|
||
"""
|
||
nodes = list(G)
|
||
seed.shuffle(nodes)
|
||
return nodes
|
||
|
||
|
||
def strategy_smallest_last(G, colors):
|
||
"""Returns a deque of the nodes of ``G``, "smallest" last.
|
||
|
||
Specifically, the degrees of each node are tracked in a bucket queue.
|
||
From this, the node of minimum degree is repeatedly popped from the
|
||
graph, updating its neighbors' degrees.
|
||
|
||
``G`` is a NetworkX graph. ``colors`` is ignored.
|
||
|
||
This implementation of the strategy runs in $O(n + m)$ time
|
||
(ignoring polylogarithmic factors), where $n$ is the number of nodes
|
||
and $m$ is the number of edges.
|
||
|
||
This strategy is related to :func:`strategy_independent_set`: if we
|
||
interpret each node removed as an independent set of size one, then
|
||
this strategy chooses an independent set of size one instead of a
|
||
maximal independent set.
|
||
|
||
"""
|
||
H = G.copy()
|
||
result = deque()
|
||
|
||
# Build initial degree list (i.e. the bucket queue data structure)
|
||
degrees = defaultdict(set) # set(), for fast random-access removals
|
||
lbound = float('inf')
|
||
for node, d in H.degree():
|
||
degrees[d].add(node)
|
||
lbound = min(lbound, d) # Lower bound on min-degree.
|
||
|
||
def find_min_degree():
|
||
# Save time by starting the iterator at `lbound`, not 0.
|
||
# The value that we find will be our new `lbound`, which we set later.
|
||
return next(d for d in itertools.count(lbound) if d in degrees)
|
||
|
||
for _ in G:
|
||
# Pop a min-degree node and add it to the list.
|
||
min_degree = find_min_degree()
|
||
u = degrees[min_degree].pop()
|
||
if not degrees[min_degree]: # Clean up the degree list.
|
||
del degrees[min_degree]
|
||
result.appendleft(u)
|
||
|
||
# Update degrees of removed node's neighbors.
|
||
for v in H[u]:
|
||
degree = H.degree(v)
|
||
degrees[degree].remove(v)
|
||
if not degrees[degree]: # Clean up the degree list.
|
||
del degrees[degree]
|
||
degrees[degree - 1].add(v)
|
||
|
||
# Finally, remove the node.
|
||
H.remove_node(u)
|
||
lbound = min_degree - 1 # Subtract 1 in case of tied neighbors.
|
||
|
||
return result
|
||
|
||
|
||
def _maximal_independent_set(G):
|
||
"""Returns a maximal independent set of nodes in ``G`` by repeatedly
|
||
choosing an independent node of minimum degree (with respect to the
|
||
subgraph of unchosen nodes).
|
||
|
||
"""
|
||
result = set()
|
||
remaining = set(G)
|
||
while remaining:
|
||
G = G.subgraph(remaining)
|
||
v = min(remaining, key=G.degree)
|
||
result.add(v)
|
||
remaining -= set(G[v]) | {v}
|
||
return result
|
||
|
||
|
||
def strategy_independent_set(G, colors):
|
||
"""Uses a greedy independent set removal strategy to determine the
|
||
colors.
|
||
|
||
This function updates ``colors`` **in-place** and return ``None``,
|
||
unlike the other strategy functions in this module.
|
||
|
||
This algorithm repeatedly finds and removes a maximal independent
|
||
set, assigning each node in the set an unused color.
|
||
|
||
``G`` is a NetworkX graph.
|
||
|
||
This strategy is related to :func:`strategy_smallest_last`: in that
|
||
strategy, an independent set of size one is chosen at each step
|
||
instead of a maximal independent set.
|
||
|
||
"""
|
||
remaining_nodes = set(G)
|
||
while len(remaining_nodes) > 0:
|
||
nodes = _maximal_independent_set(G.subgraph(remaining_nodes))
|
||
remaining_nodes -= nodes
|
||
for v in nodes:
|
||
yield v
|
||
|
||
|
||
def strategy_connected_sequential_bfs(G, colors):
|
||
"""Returns an iterable over nodes in ``G`` in the order given by a
|
||
breadth-first traversal.
|
||
|
||
The generated sequence has the property that for each node except
|
||
the first, at least one neighbor appeared earlier in the sequence.
|
||
|
||
``G`` is a NetworkX graph. ``colors`` is ignored.
|
||
|
||
"""
|
||
return strategy_connected_sequential(G, colors, 'bfs')
|
||
|
||
|
||
def strategy_connected_sequential_dfs(G, colors):
|
||
"""Returns an iterable over nodes in ``G`` in the order given by a
|
||
depth-first traversal.
|
||
|
||
The generated sequence has the property that for each node except
|
||
the first, at least one neighbor appeared earlier in the sequence.
|
||
|
||
``G`` is a NetworkX graph. ``colors`` is ignored.
|
||
|
||
"""
|
||
return strategy_connected_sequential(G, colors, 'dfs')
|
||
|
||
|
||
def strategy_connected_sequential(G, colors, traversal='bfs'):
|
||
"""Returns an iterable over nodes in ``G`` in the order given by a
|
||
breadth-first or depth-first traversal.
|
||
|
||
``traversal`` must be one of the strings ``'dfs'`` or ``'bfs'``,
|
||
representing depth-first traversal or breadth-first traversal,
|
||
respectively.
|
||
|
||
The generated sequence has the property that for each node except
|
||
the first, at least one neighbor appeared earlier in the sequence.
|
||
|
||
``G`` is a NetworkX graph. ``colors`` is ignored.
|
||
|
||
"""
|
||
if traversal == 'bfs':
|
||
traverse = nx.bfs_edges
|
||
elif traversal == 'dfs':
|
||
traverse = nx.dfs_edges
|
||
else:
|
||
raise nx.NetworkXError("Please specify one of the strings 'bfs' or"
|
||
" 'dfs' for connected sequential ordering")
|
||
for component in nx.connected_components(G):
|
||
source = arbitrary_element(component)
|
||
# Yield the source node, then all the nodes in the specified
|
||
# traversal order.
|
||
yield source
|
||
for (_, end) in traverse(G.subgraph(component), source):
|
||
yield end
|
||
|
||
|
||
def strategy_saturation_largest_first(G, colors):
|
||
"""Iterates over all the nodes of ``G`` in "saturation order" (also
|
||
known as "DSATUR").
|
||
|
||
``G`` is a NetworkX graph. ``colors`` is a dictionary mapping nodes of
|
||
``G`` to colors, for those nodes that have already been colored.
|
||
|
||
"""
|
||
distinct_colors = {v: set() for v in G}
|
||
for i in range(len(G)):
|
||
# On the first time through, simply choose the node of highest degree.
|
||
if i == 0:
|
||
node = max(G, key=G.degree)
|
||
yield node
|
||
# Add the color 0 to the distinct colors set for each
|
||
# neighbors of that node.
|
||
for v in G[node]:
|
||
distinct_colors[v].add(0)
|
||
else:
|
||
# Compute the maximum saturation and the set of nodes that
|
||
# achieve that saturation.
|
||
saturation = {v: len(c) for v, c in distinct_colors.items()
|
||
if v not in colors}
|
||
# Yield the node with the highest saturation, and break ties by
|
||
# degree.
|
||
node = max(saturation, key=lambda v: (saturation[v], G.degree(v)))
|
||
yield node
|
||
# Update the distinct color sets for the neighbors.
|
||
color = colors[node]
|
||
for v in G[node]:
|
||
distinct_colors[v].add(color)
|
||
|
||
|
||
#: Dictionary mapping name of a strategy as a string to the strategy function.
|
||
STRATEGIES = {
|
||
'largest_first': strategy_largest_first,
|
||
'random_sequential': strategy_random_sequential,
|
||
'smallest_last': strategy_smallest_last,
|
||
'independent_set': strategy_independent_set,
|
||
'connected_sequential_bfs': strategy_connected_sequential_bfs,
|
||
'connected_sequential_dfs': strategy_connected_sequential_dfs,
|
||
'connected_sequential': strategy_connected_sequential,
|
||
'saturation_largest_first': strategy_saturation_largest_first,
|
||
'DSATUR': strategy_saturation_largest_first,
|
||
}
|
||
|
||
|
||
def greedy_color(G, strategy='largest_first', interchange=False):
|
||
"""Color a graph using various strategies of greedy graph coloring.
|
||
|
||
Attempts to color a graph using as few colors as possible, where no
|
||
neighbours of a node can have same color as the node itself. The
|
||
given strategy determines the order in which nodes are colored.
|
||
|
||
The strategies are described in [1]_, and smallest-last is based on
|
||
[2]_.
|
||
|
||
Parameters
|
||
----------
|
||
G : NetworkX graph
|
||
|
||
strategy : string or function(G, colors)
|
||
A function (or a string representing a function) that provides
|
||
the coloring strategy, by returning nodes in the ordering they
|
||
should be colored. ``G`` is the graph, and ``colors`` is a
|
||
dictionary of the currently assigned colors, keyed by nodes. The
|
||
function must return an iterable over all the nodes in ``G``.
|
||
|
||
If the strategy function is an iterator generator (that is, a
|
||
function with ``yield`` statements), keep in mind that the
|
||
``colors`` dictionary will be updated after each ``yield``, since
|
||
this function chooses colors greedily.
|
||
|
||
If ``strategy`` is a string, it must be one of the following,
|
||
each of which represents one of the built-in strategy functions.
|
||
|
||
* ``'largest_first'``
|
||
* ``'random_sequential'``
|
||
* ``'smallest_last'``
|
||
* ``'independent_set'``
|
||
* ``'connected_sequential_bfs'``
|
||
* ``'connected_sequential_dfs'``
|
||
* ``'connected_sequential'`` (alias for the previous strategy)
|
||
* ``'saturation_largest_first'``
|
||
* ``'DSATUR'`` (alias for the previous strategy)
|
||
|
||
interchange: bool
|
||
Will use the color interchange algorithm described by [3]_ if set
|
||
to ``True``.
|
||
|
||
Note that ``saturation_largest_first`` and ``independent_set``
|
||
do not work with interchange. Furthermore, if you use
|
||
interchange with your own strategy function, you cannot rely
|
||
on the values in the ``colors`` argument.
|
||
|
||
Returns
|
||
-------
|
||
A dictionary with keys representing nodes and values representing
|
||
corresponding coloring.
|
||
|
||
Examples
|
||
--------
|
||
>>> G = nx.cycle_graph(4)
|
||
>>> d = nx.coloring.greedy_color(G, strategy='largest_first')
|
||
>>> d in [{0: 0, 1: 1, 2: 0, 3: 1}, {0: 1, 1: 0, 2: 1, 3: 0}]
|
||
True
|
||
|
||
Raises
|
||
------
|
||
NetworkXPointlessConcept
|
||
If ``strategy`` is ``saturation_largest_first`` or
|
||
``independent_set`` and ``interchange`` is ``True``.
|
||
|
||
References
|
||
----------
|
||
.. [1] Adrian Kosowski, and Krzysztof Manuszewski,
|
||
Classical Coloring of Graphs, Graph Colorings, 2-19, 2004.
|
||
ISBN 0-8218-3458-4.
|
||
.. [2] David W. Matula, and Leland L. Beck, "Smallest-last
|
||
ordering and clustering and graph coloring algorithms." *J. ACM* 30,
|
||
3 (July 1983), 417–427. <https://doi.org/10.1145/2402.322385>
|
||
.. [3] Maciej M. Sysło, Marsingh Deo, Janusz S. Kowalik,
|
||
Discrete Optimization Algorithms with Pascal Programs, 415-424, 1983.
|
||
ISBN 0-486-45353-7.
|
||
|
||
"""
|
||
if len(G) == 0:
|
||
return {}
|
||
# Determine the strategy provided by the caller.
|
||
strategy = STRATEGIES.get(strategy, strategy)
|
||
if not callable(strategy):
|
||
raise nx.NetworkXError('strategy must be callable or a valid string. '
|
||
'{0} not valid.'.format(strategy))
|
||
# Perform some validation on the arguments before executing any
|
||
# strategy functions.
|
||
if interchange:
|
||
if strategy is strategy_independent_set:
|
||
msg = 'interchange cannot be used with independent_set'
|
||
raise nx.NetworkXPointlessConcept(msg)
|
||
if strategy is strategy_saturation_largest_first:
|
||
msg = ('interchange cannot be used with'
|
||
' saturation_largest_first')
|
||
raise nx.NetworkXPointlessConcept(msg)
|
||
colors = {}
|
||
nodes = strategy(G, colors)
|
||
if interchange:
|
||
return _interchange.greedy_coloring_with_interchange(G, nodes)
|
||
for u in nodes:
|
||
# Set to keep track of colors of neighbours
|
||
neighbour_colors = {colors[v] for v in G[u] if v in colors}
|
||
# Find the first unused color.
|
||
for color in itertools.count():
|
||
if color not in neighbour_colors:
|
||
break
|
||
# Assign the new color to the current node.
|
||
colors[u] = color
|
||
return colors
|