第 5 章:算法中的图
图是一种基本的数据结构,用于模拟对象之间的连接和关系。它在计算机科学和其他领域有广泛的应用,从社交网络和网页链接的建模,到解决交通、调度和资源分配等问题。在本章中,我们将探讨处理图的基本属性和算法,重点关注无向图、深度优先搜索、广度优先搜索、最小生成树和最短路径。
无向图
无向图是一组通过边连接的顶点(或节点)。形式上,我们将无向图 G 定义为一对 (V, E),其中 V 是顶点集合,E 是无序顶点对的集合,称为边。边 (v, w) 连接顶点 v 和 w。我们说 v 和 w 是相邻的或邻居。顶点的度数是与之相连的边的数量。
下面是一个简单的无向图示例:
A --- B
/ / \
/ / \
C --- D --- E
在这个图中,顶点集合 V = {A, B, C, D, E}
,边集合 E = {(A, B), (A, C), (B, D), (B, E), (C, D), (D, E)}
。
有几种方法可以在程序中表示图。两种常见的表示方法是:
-
邻接矩阵: 一个 n x n 的布尔矩阵 A,其中 n 是顶点的数量。如果存在从顶点 i 到顶点 j 的边,则 A[i][j] 为 true,否则为 false。
-
邻接列表: 一个大小为 n 的数组 Adj,其中 n 是顶点的数量。Adj[v] 包含顶点 v 的邻居列表。
选择哪种表示取决于图的密度(边与顶点的比率)以及需要执行的操作。邻接矩阵简单但可能对于稀疏图效率较低。邻接列表对于稀疏图更加空间高效,并且可以更快地访问顶点的邻居。
下面是一个使用 Java 中邻接列表表示上述图的示例:
List<Integer>[] graph = (List<Integer>[]) new List[5];
graph[0] = Arrays.asList(1, 2); // A -> B, C
graph[1] = Arrays.asList(0, 3, 4); // B -> A, D, E
graph[2] = Arrays.asList(0, 3); // C -> A, D
graph[3] = Arrays.asList(1, 2, 4); // D -> B, C, E
graph[4] = Arrays.asList(1, 3); // E -> B, D
深度优先搜索 (DFS)
深度优先搜索 (DFS) 是一种基本的图遍历算法,它沿着每个分支尽可能深入地探索,在回溯之前。它可用于解决许多图问题,如寻找连通分量、拓扑排序和检测循环。
DFS 算法的工作原理如下:
- 从源顶点 s 开始。
- 将当前顶点标记为已访问。
- 递归地访问所有未标记的相邻顶点 w。
- 如果当前顶点的所有相邻顶点都已访问,则回溯到当前顶点被探索的顶点。
- 如果还有未访问的顶点,选择一个并从步骤 1 重复。
以下是使用邻接表实现的简单 Java DFS 代码:
boolean[] visited;
void dfs(List<Integer>[] graph, int v) {
// 将当前顶点标记为已访问
visited[v] = true;
// 递归地访问所有未标记的相邻顶点
for (int w : graph[v]) {
if (!visited[w]) {
dfs(graph, w);
}
}
}
要执行完整的 DFS 遍历,我们需要对图中的每个顶点 s 调用 dfs(graph, s)
,其中 visited
数组初始化为 false
。
DFS 有许多应用。例如,我们可以使用它来找出无向图中的连通分量,方法是从每个未访问的顶点运行 DFS,并根据 DFS 树将每个顶点分配到一个分量。
广度优先搜索 (BFS)
广度优先搜索 (BFS) 是另一种基本的图遍历算法,它按层次探索顶点。它首先访问所有当前深度的顶点,然后再移动到下一个深度级别的顶点。
BFS 算法的工作原理如下:
- 从源顶点 s 开始,并将其标记为已访问。
- 将 s 入队到一个先进先出 (FIFO) 队列中。
- 当队列不为空时,执行以下操作:
- 从队列中出队一个顶点 v。
- 对于 v 的所有未访问的相邻顶点 w,将其标记为已访问并入队。当队列不为空时:
- 出队一个顶点 v。
- 对于每个未标记的顶点 w 与 v 相邻:
- 将 w 标记为已访问。
- 将 w 入队。
以下是使用邻接列表实现 BFS 的 Java 代码:
boolean[] visited;
void bfs(List<Integer>[] graph, int s) {
Queue<Integer> queue = new LinkedList<>();
visited[s] = true;
queue.offer(s);
while (!queue.isEmpty()) {
int v = queue.poll();
for (int w : graph[v]) {
if (!visited[w]) {
visited[w] = true;
queue.offer(w);
}
}
}
}
BFS 特别适用于在无权图中寻找最短路径。从源顶点到任何其他顶点的距离是两者之间路径上边的最小数量。BFS 可以保证找到最短路径。
最小生成树
最小生成树 (MST) 是一个连通、带权无向图的一个子集,它连接所有顶点,没有环,且总边权重最小。
两个经典的寻找 MST 的算法是 Kruskal 算法和 Prim 算法。
Kruskal 算法的工作过程如下:
- 创建一个森林 F,每个顶点都是一棵独立的树。
- 创建一个包含图中所有边的集合 S。
- 当 S 不为空且 F 还不是一棵生成树时:
- 从 S 中移除一条权重最小的边。
- 如果被移除的边连接了两棵不同的树,则将其加入 F,合并这两棵树。
Prim 算法的工作过程如下:
- 初始化一棵树,包含一个任意选择的顶点。
- 通过添加一条边来扩展这棵树:在连接树和尚未加入树的顶点的所有边中,找到权重最小的边,并将其加入树中。
- 重复步骤 2,直到所有顶点都在树中。
以下是 Prim 算法的 Java 实现:
int minKey(int[] key, boolean[] mstSet, int V) {
int min = Integer.MAX_VALUE, min_index = -1;
.
```以下是该 Markdown 文件的中文翻译版本。对于代码部分,只翻译注释,不翻译代码本身。文件开头没有添加任何额外的注释。
```java
for (int v = 0; v < V; v++) {
// 如果顶点 v 不在最小生成树集合中,且 v 的键值小于当前最小值
if (!mstSet[v] && key[v] < min) {
// 更新最小值和最小索引
min = key[v];
min_index = v;
}
}
return min_index;
}
void primMST(int[][] graph, int V) {
// 父节点数组
int[] parent = new int[V];
// 键值数组
int[] key = new int[V];
// 最小生成树集合
boolean[] mstSet = new boolean[V];
// 初始化键值为无穷大,最小生成树集合为 false
for (int i = 0; i < V; i++) {
key[i] = Integer.MAX_VALUE;
mstSet[i] = false;
}
// 源点的键值为 0,父节点为 -1
key[0] = 0;
parent[0] = -1;
// 循环 V-1 次,构建最小生成树
for (int count = 0; count < V - 1; count++) {
// 找到当前最小键值的顶点
int u = minKey(key, mstSet, V);
// 将该顶点加入最小生成树集合
mstSet[u] = true;
// 更新与 u 相邻顶点的键值和父节点
for (int v = 0; v < V; v++) {
if (graph[u][v] != 0 && !mstSet[v] && graph[u][v] < key[v]) {
parent[v] = u;
key[v] = graph[u][v];
}
}
}
// 打印最小生成树
printMST(parent, graph, V);
}
最小生成树有许多应用,如设计网络(通信、电力、水利、计算机)和近似旅行商问题。
最短路径
最短路径问题是在图中找到两个顶点之间的一条路径,使得路径上边的权重之和最小。这个问题有许多变体,如单源最短路径、所有对最短路径和单目标最短路径。
Dijkstra 算法是一种贪心算法,用于解决图中非负边权重的单源最短路径问题。它的工作原理如下:
- 创建一个集合
sptSet
(最短路径树集合),用于跟踪已包含在最短路径树中的顶点。 - 为图中的所有顶点分配一个距离值。将所有距离值初始化为无穷大,将源顶点的距离值设为 0。
- 当
sptSet
没有包含所有顶点时,选择一个不在sptSet
中且距离值最小的顶点 v,将其加入sptSet
。
更新 v 的所有相邻顶点的距离值。要更新距离值,遍历所有相邻顶点。对于每个相邻顶点 w,如果 v 到 w 的距离加上 v 的距离值小于 w 当前的距离值,则更新 w 的距离值。这是一个 Markdown 文件,包含了 Dijkstra 算法和 Bellman-Ford 算法的 Java 实现。以下是中文翻译:
如果从顶点 v 到顶点 w 的边的权重加上顶点 v 的距离值小于顶点 w 的距离值,则更新顶点 w 的距离值。
以下是 Dijkstra 算法的 Java 实现:
public void dijkstra(int[][] graph, int src) {
int V = graph.length;
int[] dist = new int[V];
boolean[] sptSet = new boolean[V];
// 初始化所有顶点的距离值为无穷大,并将它们标记为未被访问
for (int i = 0; i < V; i++) {
dist[i] = Integer.MAX_VALUE;
sptSet[i] = false;
}
// 源顶点的距离值设为 0
dist[src] = 0;
// 循环 V-1 次,每次找到距离源顶点最近的未访问顶点,并更新其相邻顶点的距离值
for (int count = 0; count < V - 1; count++) {
int u = minDistance(dist, sptSet);
sptSet[u] = true;
for (int v = 0; v < V; v++) {
if (!sptSet[v] && graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE
&& dist[u] + graph[u][v] < dist[v]) {
dist[v] = dist[u] + graph[u][v];
}
}
}
printSolution(dist);
}
Bellman-Ford 算法是另一种用于查找单源最短路径的算法,它可以处理包含负权边的图。该算法的工作原理如下:
- 将所有顶点的距离值初始化为无穷大,源顶点的距离值设为 0。
- 重复 |V| - 1 次,对每条边进行松弛操作。如果通过某条边可以缩短到达顶点的距离,则更新该顶点的距离值。
- 检查是否存在负权重循环。再次对所有边进行松弛操作,如果任何距离值发生变化,则说明存在负权重循环。
以下是 Bellman-Ford 算法的 Java 实现:
public void bellmanFord(int[][] graph, int src) {
int V = graph.length;
int[] dist = new int[V];
// 初始化所有顶点的距离值为无穷大,源顶点的距离值设为 0
for (int i = 0; i < V; i++)
dist[i] = Integer.MAX_VALUE;
dist[src] = 0;
// 重复 |V| - 1 次,对每条边进行松弛操作
for (int i = 1; i < V; i++) {
for (int u = 0; u < V; u++) {
for (int v = 0; v < V; v++) {
if (graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE
&& dist[u] + graph[u][v] < dist[v]) {
dist[v] = dist[u] + graph[u][v];
}
}
}
}
// 检查是否存在负权重循环
for (int u = 0; u < V; u++) {
for (int v = 0; v < V; v++) {
if (graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE
&& dist[u] + graph[u][v] < dist[v]) {
System.out.println("Graph contains negative-weight cycle");
return;
}
}
}
printSolution(dist);
}
```这个 Markdown 文件的中文翻译如下:
```java
if (dist[u] != Integer.MAX_VALUE
&& dist[u] + graph[u][v] < dist[v]) {
// 如果从起点 u 到顶点 v 的距离小于当前记录的距离,
// 则更新 v 的距离为从 u 到 v 的距离
dist[v] = dist[u] + graph[u][v];
}
for (int u = 0; u < V; u++) {
for (int v = 0; v < V; v++) {
// 检查是否存在负权重环
if (graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE
&& dist[u] + graph[u][v] < dist[v]) {
System.out.println("Graph contains negative weight cycle");
return;
}
}
}
最短路径算法有许多应用,如导航系统、网络路由协议和交通规划。它们是图论中的基础工具,在许多图处理任务中都是必需的。
结论
图是一种多样且强大的数据结构,可以模拟各种各样的问题。在本章中,我们探讨了图的基本属性和类型,并研究了深度优先搜索、广度优先搜索、最小生成树和最短路径等基本图算法。
深度优先搜索和广度优先搜索提供了系统地探索图的方法,并构成了许多高级图算法的基础。Kruskal 和 Prim 算法等最小生成树算法可以找到连接所有顶点的最小总边权重的树。Dijkstra 和 Bellman-Ford 等最短路径算法可以找到顶点之间的最小权重路径。
理解这些核心概念和算法对于有效地处理图并解决复杂问题至关重要。随着你在算法学习的过程中不断深入,你将会遇到更多建立在这些基础技术之上的高级图算法。
图为描述和解决计算机科学及其他领域中的问题提供了强大的语言。掌握图算法将为你提供一个多功能的工具箱,用于建模和解决广泛的计算问题。# 自然语言处理挑战
1. 情感分析
情感分析是一种自然语言处理技术,用于确定文本中表达的情感。这可以用于各种应用,如客户反馈分析、社交媒体监控和产品评论挖掘。
# 导入必要的库
import pandas as pd
from textblob import TextBlob
# 加载数据
data = pd.read_csv('reviews.csv')
# 对每个评论进行情感分析
data['sentiment'] = data['review'].apply(lambda x: TextBlob(x).sentiment.polarity)
# 根据情感得分对评论进行分类
data['sentiment_label'] = data['sentiment'].apply(lambda x: 'positive' if x > 0 else 'negative')
# 输出结果
print(data.head())
2. 命名实体识别
命名实体识别是一种自然语言处理技术,用于从文本中识别和提取具有特定意义的实体,如人名、地名、组织名等。这可以用于各种应用,如信息提取、问答系统和文档摘要。
# 导入必要的库
import spacy
# 加载 spaCy 模型
nlp = spacy.load('en_core_web_sm')
# 处理示例文本
doc = nlp("Apple CEO Tim Cook announced the new iPhone 12 in San Francisco.")
# 识别命名实体
for entity in doc.ents:
print(entity.text, entity.label_)
3. 文本摘要
文本摘要是一种自然语言处理技术,用于从长文本中提取最重要的信息,生成简洁的摘要。这可以用于各种应用,如新闻摘要、论文摘要和产品描述摘要。
# 导入必要的库
from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.lsa import LsaSummarizer
# 加载示例文本
text = """
自然语言处理(NLP)是一个广泛的研究领域,涉及计算机科学、语言学和人工智能等多个学科。它的目标是使计算机能够理解和处理人类语言,从而实现人机交互和信息处理的自动化。
NLP包括多个子任务,如情感分析、命名实体识别和文本摘要等。这些技术在各种应用中都有广泛应用,如客户反馈分析、信息提取和文档管理等。
随着深度学习等新技术的发展,NLP的性能不断提高,在很多领域都取得了突破性进展。未来,NLP将继续发展,为人类提供更智能、更便捷的语言处理服务。
"""
# 创建文本解析器
parser = PlaintextParser.from_string(text, Tokenizer('chinese'))
# 创建摘要生成器
summarizer = LsaSummarizer()
# 生成摘要
summary = summarizer(parser.document, 3) # 生成3句摘要
# 输出摘要
for sentence in summary:
print(sentence)