图论基础
图的基础介绍
-
图分为有向图和无向图,有向指连接俩个节点的边是有方向的,而无向图则是没有方向的
-
同时图还可以分成加权有向图和加权无向图。意思是在有向图和无向图的基础上把边进行赋值(每条边上面都有一个数值代表其权值)
-
图的性质
-
度:对于无向图而言,每个节点所连接的边数就是度
-
入度和出度:对于有向图而言,从某节点出发的边的数量是出度,而指向该节点的边数量是入度
图的连通性
什么是连通图?------任意两个节点都是可以到达的(用于无向图连通性的描述)
什么是强连通图?------任意两个节点都是可以相互到达的(用于描述有向图)
连通分量:
-
无向图中,极大连通 子图称之为该图的一个连通分量(判断是否是连通分量,重点在于判断1.是否是极大图 ,2. 是否是连通子图)
-
有向图中,极大****强连通 子图称之为该图的强连通分量(则判断是否为强连通分量,重点在于判断1. 是否为极大图,2. 是否为强连通子图)
图的构造
-
朴素存储:存储边到数组中。存储所有边,一定是要通过节点来存储,两个节点一个边。最大的优点就是直观。
-
邻接矩阵:从节点的角度来表示图,存储边的两个节点到二维数组中,例如grid[2][3]=6表示节点2和节点3之间有个边,边的权值为6
优点:适合稠密图(节点和边数差不多),找任意两个节点之间是否存在边十分便利。
缺点:不适合稀疏图,会大大的浪费空间n*n(n表示节点数),遍历 边 的时候需要遍历整个n * n矩阵
- 邻接表:从边的数量为基准来创建链表和数组。邻接表是有一个数组,数组存储的是链表元素,该元素后面跟着的是与其连接的节点。
优点:适合稀疏图,只用存储边,空间利用率高。遍历边十分方便
缺点: 但是找任意两个节点间是否存在边十分麻烦。
图的遍历方式
- 广度优先遍历
广度的意思是:先把本节点连接的所有节点遍历一遍,再走到下一个节点,进行同样的操作
- 深度优先遍历
深度是:一直朝一个方向去搜索
这两个其实非常像二叉树的遍历方式,层序遍历和递归遍历(前中后序)
深搜
例如1 和 6 两条节点之间有这样4条路分别是:
- 1-5-6
- 1-5-4-6
- 1-5-4-3-6
- 1-4-6
- 假设我们先走第一条路,则到达目的地后往前回溯,取消5-6之间的路,去尝试走5-4,4再到6
- 现在又到达目的地了,则往前回溯,4不走6,而是先走3,3再到达6
- 到达目的地,回溯,不走1-5,而是走1-4,4再到达6
- 到达目的地,当然我们在回溯再选择路径时,会有碰壁的情况------则一碰壁就需要换方向
🆗观察上面过程发现,整个遍历过程是有规律的:
- 搜索方向,是先认准一个方向搜,直到碰壁之后或者到达目的地之后再换方向(撤销原先的路径,改成该节点链接的下一个路径方向(回溯))
代码框架
回溯三部曲:
- 确立递归条件和参数
例如:
cpp
vector<vector<int>> result; // 保存符合条件的所有路径
vector<int> path; // 起点到终点的路径
void dfs (图,目前搜索的节点)
- 确立递归的结束条件
cpp
if (终止条件) {
存放结果;
return;
}
- 处理目前搜索节点出发的路径
cpp
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
撤销处理结果;//回溯
}
题目1:98. 所有可达路径

