图论算法

并查集

class UF:
    def __init__(self, n):
        self.parents = list(range(n))
        self.count = n

    def find(self, v):
        if self.parents[v] !=v:
            self.parents[v] = self.find(self.parents[v])
        return self.parents[v]

    def union(self, u, v):
        parent_u = self.find(u)
        parent_v = self.find(v)
        if parent_u != parent_v:
            self.parents[parent_v] = parent_u
            self.count -= 1

    def count(self):
        return self.count

递归技巧

链接, 在递归函数的开头,调用 printIndent(count++) 并打印关键变量;然后在所有 return 语句之前调用 printIndent(--count) 并打印返回值。

// 全局变量,记录递归函数的递归层数
int count = 0;

// 输入 n,打印 n 个 tab 缩进
void printIndent(int n) {
    for (int i = 0; i < n; i++) {
        printf("   ");
    }
}
# 二叉树遍历框架
def traverse(root):
    if not root:
        return
    traverse(root.left);
    traverse(root.right);

# 多叉树遍历框架
def traverse(root):
    if not root:
        return
    for child in root.children:
        traverse(child)

# 图遍历框架
def traverse(graph, v):
    global visited
    # 防止走回头路进入死循环
    if visited[v]: 
        return
    # 前序遍历位置,标记节点 v 已访问
    visited[v] = True
    for neighbor in graph.neighbors(v):
        traverse(graph, neighbor)

visited = []

# 另一种图遍历写法
visited = []
def traverse(graph: Graph, v: int) -> None:
    # 前序遍历位置,标记节点 v 已访问
    visited[v] = True
    for neighbor in graph.neighbors(v):
        if not visited[neighbor]:
            # 只遍历没标记过的相邻节点
            traverse(graph, neighbor)

BFS

  • BFS可以找到最短路径, 但是空间复杂度高,而 DFS 的空间复杂度较低
  • 如果知道重终点在哪,可以使用双向BFS,从终点开始也搜索。时间复杂度不变,运行速度更快。
from typing import List, Set
from collections import deque

class Node:
    def __init__(self, val: int):
        self.val = val
        self.neighbors = []

def BFS(start: Node, target: Node) -> int:
    q = deque() # 核心数据结构
    visited = set() # 避免走回头路
    q.append(start) # 将起点加入队列
    visited.add(start)

    step = 0 # 记录扩散的步数

    while q:
        step += 1
        size = len(q)
        # 将当前队列中的所有节点向四周扩散
        for i in range(size):
            cur = q.popleft()
            # 划重点:这里判断是否到达终点
            if cur == target:
                return step
            # 将cur相邻节点加入队列
            for x in cur.neighbors:
                if x not in visited:
                    q.append(x)
                    visited.add(x)
    # 如果走到这里,说明在图中没有找到目标节点
    return -1

DFS (回溯算法)

多源最短路径 Floyd

适用于加权有向图,能够处理负权重的边,但不能有回路。
Flyod是处理多源最短路径(任意 2 点),时间复杂度为O(n^3) 空间复杂度为O(n ^ 2);
如果是任意两个点之间的最短路径或者是负权图,就用Floyd;

# Floyd-Warshall Algorithm Implementation

# Define a large value for infinity
INF = float('inf')

