Weighted Graph Implementation and Practical Applications

Weighted Graph: Graphs with Edge Weights

Weighted Graph is a graph where edges are assigned numerical values (called weights) that represent costs, distances, times, or other metrics between vertices. Unlike unweighted graphs (where edges only indicate connectivity), weighted graphs model real-world scenarios like road networks (distance/time between cities), communication networks (bandwidth/latency), or financial transactions (costs/fees).

Weighted graphs can be undirected (weights are bidirectional) or directed (digraphs) (weights are direction-specific). Below, we’ll cover core concepts, implementations, and key algorithms for weighted graphs—with a focus on practical use cases like shortest path and minimum spanning tree (MST) problems.


I. Core Concepts & Terminology

1. Key Definitions

  • Weight: A numerical value (integer/float) associated with an edge (e.g., A-B with weight 5 means the cost from A to B is 5).
  • Weighted Path: The sum of weights of edges in a path (e.g., path A→B→C with weights 2 and 3 has a total weight of 5).
  • Shortest Path: A path between two vertices with the minimum total weight (not necessarily the fewest edges).
  • Minimum Spanning Tree (MST): A subset of edges that connects all vertices with no cycles and the minimum total edge weight (only for undirected, connected weighted graphs).
  • Negative Weight Edge: An edge with a negative weight (e.g., a rebate or credit). Requires specialized algorithms (e.g., Bellman-Ford) to handle.
  • Negative Weight Cycle: A cycle where the total weight sum is negative (e.g., A→B→A with weights -2 and -3 → total -5). Makes shortest paths undefined (infinite loops reduce the path weight indefinitely).

2. Types of Weighted Graphs

TypeDescription
Undirected Weighted GraphEdges are bidirectional; weight u-v = weight v-u (e.g., road networks with distance).
Directed Weighted Graph (Weighted Digraph)Edges are directed; weight u→v ≠ weight v→u (e.g., one-way roads with tolls, network latency).
Graph with Non-Negative WeightsAll edge weights ≥ 0 (most common; compatible with Dijkstra’s Algorithm).
Graph with Negative WeightsContains edges with weight < 0 (requires Bellman-Ford or Floyd-Warshall).

II. Weighted Graph Implementation (Python)

We’ll implement a general weighted graph (supports undirected/directed modes) using an adjacency list (efficient for sparse graphs). The implementation includes core operations and key algorithms: Dijkstra’s (shortest path, non-negative weights), Bellman-Ford (shortest path, negative weights), and Prim’s (MST, undirected).

1. WeightedGraph Class

python

运行

from collections import deque
import heapq

