mirror of
https://github.com/TheAlgorithms/Python.git
synced 2026-09-29 05:53:07 +08:00
* Initial Commit: Add algorithms to determine peripheral and farthest nodes in a weighted graph
- Implemented Floyd-Warshall algorithm to calculate all-pairs shortest paths,
supporting positive edge weights for weighted graphs.
- Added functions to identify peripheral and far nodes:
- `find_peripheral_node`: Determines the node with maximal eccentricity.
- `find_far_node`: Finds the node with maximal farness, summing shortest-path distances.
- Included error handling for non-positive edge weights, with appropriate ValueError raising.
- Developed extensive test cases using doctests:
- Covered single-node, fully connected, sparse, cyclic, directed acyclic, and
disconnected graphs.
- Validated handling of edge cases including zero and negative weights.
- Ensured code meets PEP 8 standards and passed all lint checks with `ruff`.
- Provided detailed module-level docstring with explanations of algorithms,
complexity, and example applications.
This commit provides foundational functionality for graph-theoretical analysis
and sets up for future expansions or integrations.
* Refactor main function and update docstring formatting
* updating DIRECTORY.md
---------
Co-authored-by: Christian Clauss <cclauss@me.com>
Co-authored-by: cclauss <cclauss@users.noreply.github.com>
394 lines
12 KiB
Python
394 lines
12 KiB
Python
"""
|
|
Graph Peripheral and Farness Algorithms for Determining Peripheral and Farthest Nodes
|
|
in a Graph.
|
|
|
|
This module provides functions to compute the peripheral and farthest nodes in a
|
|
weighted graph based on graph-theoretical distance measures. The peripheral node
|
|
maximizes the maximum shortest-path distance to all other reachable nodes
|
|
(eccentricity), while the far node maximizes the sum of shortest-path distances to
|
|
all other reachable nodes (farness).
|
|
|
|
Problem Description:
|
|
Given a weighted graph G = (V, E), where V is the set of vertices, and E is the set
|
|
of edges with positive weights representing distances between nodes, determine:
|
|
|
|
- Peripheral Node: The node with maximal eccentricity. The eccentricity of a node v
|
|
is defined as the greatest distance between v and any other node reachable from v.
|
|
|
|
- Far Node: The node with maximal farness. Farness of a node v is the sum of the
|
|
shortest-path distances from v to all other reachable nodes.
|
|
|
|
Algorithms Implemented:
|
|
- Floyd-Warshall Algorithm for All-Pairs Shortest Paths.
|
|
- Calculation of Eccentricity and Farness for identifying Peripheral and Far nodes.
|
|
|
|
Algorithm Descriptions:
|
|
|
|
Floyd-Warshall Algorithm (Pseudo-code):
|
|
---------------------------------------
|
|
for k from 1 to N:
|
|
for i from 1 to N:
|
|
for j from 1 to N:
|
|
if distance[i][j] > distance[i][k] + distance[k][j]:
|
|
distance[i][j] = distance[i][k] + distance[k][j]
|
|
|
|
Peripheral and Far Node Calculation:
|
|
------------------------------------
|
|
For each node i:
|
|
- Eccentricity[i] = maximum distance from node i to any other reachable node.
|
|
- Farness[i] = sum of distances from node i to all reachable nodes.
|
|
|
|
Select:
|
|
- Peripheral Node: node with maximal eccentricity.
|
|
- Far Node: node with maximal farness.
|
|
|
|
References:
|
|
- Floyd-Warshall Algorithm: https://en.wikipedia.org/wiki/Floyd-Warshall_algorithm
|
|
- Eccentricity and Farness: https://en.wikipedia.org/wiki/Distance_(graph_theory)
|
|
|
|
Example Application:
|
|
These algorithms can be applied to network analysis, such as identifying the most
|
|
distant nodes within a network, analyzing communication delays, or planning
|
|
infrastructure at the boundaries of a network.
|
|
"""
|
|
|
|
import numpy as np
|
|
|
|
|
|
def initialize_distance_matrix(
|
|
graph: dict[int, list[tuple[int, float]]], number_of_nodes: int
|
|
) -> np.ndarray:
|
|
"""Initialize the distance matrix and validate edge weights.
|
|
|
|
Args:
|
|
graph: The graph represented as an adjacency list.
|
|
number_of_nodes: The total number of nodes in the graph.
|
|
|
|
Returns:
|
|
A numpy.ndarray representing the initialized distance matrix.
|
|
|
|
Raises:
|
|
ValueError: If any edge has a non-positive weight.
|
|
"""
|
|
distance_matrix: np.ndarray = np.full((number_of_nodes, number_of_nodes), np.inf)
|
|
np.fill_diagonal(distance_matrix, 0)
|
|
|
|
for node_index, edges in graph.items():
|
|
for neighbor_index, edge_weight in edges:
|
|
if edge_weight <= 0:
|
|
error_message: str = (
|
|
f"Edge weight must be positive. Found {edge_weight} between "
|
|
f"nodes {node_index} and {neighbor_index}."
|
|
)
|
|
raise ValueError(error_message)
|
|
distance_matrix[node_index, neighbor_index] = edge_weight
|
|
|
|
return distance_matrix
|
|
|
|
|
|
def floyd_warshall_algorithm(graph: dict[int, list[tuple[int, float]]]) -> np.ndarray:
|
|
"""Compute all-pairs shortest paths using the Floyd-Warshall algorithm.
|
|
|
|
Floyd-Warshall Complexity:
|
|
--------------------------
|
|
Time Complexity: O(N^3), where N is the number of nodes.
|
|
Space Complexity: O(N^2), for storing the distance matrix.
|
|
|
|
Args:
|
|
graph: The graph represented as an adjacency list.
|
|
|
|
Returns:
|
|
The distance matrix with the shortest paths between all pairs of nodes.
|
|
"""
|
|
number_of_nodes: int = len(graph)
|
|
distance_matrix: np.ndarray = initialize_distance_matrix(graph, number_of_nodes)
|
|
|
|
for k in range(number_of_nodes):
|
|
# Use broadcasting to update the distance matrix in place
|
|
distance_matrix[:] = np.minimum(
|
|
distance_matrix,
|
|
distance_matrix[:, k][:, np.newaxis] + distance_matrix[k, :],
|
|
)
|
|
|
|
return distance_matrix
|
|
|
|
|
|
def get_reachable_distances(distances: np.ndarray) -> np.ndarray:
|
|
"""Filter reachable distances, excluding infinite values (unreachable nodes).
|
|
|
|
Args:
|
|
distances: Array of shortest-path distances from a specific node.
|
|
|
|
Returns:
|
|
An array of distances to reachable nodes only (finite values).
|
|
"""
|
|
return distances[np.isfinite(distances) & (distances != 0)]
|
|
|
|
|
|
def find_peripheral_node(
|
|
eccentricities: list[tuple[int, float]],
|
|
) -> tuple[int, float]:
|
|
"""Identify the node with maximal eccentricity among reachable nodes.
|
|
|
|
Args:
|
|
eccentricities: List of tuples (node index, eccentricity).
|
|
|
|
Returns:
|
|
The node with maximal eccentricity and its value. Returns (-1, inf) if
|
|
no valid nodes are found.
|
|
"""
|
|
return max(
|
|
eccentricities,
|
|
key=lambda node_eccentricity: (
|
|
node_eccentricity[1], # Prioritize highest eccentricity
|
|
-node_eccentricity[0], # For equal values, prefer nodes with higher index
|
|
),
|
|
default=(-1, float("inf")),
|
|
)
|
|
|
|
|
|
def find_far_node(farnesses: list[tuple[int, float]]) -> tuple[int, float]:
|
|
"""Identify the node with maximal farness among reachable nodes.
|
|
|
|
Args:
|
|
farnesses: List of tuples (node index, farness).
|
|
|
|
Returns:
|
|
The node with maximal farness and its value. Returns (-1, inf) if
|
|
no valid nodes are found.
|
|
"""
|
|
return max(
|
|
farnesses,
|
|
key=lambda node_farness: (
|
|
node_farness[1], # Prioritize highest farness
|
|
-node_farness[0], # For equal values, prefer nodes with higher index
|
|
),
|
|
default=(-1, float("inf")),
|
|
)
|
|
|
|
|
|
def find_peripheral_and_far_node(
|
|
distance_matrix: np.ndarray,
|
|
) -> tuple[tuple[int, float], tuple[int, float]]:
|
|
"""Determine the peripheral and far nodes based on shortest-path distances.
|
|
|
|
For each node, calculates its eccentricity and farness (sum of distances to all
|
|
reachable nodes). Identifies the peripheral node (maximal eccentricity) and
|
|
farthest node (maximal farness).
|
|
|
|
Args:
|
|
distance_matrix: A numpy.ndarray representing shortest-path distances
|
|
between all pairs of nodes.
|
|
|
|
Returns:
|
|
A tuple containing:
|
|
- peripheral_node: A tuple (node index, eccentricity) for the node with
|
|
maximal eccentricity.
|
|
- far_node: A tuple (node index, farness) for the node with maximal
|
|
farness (sum of shortest-path distances).
|
|
"""
|
|
num_nodes: int = len(distance_matrix)
|
|
|
|
# Handle single-node graphs
|
|
if num_nodes == 1:
|
|
return (0, 0.0), (0, 0.0)
|
|
|
|
eccentricities: list[tuple[int, float]] = []
|
|
farnesses: list[tuple[int, float]] = []
|
|
all_disconnected = True # Assume all are disconnected until proven otherwise
|
|
|
|
for i in range(num_nodes):
|
|
reachable_distances = get_reachable_distances(distance_matrix[i])
|
|
|
|
if reachable_distances.size == 0:
|
|
# Fully disconnected node
|
|
eccentricity = float("inf")
|
|
farness = float("inf")
|
|
else:
|
|
all_disconnected = False # If any node has reachable nodes, we update this
|
|
eccentricity = float(np.max(reachable_distances))
|
|
farness = float(np.sum(reachable_distances))
|
|
|
|
eccentricities.append((i, eccentricity))
|
|
farnesses.append((i, farness))
|
|
|
|
# If all nodes are disconnected, we return (-1, inf)
|
|
if all_disconnected:
|
|
return (-1, float("inf")), (-1, float("inf"))
|
|
|
|
peripheral_node = find_peripheral_node(eccentricities)
|
|
far_node = find_far_node(farnesses)
|
|
|
|
return peripheral_node, far_node
|
|
|
|
|
|
# Test cases included as doctests
|
|
def test_single_node() -> None:
|
|
"""
|
|
Test a graph with a single node.
|
|
|
|
>>> graph = {0: []}
|
|
>>> distance_matrix = floyd_warshall_algorithm(graph)
|
|
>>> peripheral_node, far_node = find_peripheral_and_far_node(distance_matrix)
|
|
>>> peripheral_node
|
|
(0, 0.0)
|
|
>>> far_node
|
|
(0, 0.0)
|
|
"""
|
|
|
|
|
|
def test_two_nodes_positive_weight() -> None:
|
|
"""
|
|
Test a graph with two nodes connected by a positive weight.
|
|
|
|
>>> graph = {0: [(1, 5.0)], 1: [(0, 5.0)]}
|
|
>>> distance_matrix = floyd_warshall_algorithm(graph)
|
|
>>> peripheral_node, far_node = find_peripheral_and_far_node(distance_matrix)
|
|
>>> peripheral_node
|
|
(0, 5.0)
|
|
>>> far_node
|
|
(0, 5.0)
|
|
"""
|
|
|
|
|
|
def test_fully_connected_graph() -> None:
|
|
"""
|
|
Test a fully connected graph.
|
|
|
|
>>> graph = {
|
|
... 0: [(1, 1.0), (2, 1.0)],
|
|
... 1: [(0, 1.0), (2, 1.0)],
|
|
... 2: [(0, 1.0), (1, 1.0)],
|
|
... }
|
|
>>> distance_matrix = floyd_warshall_algorithm(graph)
|
|
>>> peripheral_node, far_node = find_peripheral_and_far_node(distance_matrix)
|
|
>>> peripheral_node
|
|
(0, 1.0)
|
|
>>> far_node
|
|
(0, 2.0)
|
|
"""
|
|
|
|
|
|
def test_graph_with_zero_weight() -> None:
|
|
"""
|
|
Test a graph with zero weight, which should raise a ValueError.
|
|
|
|
>>> graph = {0: [(1, 0.0)], 1: []}
|
|
>>> floyd_warshall_algorithm(graph)
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: Edge weight must be positive. Found 0.0 between nodes 0 and 1.
|
|
"""
|
|
|
|
|
|
def test_graph_with_negative_weight() -> None:
|
|
"""
|
|
Test a graph with negative weight, which should raise a ValueError.
|
|
|
|
>>> graph = {0: [(1, -2.0)], 1: []}
|
|
>>> floyd_warshall_algorithm(graph)
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: Edge weight must be positive. Found -2.0 between nodes 0 and 1.
|
|
"""
|
|
|
|
|
|
def test_sparse_graph() -> None:
|
|
"""
|
|
Test a larger sparse graph.
|
|
|
|
>>> graph = {
|
|
... 0: [(1, 2.0)],
|
|
... 1: [(2, 3.0)],
|
|
... 2: [(3, 4.0)],
|
|
... 3: [(4, 5.0)],
|
|
... 4: []
|
|
... }
|
|
>>> distance_matrix = floyd_warshall_algorithm(graph)
|
|
>>> peripheral_node, far_node = find_peripheral_and_far_node(distance_matrix)
|
|
>>> peripheral_node
|
|
(4, inf)
|
|
>>> far_node
|
|
(4, inf)
|
|
"""
|
|
|
|
|
|
def test_cyclic_graph() -> None:
|
|
"""
|
|
Test a cyclic graph where there is a cycle between nodes.
|
|
|
|
>>> graph = {
|
|
... 0: [(1, 1.0)],
|
|
... 1: [(2, 1.0)],
|
|
... 2: [(0, 1.0)]
|
|
... }
|
|
>>> distance_matrix = floyd_warshall_algorithm(graph)
|
|
>>> peripheral_node, far_node = find_peripheral_and_far_node(distance_matrix)
|
|
>>> peripheral_node
|
|
(0, 2.0)
|
|
>>> far_node
|
|
(0, 3.0)
|
|
"""
|
|
|
|
|
|
def test_directed_acyclic_graph() -> None:
|
|
"""
|
|
Test a directed acyclic graph (DAG).
|
|
|
|
>>> graph = {
|
|
... 0: [(1, 1.0), (2, 2.0)],
|
|
... 1: [(3, 3.0)],
|
|
... 2: [(3, 1.0)],
|
|
... 3: []
|
|
... }
|
|
>>> distance_matrix = floyd_warshall_algorithm(graph)
|
|
>>> peripheral_node, far_node = find_peripheral_and_far_node(distance_matrix)
|
|
>>> peripheral_node
|
|
(3, inf)
|
|
>>> far_node
|
|
(3, inf)
|
|
"""
|
|
|
|
|
|
def test_disconnected_graph() -> None:
|
|
"""
|
|
Test a disconnected graph with nodes that cannot reach each other.
|
|
|
|
>>> graph = {
|
|
... 0: [],
|
|
... 1: [],
|
|
... 2: []
|
|
... }
|
|
>>> distance_matrix = floyd_warshall_algorithm(graph)
|
|
>>> peripheral_node, far_node = find_peripheral_and_far_node(distance_matrix)
|
|
>>> peripheral_node
|
|
(-1, inf)
|
|
>>> far_node
|
|
(-1, inf)
|
|
"""
|
|
|
|
|
|
def test_large_fully_connected_graph() -> None:
|
|
"""
|
|
Test a larger fully connected graph with random weights.
|
|
|
|
>>> import random
|
|
>>> random.seed(42)
|
|
>>> number_of_nodes = 10
|
|
>>> graph = {i: [(j, random.uniform(1, 10)) for j in
|
|
... range(number_of_nodes) if i != j]
|
|
... for i in range(number_of_nodes)}
|
|
>>> distance_matrix = floyd_warshall_algorithm(graph)
|
|
>>> peripheral_node, far_node = find_peripheral_and_far_node(distance_matrix)
|
|
>>> peripheral_node[0] is not None # Ensure it found a peripheral node
|
|
True
|
|
>>> far_node[0] is not None # Ensure it found a far node
|
|
True
|
|
"""
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import doctest
|
|
|
|
doctest.testmod()
|