Initial commit: C++ courseware
This commit is contained in:
commit
4a4ee7c287
1789 files changed
+373489
No files matched your search
@@ -0,0 +1,194 @@
|
||||
### **C++ 算法:图论(二)- 图的存储结构 讲义**
|
||||
|
||||
#### **一、 引言**
|
||||
|
||||
##### **1.1 问题引入**
|
||||
|
||||
我们已经知道,一张校园地图可以被抽象为一个由顶点(建筑)和边(道路)组成的图。但计算机不认识“点”和“边”,我们必须找到一种方式,用代码来描述“哪个顶点和哪个顶点相连?”以及“连接它们的道路有多长?”这个问题。
|
||||
|
||||
换言之,我们需要一个**数据结构**,来充当理论模型与算法实现之间的桥梁。
|
||||
|
||||
而存储图有多种办法,不同的方法会影响后续图算法的**时间复杂度**和**空间复杂度**。
|
||||
|
||||
---
|
||||
|
||||
#### **二、 存储方法一:邻接矩阵 (Adjacency Matrix)**
|
||||
|
||||
##### **2.1 核心思想:关系地图**
|
||||
|
||||
邻接矩阵:我们创建一个二维数组,用它来记录**每一对顶点之间**是否存在边。
|
||||
|
||||
例如我们有一张城市间的航班距离表:
|
||||
|
||||
| | 北京 | 上海 | 广州 |
|
||||
| :------- | :----- | :----- | :----- |
|
||||
| **北京** | 0 | 1200km | 2100km |
|
||||
| **上海** | 1200km | 0 | 1400km |
|
||||
| **广州** | 2100km | 1400km | 0 |
|
||||
|
||||
这个表格就是一个邻接矩阵。行和列都代表图的顶点,表格中的`[行, 列]`位置的数值,就代表了对应两个顶点间的关系(边的信息)。
|
||||
|
||||
##### **2.2 实现细节**
|
||||
|
||||
在C++中,我们通常使用一个二维数组 `G[N][N]` 来表示邻接矩阵,其中 `N` 是顶点的最大数量。`G[u][v]` 的值代表了从顶点 `u` 到顶点 `v` 的边的信息。
|
||||
|
||||
* **对于无权图**:
|
||||
* `G[u][v] = 1` 表示从 `u`到 `v` 存在一条边。
|
||||
* `G[u][v] = 0` 表示从 `u`到 `v` 不存在边。
|
||||
|
||||
* **对于带权图**:
|
||||
* `G[u][v] = w` 表示从 `u`到 `v` 存在一条边,且其权重为 `w`。
|
||||
* 如果边不存在,我们可以用一个特殊值来表示,通常是 **0, -1 或 无穷大**。如果用一个很大的整数(可以用 `0x3f3f3f3f`)来模拟无穷大,这样如果需要进行一些计算的时候的时候不易溢出。
|
||||
|
||||
* **对于无向图**:
|
||||
* 图中的边是双向的,所以矩阵是**对称**的。即 `G[u][v]` 和 `G[v][u]` 的值总是相等。我们在存边时,需要同时设置这两个值。
|
||||
|
||||
##### **2.3 代码实现**
|
||||
|
||||
我们实现储存一个顶点数量为 n , 边的数量为 m 的一个有向带权图(权重定义为顶点之间的距离)。
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
int n, m; // n个顶点, m条边
|
||||
cin >> n >> m;
|
||||
|
||||
// vector的大小为 n x n,初始全部设置为 0,代表不存在任何边
|
||||
vector<vector<int> > G(n, vecotr<int>(n, 0));
|
||||
|
||||
for (int i = 0; i < m; i++) {
|
||||
int u, v, w;
|
||||
cin >> u >> v >> w; // 读入一条从u到v,权重为w的边
|
||||
|
||||
// 如果是无向图,需要存两条边
|
||||
G[u][v] = w;
|
||||
G[v][u] = w; // 若是有向图,则没有这一行
|
||||
}
|
||||
|
||||
// 查询从顶点2到顶点3的距离(权值)
|
||||
if (G[2][3] == 0) {
|
||||
cout << "顶点2和3没有边相连" << endl;
|
||||
} else {
|
||||
cout << "顶点2到3的距离是: " << G[2][3] << endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
##### **2.4 优劣分析**
|
||||
|
||||
* **优点**:
|
||||
* **实现简单**:非常直观,易于理解和编写。
|
||||
* **查询效率高**:判断两个顶点 `u` 和 `v` 是否有边只需访问 `G[u][v]` 即可,时间复杂度为 **O(1)**。
|
||||
* **缺点**:
|
||||
* **空间复杂度高**:无论图中有多少条边,都需要一个 `N*N` 的空间,空间复杂度为 **O(N²)**。
|
||||
* **对于稀疏图(边数远小于顶点数的平方)来说,空间浪费巨大**。例如,1万个点但只有100条边的图,矩阵中绝大部分空间都是储存的 0,代表没有边相连,被浪费了。
|
||||
* **遍历邻居效率低**:要找到一个顶点的所有邻居,必须遍历一整行,时间复杂度为 **O(N)**。
|
||||
|
||||
---
|
||||
|
||||
#### **三、 存储方法二:邻接表 (Adjacency List)**
|
||||
|
||||
##### **3.1 核心思想:记录我的邻居是谁**
|
||||
|
||||
邻接矩阵像一张全国地图,记录了所有城市两两之间的关系,信息很全但很占地方。而邻接表则更像每个人的通讯录,只记录与自己有直接关系的人。
|
||||
|
||||
具体来说,我们为图中的**每一个顶点**,都维护一个**列表**,这个列表里只存放与该顶点**直接相连**的其他顶点(以及边的权重,如果是带权图)。
|
||||
|
||||
##### **3.2 通过`struct`封装实现邻接表**
|
||||
|
||||
为了更好地组织和管理图的数据,我们可以使用一个自定义的结构体 `Graph` 来表示和存储一个图。这种方式将图的所有关键信息打包在一起,让代码结构更清晰。
|
||||
|
||||
**1. 定义图的结构体**
|
||||
```cpp
|
||||
// 定义图的结构体
|
||||
struct Graph {
|
||||
int V = -1, E = -1; // V:顶点数量, E:边的数量
|
||||
vector<vector<int>> adjList; // 邻接表
|
||||
};
|
||||
```
|
||||
* `V` 和 `E` 分别存储图的顶点数和边数。
|
||||
* `adjList` 是一个 `vector` 的 `vector`。一个动态的二维数组。`adjList[u]` 本身就是一个 `vector`,它将作为顶点 `u` 的“邻居通讯录”,里面存放所有与 `u` 直接相连的顶点编号。
|
||||
|
||||
**2. 初始化图的结构**
|
||||
在使用这个图之前,我们需要一个初始化函数来设置其规模,并准备好存放邻居列表的空间。
|
||||
|
||||
```cpp
|
||||
struct Graph {
|
||||
int V = -1, E = -1; // V:顶点数量, E:边的数量
|
||||
vector<vector<int>> adjList; // 邻接表
|
||||
|
||||
// 初始化函数,设置顶点和边的数量
|
||||
void setCount(int new_V, int new_E) {
|
||||
V = new_V; E = new_E;
|
||||
// 一共有 V 个顶点,故需要 V 个对应顶点的通讯录来储存直接相连的顶点编号
|
||||
adjList.resize(new_V);
|
||||
}
|
||||
};
|
||||
```
|
||||
* `setCount` 函数接收顶点的数量 `new_V`,然后调用 `resize` 方法,将 `adjList` 变成一个包含 `new_V` 个空 `vector` 。
|
||||
|
||||
**3. 添加边**
|
||||
图的结构准备好后,我们需要一个方法来添加边,以描述顶点间的连通情况。
|
||||
|
||||
```cpp
|
||||
struct Graph {
|
||||
// 省略之前的结构体内容...以下为添加边的函数
|
||||
void addEdge(int src, int dest) {
|
||||
if (V == -1) return; // 确保图已被初始化
|
||||
|
||||
// 代表存在一条边src->sest, 在起点 src 的相连顶点数组中添加顶点 dest
|
||||
// 注意如果是无向图,还需要添加从 dest 到 src 的反向边
|
||||
adjList[src].push_back(dest);
|
||||
}
|
||||
};
|
||||
```
|
||||
* `addEdge` 函数就是在起点 `src` 的相连顶点列表 `adjList[src]` 的末尾,添加上终点 `dest`。
|
||||
* **注意**:对于**无向图**,由于边是双向的,添加一条 `(u, v)` 的边,意味着 `v` 是 `u` 的邻居,同时 `u` 也是 `v` 的邻居。因此需要**双向添加**。
|
||||
|
||||
##### **3.3 代码实现:使用`Graph`结构体**
|
||||
|
||||
下面我们通过一个完整的例子,演示如何使用这个 `Graph` 结构体来存储一个**有向无权图**。
|
||||
|
||||
```cpp
|
||||
int main() {
|
||||
// 创建一个图对象
|
||||
Graph G;
|
||||
int V, E;
|
||||
cin >> V >> E;
|
||||
|
||||
// 调用初始化函数,准备好邻接表的结构
|
||||
G.setCount(V, E);
|
||||
|
||||
// 读取 E 条边,并添加到图中
|
||||
for (int i = 0; i < E; ++i) {
|
||||
int src, dest;
|
||||
cin >> src >> dest;
|
||||
G.addEdge(src, dest);
|
||||
}
|
||||
|
||||
// 此时,图G中已经存好了所有的顶点和边信息,可以进行操作,例:打印顶点3的所有邻居
|
||||
for (int neighbor : G.adjList[3]) {
|
||||
cout << neighbor << " ";
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
##### **3.4 优劣分析**
|
||||
|
||||
* **优点**:
|
||||
* **空间效率高**:邻接表只存储实际存在的边,空间复杂度为 **O(N + M)**(N为顶点数,M为边数)。对于使用邻接矩阵的的 **O(N²)**,节省了大量空间。
|
||||
* **遍历邻居效率高**:邻接表要找到一个顶点的所有邻居,只需遍历其对应的 `vector` 即可,时间复杂度为 **O(出度(u))**,其中 `出度(u)` 是顶点u的度。优于邻接矩阵的遍历邻居时间复杂度**O(N)** 。
|
||||
* **缺点**:
|
||||
* **查询效率较低**:邻接表判断两个顶点 `u` 和 `v` 是否有边,需要遍历 `adj[u]` 这个列表,时间复杂度为 **O(出度(u))**,不如邻接矩阵直接查询的 O(1) 快。
|
||||
|
||||
#### **四、 总结**
|
||||
|
||||
* 我们学习了两种主流的图存储方法:**邻接矩阵**和**邻接表**。
|
||||
* **邻接矩阵**是一个二维数组,用 `G[u][v]` 存储边的信息,它像一张**完整的地图**,优点是查询快,缺点是浪费空间。
|
||||
* **邻接表**是一个 `vector` 数组,`adj[u]` 存储了 `u` 的所有邻居,它像一本**通讯录**,优点是节省空间,缺点是查询稍慢。选择主要在于分析图的**疏密程度**:**稠密用矩阵,稀疏用表**。
|
||||
* **在信息学竞赛和实际应用中,由于图的稀疏性,其实邻接表的使用频率会稍高于邻接矩阵。**
|
||||
Reference in new issue
Block a user