蓝桥杯必掌握知识点之图论(持续更新...)

目录

基本概念

存储方式

1. 邻接矩阵(存储邻接点的矩阵)

a. 无向无权图

b. 有向无权图

2. 邻接表

a. 无向无权图

b. 有向无权图

深度优先搜索(算法)

1. 栈实现(邻接矩阵)

2. 递归实现

a. 邻接矩阵

b. 邻接表

3. 连通块问题(邻接矩阵)

4. 无权图最短路问题


基本概念

        1.生活中的图:交通路线图、电路图、网络拓扑图...

        2.数据结构中的图:

  • 图的定义:图是由一些顶点V和连线E构成的集合,记为G=(V,E)

        解决图论问题对现实生活中的实际图论问题具有重大的意义

  • 图的基本概念

        邻接/邻居点:一无向条边上的两个顶点互为邻接点,一条有向边如i->j,则j是i的邻接点,但i不是j的邻接点

        路径:一个顶点到达另一个顶点经过的顶点序列

  • 图的分类:
  1. 无向图:边没有方向,一条无向边上两个顶点可以互相到达
  2.  有向图:边是有方向的,一个有向边只能单向通过
  3. 无权图:边没有实际的权重,一般情况下,顶点之间有边置1,无边置0
  4. 有权图:边有实际的权重(比如:距离)
  5. 完全图:任意两个顶点之间都有连边,n个定点构成的完全图,有n*(n-1)/2
  6. 稀疏图:比如n个顶点m条边构成图,其中m远小于n
  7. 稠密图:比如n个顶点m条边构成图,其中m远大于n,最高达到完全图的边数
  8. 连通图:对于无向图,图中任意两个顶点都是连通(有路径)的,则称为为连通图
  9. 强连通图:对于有向图,图中任意两个顶点都是连通(有路径)的,则称为强连通图

存储方式

现阶段常用的图的存储即邻接矩阵和邻接表

  • 1.邻接矩阵
  • 2.邻接表

后期主要用链式前向星

  • 3.链式前向星

邻接多重表、十字链表算法竞赛不用

  • 4.邻接多重表
  • 5.十字链表

什么是邻接矩阵?什么是邻接表?见下图 

蓝桥杯必掌握知识点之图论(持续更新...)_第1张图片

1. 邻接矩阵(存储邻接点的矩阵)

邻接矩阵优缺点:

  • 优点:可以快速定位邻接点,时间复杂度为o(1)(简单易学,容易理解,新手入门图论必备)
  • 缺点:空间消耗太大,空间复杂度为o(n^2),遍历n个顶点的邻接点的时间复杂度是o(n^2),其中n为点数
  • 邻接矩阵更适合于存储稠密图,不适合存稀疏图
a. 无向无权图
#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;
    }
}
b. 有向无权图
#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;
	}
}
2. 邻接表

邻接表优缺点:

  • 优点:空间复杂度O(m),查找n个顶点的邻接点时间复杂度O(n+m)
  • 缺点:无法快速定位两点之间是否有边,如果要查找邻接点,时间复杂度O(n)邻接表更适合存稀疏图
a. 无向无权图
#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 
b. 有向无权图
#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;
}

深度优先搜索(算法)

图论基础/核心算法

  1. 深度优先搜索 dfs -> 搜索与回溯算法 -> 剪枝、迭代加深搜索、Tarjan、树形dp、二分图最大匹配问题-匈牙利算法
  2. 广度优先搜索 bfs
1. 栈实现(邻接矩阵)
#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;
}

模拟栈实现过程:

蓝桥杯必掌握知识点之图论(持续更新...)_第2张图片

模拟过程

蓝桥杯必掌握知识点之图论(持续更新...)_第3张图片

结果是:15847236

2. 递归实现
a. 邻接矩阵

递归特点:代码简洁、过程复杂(所以前期必须重复模拟递归过程,熟练掌握递归的机制,方便后续学习搜索与回溯等相关算法)

/*
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;
}
b. 邻接表
#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;
}

为什么递归深搜结果是字典序正序深搜 因为遇到谁就搜谁

而栈由于是后进先出的缘故所以搜索结果是字典序逆序深搜

3. 连通块问题(邻接矩阵)
/*
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;
}
4. 无权图最短路问题

搜索与回溯求无权图的最短路-时间复杂度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,这样算法不会超时

蓝桥杯必掌握知识点之图论(持续更新...)_第4张图片

模拟递归过程

/*--数字代表层数/深度

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 结束

*/

你可能感兴趣的:(图论,算法)