跳转至

代码实战项目10:图算法实现

项目概述

图算法是计算机科学的重要组成部分,在社交网络、路由协议、资源调度等领域有广泛应用。本项目将使用 Python 实现常见的图算法,帮助深入理解图论和算法设计。

学习目标

  • 理解图的基本概念和表示方法
  • 掌握图的遍历算法(DFS、BFS)
  • 学会最短路径算法(Dijkstra、Floyd-Warshall)
  • 理解最小生成树算法(Kruskal、Prim)
  • 掌握拓扑排序和关键路径算法

图的基本概念

图的定义

图 G = (V, E) 由顶点集 V 和边集 E 组成。

图的类型

  1. 有向图:边有方向
  2. 无向图:边没有方向
  3. 带权图:边有权重
  4. 无权图:边没有权重

图的表示

  1. 邻接矩阵:二维数组表示
  2. 邻接表:链表或字典表示

代码实现

1. 图类定义(graph.py)

from collections import deque
import heapq

class Graph:
    """图类"""

    def __init__(self, directed=False):
        """
        初始化图

        参数:
            directed: 是否为有向图
        """
        self.directed = directed
        self.vertices = {}  # 顶点集合
        self.adj_list = {}  # 邻接表

    def add_vertex(self, vertex):
        """
        添加顶点

        参数:
            vertex: 顶点标识
        """
        if vertex not in self.vertices:
            self.vertices[vertex] = True
            self.adj_list[vertex] = []

    def add_edge(self, from_vertex, to_vertex, weight=1):
        """
        添加边

        参数:
            from_vertex: 起始顶点
            to_vertex: 目标顶点
            weight: 边的权重
        """
        if from_vertex not in self.vertices:
            self.add_vertex(from_vertex)
        if to_vertex not in self.vertices:
            self.add_vertex(to_vertex)

        self.adj_list[from_vertex].append((to_vertex, weight))

        if not self.directed:
            self.adj_list[to_vertex].append((from_vertex, weight))

    def get_vertices(self):
        """获取所有顶点"""
        return list(self.vertices.keys())

    def get_edges(self):
        """获取所有边"""
        edges = []
        for vertex in self.adj_list:
            for neighbor, weight in self.adj_list[vertex]:
                if self.directed or (neighbor, vertex, weight) not in edges:
                    edges.append((vertex, neighbor, weight))
        return edges

    def __str__(self):
        """字符串表示"""
        result = ""
        for vertex in self.adj_list:
            neighbors = [f"{n}({w})" for n, w in self.adj_list[vertex]]
            result += f"{vertex} -> {', '.join(neighbors)}\n"
        return result


class DirectedGraph(Graph):
    """有向图类"""

    def __init__(self):
        super().__init__(directed=True)


class WeightedGraph(Graph):
    """带权图类"""

    def __init__(self, directed=False):
        super().__init__(directed=directed)

2. 深度优先搜索(DFS)

def dfs(graph, start, visited=None):
    """
    深度优先搜索

    参数:
        graph: 图对象
        start: 起始顶点
        visited: 已访问集合

    返回:
        list: 遍历顺序
    """
    if visited is None:
        visited = set()

    visited.add(start)
    result = [start]

    for neighbor, _ in graph.adj_list.get(start, []):
        if neighbor not in visited:
            result.extend(dfs(graph, neighbor, visited))

    return result


def dfs_iterative(graph, start):
    """
    深度优先搜索(迭代版本)

    参数:
        graph: 图对象
        start: 起始顶点

    返回:
        list: 遍历顺序
    """
    visited = set()
    stack = [start]
    result = []

    while stack:
        vertex = stack.pop()
        if vertex not in visited:
            visited.add(vertex)
            result.append(vertex)

            # 将邻居按相反顺序压入栈,保证正确的遍历顺序
            neighbors = [n for n, _ in graph.adj_list.get(vertex, [])]
            stack.extend(reversed(neighbors))

    return result


