目录
基本概念
存储方式
1. 邻接矩阵(存储邻接点的矩阵)
a. 无向无权图
b. 有向无权图
2. 邻接表
a. 无向无权图
b. 有向无权图
深度优先搜索(算法)
1. 栈实现(邻接矩阵)
2. 递归实现
a. 邻接矩阵
b. 邻接表
3. 连通块问题(邻接矩阵)
4. 无权图最短路问题
1.生活中的图:交通路线图、电路图、网络拓扑图...
2.数据结构中的图:
解决图论问题对现实生活中的实际图论问题具有重大的意义
邻接/邻居点:一无向条边上的两个顶点互为邻接点,一条有向边如i->j,则j是i的邻接点,但i不是j的邻接点
路径:一个顶点到达另一个顶点经过的顶点序列
- 无向图:边没有方向,一条无向边上两个顶点可以互相到达
- 有向图:边是有方向的,一个有向边只能单向通过
- 无权图:边没有实际的权重,一般情况下,顶点之间有边置1,无边置0
- 有权图:边有实际的权重(比如:距离)
- 完全图:任意两个顶点之间都有连边,n个定点构成的完全图,有n*(n-1)/2边
- 稀疏图:比如n个顶点m条边构成图,其中m远小于n
- 稠密图:比如n个顶点m条边构成图,其中m远大于n,最高达到完全图的边数
- 连通图:对于无向图,图中任意两个顶点都是连通(有路径)的,则称为为连通图
- 强连通图:对于有向图,图中任意两个顶点都是连通(有路径)的,则称为强连通图
现阶段常用的图的存储即邻接矩阵和邻接表
- 1.邻接矩阵
- 2.邻接表
后期主要用链式前向星
- 3.链式前向星
邻接多重表、十字链表算法竞赛不用
- 4.邻接多重表
- 5.十字链表
什么是邻接矩阵?什么是邻接表?见下图
邻接矩阵优缺点:
- 优点:可以快速定位邻接点,时间复杂度为o(1)(简单易学,容易理解,新手入门图论必备)
- 缺点:空间消耗太大,空间复杂度为o(n^2),遍历n个顶点的邻接点的时间复杂度是o(n^2),其中n为点数
- 邻接矩阵更适合于存储稠密图,不适合存稀疏图
#include
#include
using namespace std;
const int N= 1e2;
//g[i][j]=0/1 1代表顶点i,j之间是有边 0代表无边
/*树的形状:
5——1——2——3
|
4
*/
int g[N][N] = {
// 0 1 2 3 4 5
/*0*/{0,0,0,0,0,0},
/*1*/{0,0,1,0,1,1},
/*2*/{0,1,0,1,0,0},
/*3*/{0,0,1,0,0,0},
/*4*/{0,1,0,0,0,0},
/*5*/{0,1,0,0,0,0}
};
//练习:找出图中每一个顶点的邻接点
int main() {
//枚举每一个顶点
for (int i = 1; i <= 5; i++) {
//找出顶点i的邻接点j
cout << "顶点" << i << "的邻接点有:";
for (int j = 1; j <= 5; j++) {
if (g[i][j] == 1) cout << j << " ";
}cout << endl;
}
}
#include
#include
using namespace std;
const int N= 1e2;
int g1[N][N] = {
//0 1 2 3 4 5
/*0*/{0,0,0,0,0,0},
/*1*/{0,0,1,0,0,1},
/*2*/{0,0,0,0,0,0},
/*3*/{0,0,1,0,0,0},
/*4*/{0,1,0,0,0,0},
/*5*/{0,0,0,0,0,0}
};
//练习:找出图中每一个顶点的邻接点
int main() {
//枚举每一个顶点
for (int i = 1; i <= 5; i++) {
//找出顶点i的邻接点j
cout << "顶点" << i << "的邻接点有:";
for (int j = 1; j <= 5; j++) {
if (g1[i][j] == 1) cout << j << " ";
}cout << endl;
}
}
练习
#include
#include
using namespace std;
int main() {
int n, m; cin >> n >> m;
//连接m条边
for (int i = 1; i <= m; i++) {
int u, v; cin >> u >> v;
/*有向图和无向图的区别*/
g[u][v] = g[v][u] = 1;//注意,无向图需要双向连边
//g[u][v] = 1;//注意,有向图仅需要单向连边
}
for (int i = 1; i <= n; i++) {
cout << "顶点" << i << "的邻接点有:";
for (int j = 1; j <= n; j++) {
if (g[i][j]) cout << j << " ";
}cout << endl;
}
}
邻接表优缺点:
- 优点:空间复杂度O(m),查找n个顶点的邻接点时间复杂度O(n+m)
- 缺点:无法快速定位两点之间是否有边,如果要查找邻接点,时间复杂度O(n)邻接表更适合存稀疏图
#include
#include
using namespace std;
/*
给定n(n<=10^4)个顶点m(m<=10^4)条边的无向图,找出每个顶点的邻接点
输入:
n=5 m=4
u=1 v=2
2 3
1 4
1 5
输出
顶点1的邻接点有:...
顶点2的邻接点有:...
...
*/
const int N = 1e4 + 10;
vector g[N] = {
/*0*/{0},
/*1*/{2,4,5},
/*2*/{1,3},
/*3*/{2},
/*4*/{1},
/*5*/{1}
};
int main()
{
for (int i = 1; i <= 5; i++)
{
cout << "顶点" << i << "的邻接点有:";
for (int j = 0; j
#include
#include
using namespace std;
const int N = 1e4 + 10;
int g[N][N];
vector g1[N];
int main() {
//邻接表练习
int n, m; cin >> n >> m;
//连接m条边
for (int i = 1; i <= m; i++) {
int u, v; cin >> u >> v;
/*有向和无向的区别*/
g1[u].push_back(v); g1[v].push_back(u);//注意,无向图需要双向连边
//g1[u].push_back(v); //注意,有向图仅需要单向连边
}
for (int i = 1; i <= n; i++) {
cout << "顶点" << i << "的邻接点有:";
for (int j = 0; j < g1[i].size(); j++) {
cout << g1[i][j] << " ";
}cout << endl;
}
return 0;
}
图论基础/核心算法
- 深度优先搜索 dfs -> 搜索与回溯算法 -> 剪枝、迭代加深搜索、Tarjan、树形dp、二分图最大匹配问题-匈牙利算法
- 广度优先搜索 bfs
#include
#include
using namespace std;
/*
input:
8 7
1 2
2 3
3 5
1 7
7 6
1 8
8 4
1
output:
18476235
*/
const int N = 1e4 + 10;
int g[N][N];//邻接矩阵
bool vis[N];//标记数组
int n, m;//点数 边数
void dfs(int s)
{
stackstk;
//dfs1.起点入栈,入栈即标记--防止重复搜索
stk.push(s); vis[s] = 1;
//结束时机--栈为空
while (!stk.empty())
{
//dfs2.取出栈顶元素
int cur = stk.top(); stk.pop();
cout << cur << " ";//输出深搜结果
//dfs3.循环遍历所有点,找到当前节点未被标记的邻接点继续深搜
for (int i = 1; i <= n; i++)
{
if (!vis[i] && g[cur][i])
//沿着邻接点继续深搜
stk.push(i), vis[i] = 1;
}
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int u, v; cin >> u >> v;
g[u][v] = g[v][u] = 1;//无向图 双向连边
}
int s; cin >> s;//深搜起点
dfs(s);
return 0;
}
模拟栈实现过程:
模拟过程
结果是:15847236
递归特点:代码简洁、过程复杂(所以前期必须重复模拟递归过程,熟练掌握递归的机制,方便后续学习搜索与回溯等相关算法)
/*
input:
8 7
1 2
2 3
3 5
1 7
7 6
1 8
8 4
1
output:
1 2 3 5 7 6 8 4
*/
#include
using namespace std;
const int N = 1e4 + 10;
int g[N][N];
bool vis[N];
int n, m;//点数 边数
/*
模拟递归过程:
1.dfs(1) cout<<1 i=2 vis[2]=1 dfs(2) i=7 dfs(7) vis[7]=1 i=8 vis[8 =1 dfs(8) 结束
2.dfs(2) cout<<2 i=3 vis[3]=1 dfs(3) 结束
3.dfs(3) cout<<3 i=5 vis[5]=1 dfs(5) 结束
4.dfs(5) cout<<5 结束返回dfs(3)
2.dfs(7) cout<<7 i=6 vis[6]=1 dfs(6) 结束
3.dfs(6) cout<<6 结束
2.dfs(8) cout<<8 i=4 vis[4]=1 dfs(4) 结束
3.dfs(4) cout<<4 结束
*/
/******向下的过程为搜索,逐层的返回的过程即回溯******/
//dfs(int status)--status称搜索状态
void dfs(int s)//正在搜索状态s
{
cout << s << " ";
//沿着s的邻接点继续深搜
for (int i = 1; i <= n; i++)
{
//如果i未被标记且是邻接点,继续深搜i
if (!vis[i] && g[s][i])
{
vis[i]=1;
dfs(i);
}
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int u, v; cin >> u >> v;
g[u][v] = g[v][u] = 1;//无向图 双向连边
}
int s; cin >> s;//深搜起点
vis[s] = 1;//注意先标记
dfs(s);//称初始状态
return 0;
}
#include
#include
using namespace std;
const int N = 1e4 + 10;
vector g[N];
bool vis[N];
int n, m;//点数 边数
//dfs(int status)--status称搜索状态
void dfs(int s)//正在搜索状态s
{
cout << s << " ";
//沿着s的邻接点继续深搜
/*****注意:vector从0开始的下标表示元素的位置(这里总习惯写成1)*/
for (int i = 0; i > n >> m;
for (int i = 1; i <= m; i++)
{
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);//无向图 双向连边
}
int s; cin >> s;//深搜起点
vis[s] = 1;//注意先标记
dfs(s);//称初始状态
return 0;
}
为什么递归深搜结果是字典序正序深搜 因为遇到谁就搜谁
而栈由于是后进先出的缘故所以搜索结果是字典序逆序深搜
/*
input:
8 7
1 2
2 3
3 5
1 7
7 6
1 8
8 4
1
output:
1 2 3 5 7 6 8 4
*/
#include
#include
using namespace std;
/*
input:
8 5
1 2
2 3
4 5
6 7
6 8
output:
3
*/
const int N = 1e4 + 10;
int g[N][N];//邻接矩阵
bool vis[N];//标记数组
int n, m;
//dfs(int status-搜索状态)
void dfs(int s) {//正在搜索状态s
/*cout << s << " ";*/
//沿着s的邻接点继续深搜
for (int i = 1; i <= n; i++)
//如果i没有标记过且是s的邻接点,则继续深搜i
if (!vis[i] && g[s][i])
vis[i] = 1, dfs(i);
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v; cin >> u >> v;
g[u][v] = g[v][u] = 1;//注意,无向图双向连边
}
int cnt = 0;
for (int i = 1; i <= n; i++)
{
//未被标记过的点i一定是连通块的起点
if (!vis[i])
{
vis[i] = 1;
cnt++;//连通块计数++
dfs(i);//从i开始深搜
}
}
cout << cnt << endl;
return 0;
}
搜索与回溯求无权图的最短路-时间复杂度O(2^n)
#include
#include
using namespace std;
/*
input:
19 11
1 2
2 3
3 4
4 7
7 8
1 10
10 5
5 6
1 19
19 6
6 8
1 8
output:
3
*/
const int N = 1e4 + 10;
int g[N][N];//邻接矩阵
bool vis[N];//标记数组
int s, t;//s是起点,t是终点
int n, m;
int ans = 0x3f3f3f3f;
/******向下的过程为搜索,逐层的返回的过程即回溯******/
void dfs(int s,int depth)//正在搜索状态s 搜索深度depth
{
if (s == t)
{
ans = min(ans,depth-1);
return;
}
//沿着s的邻接点继续深搜
for (int i = 1; i <= n; i++)
{
//如果i未被标记且是邻接点,继续深搜i
if (!vis[i] && g[s][i])
{
vis[i] = 1;
dfs(i,depth+1);
vis[i] = 0;
}
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int u, v; cin >> u >> v;
g[u][v] = g[v][u] = 1;//无向图 双向连边
}
cin >> s >> t;
vis[s] = 1;
dfs(s,1);//称初始状态
cout << ans << endl;
return 0;
}
搜索与回溯常见时间复杂度:
- 组合问题是2^n,此时n最多是25,这样算法不会超时
- 排列问题是n!,此时n最多是11,这样算法不会超时
模拟递归过程
/*--数字代表层数/深度
1.dfs(1) i=2 vis[2]=1 dfs(2) vis[2]=0 i=5 vis[5]=1 dfs(5) vis[5]=0 结束
2.dfs(2) i=3 vis[3]=1 dfs(3) vis[3]=0 结束
3.dfs(3) i=4 vis[4]=1 dfs(4) vis[4]=0 结束
4.dfs(4) ans=4-1=3 结束 回退到调用他的函数
2.dfs(5) i=4 vis[4]=1 dfs(4) vis[4]=0 结束
3.dfs(4) ans=3-1=2 结束
*/