class WeightedGraph:
    def __init__(self, directed=False):
        self.graph = {}  # Adjacency list: {vertex: [(neighbor, weight), ...]}
        self.directed = directed  # True = weighted digraph, False = undirected weighted graph

    # -------------------------- Core Operations --------------------------
    def add_vertex(self, vertex):
        """Add a vertex (no duplicates)"""
        if vertex not in self.graph:
            self.graph[vertex] = []
            return True
        return False

    def add_edge(self, u, v, weight):
        """Add a weighted edge between u and v (directional if directed=True)"""
        if u not in self.graph or v not in self.graph:
            raise ValueError("Both vertices must exist in the graph")
        # Validate weight (allow integers/floats)
        if not isinstance(weight, (int, float)):
            raise TypeError("Weight must be a number")
        # Add edge u→v
        self.graph[u].append((v, weight))
        # Add reverse edge v→u if undirected
        if not self.directed:
            self.graph[v].append((u, weight))

    # -------------------------- Shortest Path Algorithms --------------------------
    def dijkstra(self, start_vertex):
        """
        Shortest path from start_vertex to all others (non-negative weights only)
        Returns: {vertex: shortest_distance, ...}
        """
        if start_vertex not in self.graph:
            raise ValueError("Start vertex not in graph")
        
        # Initialize distances: infinity except start_vertex (0)
        INF = float('inf')
        distance = {vertex: INF for vertex in self.graph}
        distance[start_vertex] = 0
        
        # Priority queue: (current_distance, current_vertex) → always expands shortest known path
        priority_queue = [(0, start_vertex)]
        visited = set()

        while priority_queue:
            current_dist, u = heapq.heappop(priority_queue)
            
            # Skip if vertex already processed (we found a shorter path earlier)
            if u in visited:
                continue
            visited.add(u)

            # Relax all outgoing edges from u
            for (v, weight) in self.graph[u]:
                if weight < 0:
                    raise ValueError("Dijkstra's Algorithm does not support negative weights")
                if distance[v] > distance[u] + weight:
                    distance[v] = distance[u] + weight
                    heapq.heappush(priority_queue, (distance[v], v))

        return distance

    def bellman_ford(self, start_vertex):
        """
        Shortest path from start_vertex to all others (supports negative weights, no negative cycles)
        Returns: {vertex: shortest_distance, ...} or raises error if negative cycle exists
        """
        if start_vertex not in self.graph:
            raise ValueError("Start vertex not in graph")
        
        V = len(self.graph)
        INF = float('inf')
        distance = {vertex: INF for vertex in self.graph}
        distance[start_vertex] = 0

        # Step 1: Relax all edges V-1 times (longest shortest path has V-1 edges)
        edges = self.get_edges()
        for _ in range(V - 1):
            updated = False
            for (u, v, weight) in edges:
                if distance[u] != INF and distance[v] > distance[u] + weight:
                    distance[v] = distance[u] + weight
                    updated = True
            if not updated:
                break  # No more updates → early exit

        # Step 2: Check for negative weight cycles
        for (u, v, weight) in edges:
            if distance[u] != INF and distance[v] > distance[u] + weight:
                raise ValueError("Graph contains a negative weight cycle (shortest path undefined)")

        return distance

    def floyd_warshall(self):
        """
        All-pairs shortest path (supports negative weights, no negative cycles)
        Returns: Distance matrix {u: {v: shortest_distance, ...}, ...}
        """
        vertices = list(self.graph.keys())
        V = len(vertices)
        INF = float('inf')

        # Initialize distance matrix: dist[i][j] = weight of edge i→j, else INF
        dist = {u: {v: INF for v in vertices} for u in vertices}
        for u in vertices:
            dist[u][u] = 0  # Distance from vertex to itself is 0
            for (v, weight) in self.graph[u]:
                dist[u][v] = weight

        # Step 1: Update distances using intermediate vertices
        for k in vertices:  # k = intermediate vertex
            for i in vertices:  # i = source
                for j in vertices:  # j = target
                    if dist[i][k] != INF and dist[k][j] != INF:
                        if dist[i][j] > dist[i][k] + dist[k][j]:
                            dist[i][j] = dist[i][k] + dist[k][j]

        # Step 2: Check for negative weight cycles (distance from vertex to itself < 0)
        for u in vertices:
            if dist[u][u] < 0:
                raise ValueError("Graph contains a negative weight cycle")

        return dist

    # -------------------------- Minimum Spanning Tree (MST) Algorithms --------------------------
    def prim_mst(self):
        """
        Prim's Algorithm: MST for undirected, connected weighted graphs
        Returns: List of MST edges [(u, v, weight), ...] and total MST weight
        """
        if self.directed:
            raise ValueError("Prim's Algorithm is for undirected graphs")
        if not self.is_connected():
            raise ValueError("Graph must be connected to have an MST")
        
        vertices = list(self.graph.keys())
        if not vertices:
            return [], 0
        
        # Initialize: start with first vertex
        visited = set([vertices[0]])
        mst_edges = []
        total_weight = 0

        # Priority queue: (edge_weight, u, v) → always pick the smallest edge connecting MST to non-MST
        priority_queue = []
        for (v, weight) in self.graph[vertices[0]]:
            heapq.heappush(priority_queue, (weight, vertices[0], v))

        while priority_queue and len(visited) < len(vertices):
            weight, u, v = heapq.heappop(priority_queue)
            if v not in visited:
                visited.add(v)
                mst_edges.append((u, v, weight))
                total_weight += weight

                # Add edges from v to non-MST vertices
                for (neighbor, w) in self.graph[v]:
                    if neighbor not in visited:
                        heapq.heappush(priority_queue, (w, v, neighbor))

        return mst_edges, total_weight

    # -------------------------- Helper Methods --------------------------
    def get_edges(self):
        """Return all edges as (u, v, weight) tuples"""
        edges = []
        for u in self.graph:
            for (v, weight) in self.graph[u]:
                # Avoid duplicate edges in undirected graphs (store (u, v) only if u < v)
                if not self.directed and u > v:
                    continue
                edges.append((u, v, weight))
        return edges

    def is_connected(self):
        """Check if undirected graph is connected (BFS-based)"""
        if self.directed:
            raise ValueError("is_connected() is for undirected graphs")
        if not self.graph:
            return True
        
        start_vertex = next(iter(self.graph.keys()))
        visited = set()
        queue = deque([start_vertex])
        visited.add(start_vertex)

        while queue:
            u = queue.popleft()
            for (v, _) in self.graph[u]:
                if v not in visited:
                    visited.add(v)
                    queue.append(v)

        return len(visited) == len(self.graph)

    def __str__(self):
        """String representation (adjacency list with weights)"""
        result = []
        for u in self.graph:
            neighbors = ", ".join([f"{v} (w={weight})" for (v, weight) in self.graph[u]])
            direction = "→" if self.directed else "↔"
            result.append(f"{u} {direction} [{neighbors}]")
        return "\n".join(result)