def dfs_paths(graph, start, goal, path=None):
    """
    查找从 start 到 goal 的所有路径(DFS)

    参数:
        graph: 图对象
        start: 起始顶点
        goal: 目标顶点
        path: 当前路径

    返回:
        list: 所有路径
    """
    if path is None:
        path = []

    path = path + [start]

    if start == goal:
        return [path]

    if start not in graph.adj_list:
        return []

    paths = []
    for vertex, _ in graph.adj_list[start]:
        if vertex not in path:  # 避免循环
            new_paths = dfs_paths(graph, vertex, goal, path)
            paths.extend(new_paths)

    return paths


def shortest_path_dfs(graph, start, goal):
    """
    查找最短路径(DFS版本)

    参数:
        graph: 图对象
        start: 起始顶点
        goal: 目标顶点

    返回:
        list: 最短路径,不存在返回 None
    """
    paths = dfs_paths(graph, start, goal)
    if not paths:
        return None
    return min(paths, key=len)

3. 广度优先搜索(BFS)

def bfs(graph, start):
    """
    广度优先搜索

    参数:
        graph: 图对象
        start: 起始顶点

    返回:
        list: 遍历顺序
    """
    visited = set([start])
    queue = deque([start])
    result = []

    while queue:
        vertex = queue.popleft()
        result.append(vertex)

        for neighbor, _ in graph.adj_list.get(vertex, []):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

    return result


def bfs_shortest_path(graph, start, goal):
    """
    查找最短路径(BFS)

    参数:
        graph: 图对象
        start: 起始顶点
        goal: 目标顶点

    返回:
        list: 最短路径,不存在返回 None
    """
    if start == goal:
        return [start]

    visited = {start}
    queue = deque([(start, [start])])

    while queue:
        vertex, path = queue.popleft()

        for neighbor, _ in graph.adj_list.get(vertex, []):
            if neighbor == goal:
                return path + [neighbor]

            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, path + [neighbor]))

    return None


def bfs_distance(graph, start, goal):
    """
    计算两个顶点之间的最短距离(BFS)

    参数:
        graph: 图对象
        start: 起始顶点
        goal: 目标顶点

    返回:
        int: 最短距离,不可达返回 -1
    """
    if start == goal:
        return 0

    visited = {start}
    queue = deque([(start, 0)])

    while queue:
        vertex, distance = queue.popleft()

        for neighbor, _ in graph.adj_list.get(vertex, []):
            if neighbor == goal:
                return distance + 1

            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, distance + 1))

    return -1

4. Dijkstra 最短路径算法

def dijkstra(graph, start):
    """
    Dijkstra 最短路径算法

    参数:
        graph: 带权图对象
        start: 起始顶点

    返回:
        dict: {顶点: (最短距离, 前驱顶点)}
    """
    distances = {vertex: float('infinity') for vertex in graph.vertices}
    previous = {vertex: None for vertex in graph.vertices}
    distances[start] = 0

    # 优先队列:(距离, 顶点)
    pq = [(0, start)]

    while pq:
        current_distance, current_vertex = heapq.heappop(pq)

        # 如果找到更长的路径,跳过
        if current_distance > distances[current_vertex]:
            continue

        # 遍历邻居
        for neighbor, weight in graph.adj_list.get(current_vertex, []):
            distance = current_distance + weight

            # 如果找到更短的路径
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                previous[neighbor] = current_vertex
                heapq.heappush(pq, (distance, neighbor))

    return {v: (distances[v], previous[v]) for v in graph.vertices}


def dijkstra_path(graph, start, goal):
    """
    使用 Dijkstra 算法查找最短路径

    参数:
        graph: 带权图对象
        start: 起始顶点
        goal: 目标顶点

    返回:
        tuple: (路径, 总距离)
    """
    if start not in graph.vertices or goal not in graph.vertices:
        return None, float('infinity')

    if start == goal:
        return [start], 0

    # 运行 Dijkstra
    result = dijkstra(graph, start)
    distances = {v: result[v][0] for v in result}
    previous = {v: result[v][1] for v in result}

    # 重建路径
    if distances[goal] == float('infinity'):
        return None, float('infinity')

    path = []
    current = goal
    while current is not None:
        path.append(current)
        current = previous[current]

    path.reverse()
    return path, distances[goal]