图的存储方式:邻接表 和 邻接矩阵,用这题来进行完善存储方式的代码:
- 邻接矩阵:之前说过是根据节点的角度来存储的,则邻接矩阵申请空间也与节点数量息息相关:节点多大数量,矩阵的空间就多大(二维)
cpp
vector<vector<int>> graph(n + 1, vector<int>(n + 1, 0));
cpp
while (m--) {
cin >> s >> t;
graph[s][t] = 1; //1 表示节点s指向节点t
}
- 邻接表:一个数组,一个链表(存储在数组中)。大小和边数有关:
cpp
vector<list<int>> graph(n + 1);//list是链表
⭐: 节点编号从1到n,所以申请 n+1 这么大的数组
cpp
while (m--) {
cin >> s >> t;
graph[s].push_back(t);//表示s -> t
}
深度遍历三部曲分析:
返回值和参数:由于最终是要记录路径数,则存储到res即可,返回值void,参数为图(邻接矩阵)和当前遍历的节点,以及终点
结束条件:若最终当前遍历的节点==终点,(结束条件不用写无法到达终点的路径,在处理当前遍历的节点这种情况就已经被排查了,不可能到达终点也不可能是答案路径)
处理目前搜索到的节点:当前节点往下走,则需要先判断是否为1(groups邻接矩阵的值),x到i(i是从1遍历到n)
邻接矩阵代码:
cpp
#include<iostream>
#include<vector>
using namespace std;
vector<vector<int>> res;
vector<int> v;
void f(vector<vector<int>>& group,int x,int n){
if(x==n){
res.push_back(v);
return;
}
for(int i=1;i<=n;i++){
if(group[x][i]==1){
v.push_back(i);
f(group,i,n);
v.pop_back();
}
}
}
int main(){
int n,m;
cin>>n>>m;
vector<vector<int>> group(n+1,vector<int>(n+1,0));
int s,t;
while(m--){
cin>>s>>t;
group[s][t]=1;
}
v.push_back(1);
f(group,1,n);
if (res.size() == 0) cout<<-1<<endl;
for(auto& it:res){
for(int i=0;i<it.size()-1;i++){
cout<<it[i]<<" ";
} cout << it.back()<< endl;
}
return 0;
}
邻接表代码:
cpp
#include<iostream>
#include<vector>
#include<list>
using namespace std;
vector<vector<int>> res;
vector<int> v;
void f(vector<list<int>>& graph,int x,int n){
if(x==n){
res.push_back(v);
return;
}
for(int i : graph[x]){//遍历x连接的节点
v.push_back(i);
f(graph,i,n);
v.pop_back();
}
}
int main(){
int n,m;
cin>>n>>m;
vector<list<int>> graph(n+1);
int s,t;
while(m--){
cin>>s>>t;
graph[s].push_back(t);
}
v.push_back(1);
f(graph,1,n);
if (res.size() == 0) cout<<-1<<endl;
for(auto& it:res){
for(int i=0;i<it.size()-1;i++){
cout<<it[i]<<" ";
} cout << it.back()<< endl;
}
return 0;
}
两个代码主要的区别在于1. 构建函数 2. 遍历连接x的节点
广搜
广搜------适合于解决两个点之间的最短路径问题
一圈一圈遍历,使用栈和队列都行。(常用队列)
cpp
#include <vector>
#include <queue>
using namespace std;
// 定义四个方向:右、下、上、左
int directions[4][2] = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
// BFS遍历网格
// grid: 二维网格地图
// visited: 访问标记数组
// start_x, start_y: 起始坐标
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int start_x, int start_y) {
queue<pair<int, int>> q; // 创建队列
q.push({start_x, start_y}); // 起点入队
visited[start_x][start_y] = true; // 标记已访问
while (!q.empty()) {
// 取出队首元素
auto current = q.front();
q.pop();
int current_x = current.first;
int current_y = current.second;
// 遍历四个方向
for (int i = 0; i < 4; i++) {
int next_x = current_x + directions[i][0];
int next_y = current_y + directions[i][1];
// 检查边界
if (next_x < 0 || next_x >= grid.size() ||
next_y < 0 || next_y >= grid[0].size()) {
continue;
}
// 检查是否未访问
if (!visited[next_x][next_y]) {
q.push({next_x, next_y});
visited[next_x][next_y] = true;
}
}
}
}