2. Test the WeightedGraph

python

运行

if __name__ == "__main__":
    # Test 1: Undirected Weighted Graph (Road Network)
    road_network = WeightedGraph(directed=False)
    cities = ["NYC", "LA", "Chicago", "Houston", "Miami"]
    for city in cities:
        road_network.add_vertex(city)
    # Add edges (city1, city2, distance in miles)
    road_network.add_edge("NYC", "Chicago", 800)
    road_network.add_edge("NYC", "Miami", 1300)
    road_network.add_edge("Chicago", "LA", 2000)
    road_network.add_edge("Chicago", "Houston", 1100)
    road_network.add_edge("Houston", "LA", 1500)
    road_network.add_edge("Houston", "Miami", 1200)

    print("=== Undirected Weighted Graph (Road Network) ===")
    print(road_network)

    # Dijkstra's Algorithm (shortest path from NYC)
    print("\n=== Dijkstra's Shortest Path from NYC ===")
    shortest_paths = road_network.dijkstra("NYC")
    for city, dist in shortest_paths.items():
        print(f"NYC → {city}: {dist} miles")  # Output: NYC→LA: 2800 (NYC→Chicago→LA)

    # Prim's MST (minimum cost to connect all cities)
    print("\n=== Prim's MST (Road Network) ===")
    mst_edges, total_weight = road_network.prim_mst()
    print("MST Edges:", mst_edges)
    print("Total MST Weight:", total_weight)  # Output: 800+1100+1200+1500 = 4600

    # Test 2: Directed Weighted Graph (Network Latency)
    network = WeightedGraph(directed=True)
    nodes = ["A", "B", "C", "D"]
    for node in nodes:
        network.add_vertex(node)
    # Add edges (node1→node2, latency in ms)
    network.add_edge("A", "B", 5)
    network.add_edge("A", "C", 10)
    network.add_edge("B", "C", 2)
    network.add_edge("B", "D", 15)
    network.add_edge("C", "D", 3)
    network.add_edge("D", "C", -1)  # Negative weight (no cycle → Bellman-Ford works)

    print("\n=== Directed Weighted Graph (Network Latency) ===")
    print(network)

    # Bellman-Ford Algorithm (supports negative weights)
    print("\n=== Bellman-Ford Shortest Path from A ===")
    try:
        bf_paths = network.bellman_ford("A")
        for node, latency in bf_paths.items():
            print(f"A→{node}: {latency} ms")  # Output: A→D: 5+2+3=10 ms
    except ValueError as e:
        print(e)

    # Floyd-Warshall (All-Pairs Shortest Path)
    print("\n=== Floyd-Warshall All-Pairs Shortest Path ===")
    try:
        fw_matrix = network.floyd_warshall()
        for u in fw_matrix:
            for v in fw_matrix[u]:
                print(f"{u}→{v}: {fw_matrix[u][v]} ms", end=" | ")
            print()
    except ValueError as e:
        print(e)