5. Floyd-Warshall 算法

def floyd_warshall(graph):
    """
    Floyd-Warshall 全源最短路径算法

    参数:
        graph: 带权图对象

    返回:
        tuple: (距离矩阵, 路径矩阵)
    """
    vertices = graph.get_vertices()
    n = len(vertices)
    vertex_index = {v: i for i, v in enumerate(vertices)}

    # 初始化距离矩阵和路径矩阵
    dist = [[float('infinity')] * n for _ in range(n)]
    next_node = [[None] * n for _ in range(n)]

    for i in range(n):
        dist[i][i] = 0

    for from_v, to_v, weight in graph.get_edges():
        i, j = vertex_index[from_v], vertex_index[to_v]
        dist[i][j] = weight
        next_node[i][j] = to_v

    # Floyd-Warshall 算法
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
                    next_node[i][j] = next_node[i][k]

    return dist, next_node


def floyd_warshall_path(graph, start, goal):
    """
    使用 Floyd-Warshall 算法查找最短路径

    参数:
        graph: 带权图对象
        start: 起始顶点
        goal: 目标顶点

    返回:
        tuple: (路径, 总距离)
    """
    vertices = graph.get_vertices()
    if start not in vertices or goal not in vertices:
        return None, float('infinity')

    dist, next_node = floyd_warshall(graph)
    vertex_index = {v: i for i, v in enumerate(vertices)}

    i, j = vertex_index[start], vertex_index[goal]

    if dist[i][j] == float('infinity'):
        return None, float('infinity')

    # 重建路径
    path = []
    current = start
    while current != goal:
        path.append(current)
        current_i = vertex_index[current]
        current = next_node[current_i][j]
        if current is None:
            return None, float('infinity')

    path.append(goal)
    return path, dist[i][j]

6. 最小生成树算法

Kruskal 算法

class UnionFind:
    """并查集数据结构"""

    def __init__(self, elements):
        self.parent = {e: e for e in elements}
        self.rank = {e: 0 for e in elements}

    def find(self, element):
        """查找根节点"""
        if self.parent[element] != element:
            self.parent[element] = self.find(self.parent[element])
        return self.parent[element]

    def union(self, element1, element2):
        """合并两个集合"""
        root1 = self.find(element1)
        root2 = self.find(element2)

        if root1 != root2:
            if self.rank[root1] < self.rank[root2]:
                self.parent[root1] = root2
            elif self.rank[root1] > self.rank[root2]:
                self.parent[root2] = root1
            else:
                self.parent[root2] = root1
                self.rank[root1] += 1
            return True
        return False


def kruskal(graph):
    """
    Kruskal 最小生成树算法

    参数:
        graph: 带权图对象

    返回:
        tuple: (边列表, 总权重)
    """
    # 获取所有边并按权重排序
    edges = graph.get_edges()
    edges.sort(key=lambda x: x[2])

    # 初始化并查集
    uf = UnionFind(graph.get_vertices())

    mst_edges = []
    total_weight = 0

    for edge in edges:
        from_v, to_v, weight = edge

        # 如果不在同一集合,添加边
        if uf.union(from_v, to_v):
            mst_edges.append(edge)
            total_weight += weight

            # 如果已经找到 n-1 条边,结束
            if len(mst_edges) == len(graph.vertices) - 1:
                break

    return mst_edges, total_weight

Prim 算法