def floyd_warshall(graph):
    # Number of vertices in the graph
    num_vertices = len(graph)
    
    # Initialize the distance matrix with the same values as the graph matrix
    dist = [[graph[i][j] for j in range(num_vertices)] for i in range(num_vertices)]
    
    # Apply Floyd-Warshall algorithm
    for k in range(num_vertices):
        for i in range(num_vertices):
            for j in range(num_vertices):
                # Update the distance matrix
                if dist[i][j] > dist[i][k] + dist[k][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
    
    return dist

# Example usage
graph = [
    [0, 3, INF, INF],
    [2, 0, INF, INF],
    [INF, 7, 0, 1],
    [6, INF, INF, 0]
]

shortest_paths = floyd_warshall(graph)

print("The shortest path matrix is:")
for row in shortest_paths:
    print(row)

单源最短路径

Dijkstra算法是一种用于计算单源(从单独s到其他任意node t,或给定某一个 node t)最短路径的经典算法。它适用于加权有向图,但要求所有边的权重为非负数。O(N^2),如果用堆优化会更低(O((V+E)logV))。
所以题目中如果是单源点正权图,就用Dijkstra

import heapq

def dijkstra(graph, start, end):
    # Priority queue to store (distance, vertex) tuples
    pq = [(0, start)]
    # Dictionary to store the shortest distance to each vertex
    distances = {vertex: float('inf') for vertex in graph}
    distances[start] = 0
    # Dictionary to store the shortest path to each vertex
    previous_vertices = {vertex: None for vertex in graph}

    while pq:
        # Get the vertex with the smallest distance
        current_distance, current_vertex = heapq.heappop(pq)

        # If we have reached the end vertex, we can stop
        if current_vertex == end:
            break

        # Explore neighbors
        for neighbor, weight in graph[current_vertex].items():
            distance = current_distance + weight

            # If a shorter path to the neighbor is found
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                previous_vertices[neighbor] = current_vertex
                heapq.heappush(pq, (distance, neighbor))

    # Reconstruct the shortest path
    path = []
    current_vertex = end
    while previous_vertices[current_vertex] is not None:
        path.append(current_vertex)
        current_vertex = previous_vertices[current_vertex]
    path.append(start)
    path.reverse()

    return path, distances[end]

# Example usage
graph = {
    'A': {'B': 1, 'C': 4},
    'B': {'A': 1, 'C': 2, 'D': 5},
    'C': {'A': 4, 'B': 2, 'D': 1},
    'D': {'B': 5, 'C': 1}
}

start_node = 'A'
end_node = 'D'
path, distance = dijkstra(graph, start_node, end_node)

print(f"The shortest path from {start_node} to {end_node} is: {path} with distance {distance}")

bellman-ford 算法

更加通用版本的 dijkstra,可以处理负权重。和dijkstra算法的区别是不使用小顶堆(本质是个贪心),B-F是个动态规划,要全部遍历, 复杂度是 O(V*E)。

bellman-ford适合稠密的小图,或者带负权重的边,dijkstra适合稀疏的大图。

class Graph:
   def __init__(self, vertices):
       self.V = vertices  # 顶点数
       self.edges = []    # 边的列表

   def add_edge(self, u, v, weight):
       self.edges.append((u, v, weight))

   def bellman_ford(self, source):
       # 初始化距离
       dist = [float('inf')] * self.V
       dist[source] = 0

       # 松弛所有边 V-1 次
       for _ in range(self.V - 1):
           for u, v, weight in self.edges:
               if dist[u] != float('inf') and dist[u] + weight < dist[v]:
                   dist[v] = dist[u] + weight

       # 检测负权环
       for u, v, weight in self.edges:
           if dist[u] != float('inf') and dist[u] + weight < dist[v]:
               print("Graph contains a negative weight cycle")
               return

       # 打印结果
       for i in range(self.V):
           print(f"Distance from source {source} to vertex {i} is {dist[i]}")

# 示例用法
g = Graph(5)
g.add_edge(0, 1, -1)
g.add_edge(0, 2, 4)
g.add_edge(1, 2, 3)
g.add_edge(1, 3, 2)
g.add_edge(1, 4, 2)
g.add_edge(3, 1, 1)
g.add_edge(3, 2, 5)
g.add_edge(4, 3, -3)

g.bellman_ford(0)

最小生成树

kruskal算法

并查集基础上,对edge排序,每次加入weight最小的,不连通的边。

prim算法

prim

切分定理:
对于任意一种「切分」,其中权重最小的那条「横切边」一定是构成最小生成树的一条边。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 本来下学期在学姐的强力安利之下选了算法这个课,但是取消了,于是在家打发时间看了edX上的一个法国人讲的算法网课。主...
    当年老子最帅阅读 3,678评论 0 3
  • 一个图(graph) G=(V, E)由顶点(vertex) 的集 V 和边(edge) 的集 E 组成。每一条边...
    Sun东辉阅读 734评论 0 3
  • 一、并查集 并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定...
    肖一二三四阅读 1,329评论 0 0
  • 1. 图的表示:邻接矩阵和邻接表 邻接矩阵:大小为|V|的二维数组,对于每条边(u, v),置A[u][v]=1或...
    第八天的蝉啊阅读 228评论 0 0
  • 深度优先搜索 在图中搜索的一般过程为: 记录当前结点被发现的时间(discovery time) 遍历访问未被访问...
    njzwj阅读 1,795评论 0 0