III. Key Algorithms for Weighted Graphs

Weighted graphs rely on specialized algorithms to solve critical problems like shortest paths and MSTs. Below is a breakdown of the most important ones:

1. Shortest Path Algorithms

AlgorithmUse CaseTime ComplexitySpace ComplexityKey Notes
Dijkstra’sSingle-source, non-negative weightsO((V + E) log V)O(V)Fastest for non-negative weights (uses priority queue).
Bellman-FordSingle-source, negative weights (no cycles)O(V×E)O(V)Detects negative weight cycles; slower than Dijkstra’s but more flexible.
Floyd-WarshallAll-pairs shortest pathsO(V³)O(V²)Simple to implement; good for small graphs (V < 1000).
Shortest Path in DAGSingle-source, DAG (weighted)O(V + E)O(V)Uses topological sort; fastest for acyclic graphs.

2. Minimum Spanning Tree (MST) Algorithms

AlgorithmUse CaseTime ComplexitySpace ComplexityKey Notes
Prim’sUndirected, connected, weighted graphsO((V + E) log V)O(V)Efficient for dense graphs (many edges); uses priority queue.
Kruskal’sUndirected, connected, weighted graphsO(E log E)O(V + E)Efficient for sparse graphs (few edges); uses Union-Find to avoid cycles.

IV. Practical Applications of Weighted Graphs

Weighted graphs are foundational for modeling real-world systems where “cost” or “distance” matters:

  1. GPS Navigation: Use Dijkstra’s or A* Algorithm to find the shortest/fastest route between two locations (weights = distance/time/tolls).
  2. Network Routing: Optimize data transmission by minimizing latency (weight = delay) or maximizing bandwidth (weight = 1/bandwidth).
  3. Logistics & Supply Chain: Find the cheapest route to transport goods (weights = fuel costs, distance, or time).
  4. Telecommunication Networks: Design minimum-cost fiber-optic networks (MST algorithms) to connect cities.
  5. Finance: Model portfolio optimization (weights = risk/return) or detect arbitrage (negative weight cycles in currency exchange graphs).
  6. Machine Learning: Graph neural networks (GNNs) use weighted edges to represent feature importance between nodes.
  7. Project Management: Critical Path Analysis (CPA) uses weighted DAGs to find the longest path (weights = task durations) and identify project bottlenecks.

V. Key Considerations

  1. Weight Sign: Choose the right algorithm based on weight signs:
    • Non-negative weights → Dijkstra’s (fastest).
    • Negative weights (no cycles) → Bellman-Ford or Floyd-Warshall.
    • Negative weight cycles → Undefined shortest paths (detect with Bellman-Ford/Floyd-Warshall).
  2. Graph Density:
    • Sparse graphs (few edges) → Adjacency list + Dijkstra’s/Prim’s (O((V+E) log V)).
    • Dense graphs (many edges) → Adjacency matrix + Floyd-Warshall (O(V³)) or Prim’s with array (O(V²)).
  3. Directed vs. Undirected:
    • Undirected graphs → MST algorithms (Prim’s/Kruskal’s) apply directly.
    • Directed graphs → Use “minimum spanning arborescence” (Edmonds’ Algorithm) for MST-like problems.

Summary

Key choice factors: Weight sign, graph density, and directed/undirected structure.

Weighted Graph assigns numerical weights to edges, modeling cost/distance between vertices.

Core types: Undirected/directed, non-negative/negative weights, with/without negative cycles.

Critical algorithms:

Shortest Path: Dijkstra’s (non-negative), Bellman-Ford (negative, no cycles), Floyd-Warshall (all-pairs).

MST: Prim’s (dense graphs), Kruskal’s (sparse graphs) → for undirected, connected graphs.

Practical applications: GPS navigation, network routing, logistics, telecommunications, and finance.



了解 Ruigu Electronic 的更多信息

订阅后即可通过电子邮件收到最新文章。

Posted in

Leave a comment