def prim(graph, start=None):
    """
    Prim 最小生成树算法

    参数:
        graph: 带权图对象
        start: 起始顶点(可选)

    返回:
        tuple: (边列表, 总权重)
    """
    vertices = graph.get_vertices()
    if not vertices:
        return [], 0

    # 选择起始顶点
    if start is None:
        start = vertices[0]

    # 初始化
    visited = set([start])
    mst_edges = []
    total_weight = 0

    # 优先队列:(权重, from_vertex, to_vertex)
    edges = []

    # 添加起始顶点的所有边
    for neighbor, weight in graph.adj_list.get(start, []):
        heapq.heappush(edges, (weight, start, neighbor))

    while edges and len(visited) < len(vertices):
        weight, from_v, to_v = heapq.heappop(edges)

        if to_v in visited:
            continue

        # 添加边
        visited.add(to_v)
        mst_edges.append((from_v, to_v, weight))
        total_weight += weight

        # 添加新顶点的所有边
        for neighbor, w in graph.adj_list.get(to_v, []):
            if neighbor not in visited:
                heapq.heappush(edges, (w, to_v, neighbor))

    return mst_edges, total_weight

7. 拓扑排序

def topological_sort(graph):
    """
    拓扑排序(Kahn 算法)

    参数:
        graph: 有向图对象

    返回:
        list: 拓扑排序结果,有环返回 None
    """
    if not graph.directed:
        raise ValueError("拓扑排序只适用于有向图")

    # 计算入度
    in_degree = {v: 0 for v in graph.vertices}
    for vertex in graph.adj_list:
        for neighbor, _ in graph.adj_list[vertex]:
            in_degree[neighbor] += 1

    # 找到所有入度为0的顶点
    queue = deque([v for v in graph.vertices if in_degree[v] == 0])
    result = []

    while queue:
        vertex = queue.popleft()
        result.append(vertex)

        # 减少邻居的入度
        for neighbor, _ in graph.adj_list.get(vertex, []):
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    # 检查是否有环
    if len(result) != len(graph.vertices):
        return None  # 有环

    return result


def topological_sort_dfs(graph):
    """
    拓扑排序(DFS 算法)

    参数:
        graph: 有向图对象

    返回:
        list: 拓扑排序结果,有环返回 None
    """
    if not graph.directed:
        raise ValueError("拓扑排序只适用于有向图")

    visited = set()
    temp_visited = set()
    result = []

    def dfs(vertex):
        if vertex in temp_visited:
            return False  # 有环
        if vertex in visited:
            return True

        temp_visited.add(vertex)

        for neighbor, _ in graph.adj_list.get(vertex, []):
            if not dfs(neighbor):
                return False

        temp_visited.remove(vertex)
        visited.add(vertex)
        result.append(vertex)
        return True

    for vertex in graph.vertices:
        if vertex not in visited:
            if not dfs(vertex):
                return None  # 有环

    result.reverse()
    return result

8. 测试和示例

def test_graph_algorithms():
    """测试图算法"""

    print("="*60)
    print("图算法测试")
    print("="*60)

    # 创建无向图
    print("\n1. 创建无向图")
    graph = Graph()
    edges = [
        ('A', 'B'), ('A', 'C'), ('B', 'D'),
        ('C', 'D'), ('C', 'E'), ('D', 'E')
    ]
    for u, v in edges:
        graph.add_edge(u, v)

    print(graph)

    # DFS
    print("\n2. 深度优先搜索 (DFS)")
    dfs_result = dfs(graph, 'A')
    print(f"DFS 遍历顺序: {dfs_result}")

    # BFS
    print("\n3. 广度优先搜索 (BFS)")
    bfs_result = bfs(graph, 'A')
    print(f"BFS 遍历顺序: {bfs_result}")

    # 最短路径(无权)
    print("\n4. 最短路径(无权图)")
    path = bfs_shortest_path(graph, 'A', 'E')
    print(f"A 到 E 的最短路径: {path}")

    # 创建带权图
    print("\n5. 创建带权图")
    weighted_graph = WeightedGraph()
    weighted_edges = [
        ('A', 'B', 4), ('A', 'C', 2), ('B', 'C', 1),
        ('B', 'D', 5), ('C', 'D', 8), ('C', 'E', 10),
        ('D', 'E', 2)
    ]
    for u, v, w in weighted_edges:
        weighted_graph.add_edge(u, v, w)

    print(weighted_graph)

    # Dijkstra
    print("\n6. Dijkstra 最短路径")
    dijkstra_result = dijkstra(weighted_graph, 'A')
    for v in dijkstra_result:
        dist, prev = dijkstra_result[v]
        print(f"A -> {v}: 距离 = {dist}, 前驱 = {prev}")

    path, distance = dijkstra_path(weighted_graph, 'A', 'E')
    print(f"A 到 E 的最短路径: {path}, 距离 = {distance}")

    # 最小生成树
    print("\n7. 最小生成树")
    kruskal_edges, kruskal_weight = kruskal(weighted_graph)
    print("Kruskal 算法:")
    for edge in kruskal_edges:
        print(f"  {edge[0]} - {edge[1]} (权重: {edge[2]})")
    print(f"总权重: {kruskal_weight}")

    prim_edges, prim_weight = prim(weighted_graph, 'A')
    print("\nPrim 算法:")
    for edge in prim_edges:
        print(f"  {edge[0]} - {edge[1]} (权重: {edge[2]})")
    print(f"总权重: {prim_weight}")

    # 拓扑排序
    print("\n8. 拓扑排序")
    dag = DirectedGraph()
    dag_edges = [
        ('A', 'B'), ('A', 'C'), ('B', 'D'),
        ('C', 'D'), ('D', 'E')
    ]
    for u, v in dag_edges:
        dag.add_edge(u, v)

    topo_result = topological_sort(dag)
    print(f"拓扑排序结果: {topo_result}")


if __name__ == '__main__':
    test_graph_algorithms()

算法复杂度分析

算法 时间复杂度 空间复杂度 适用场景
DFS O(V + E) O(V) 连通性检测、路径查找
BFS O(V + E) O(V) 最短路径(无权图)、层次遍历
Dijkstra O((V + E) log V) O(V) 单源最短路径(非负权)
Floyd-Warshall O(V³) O(V²) 全源最短路径
Kruskal O(E log E) O(V + E) 稀疏图的最小生成树
Prim O((V + E) log V) O(V + E) 密集图的最小生成树
拓扑排序 O(V + E) O(V) 任务调度、依赖关系

应用场景

  1. 社交网络
  2. 好友推荐(最短路径)
  3. 社区发现(连通分量)

  4. 路由协议

  5. 最短路由(Dijkstra)
  6. 网络拓扑(最小生成树)

  7. 任务调度

  8. 依赖关系(拓扑排序)
  9. 关键路径(最长路径)

  10. 地图导航

  11. 最短路径(Dijkstra、A*)
  12. 地图匹配(图匹配)

扩展算法

  1. A* 算法:启发式搜索
  2. Bellman-Ford:负权边的最短路径
  3. 强连通分量:Kosaraju、Tarjan 算法
  4. 二分图匹配:匈牙利算法
  5. 网络流:Ford-Fulkerson、Edmonds-Karp

项目总结

本项目实现了常见的图算法,涵盖了:

  1. 图的基本操作:创建、添加顶点和边
  2. 遍历算法:DFS、BFS
  3. 最短路径:Dijkstra、Floyd-Warshall
  4. 最小生成树:Kruskal、Prim
  5. 拓扑排序:Kahn、DFS

通过本项目,你将掌握: - 图的数据结构表示 - 经典图算法的实现 - 算法复杂度分析 - 实际问题的建模和求解

下一步

尝试实现更多高级算法: - A* 启发式搜索 - Bellman-Ford 算法 - 强连通分量算法 - 最大流最小割 - 二分图匹配 - 欧拉路径和哈密顿路径


祝你学习愉快!