【C++】图论基础总结(图的遍历+最短路径算法)


图论在信息学竞赛中占了很大部分,很多实际问题可以用图论来解决。
回顾三种数据结构模型:线性表、树、图。

定义

  • 什么叫图论?

  • 研究图的问题一门高深的学科。

  • 什么是图?

  • 就是由点和线组成的图形
    G=<V,E>
    G=graph V=vertex E=edge

图的描述:

来自百度地图,侵删
(上图来自百度地图,侵删)

图的表示:

来自维基百科,侵删在这里插入图片描述
(图片来自网络,侵删)

分类

  • 有向图和无向图
    有向图就是边有方向的
    无向图就是可以两边走的
  • 混合图: 既有有向又有无向边
  • 简单图:没有重边和自环的图
  • 完全图:任意两个点直接都有一条边
    比如n个点完全图有C(n,2)条边
    一个n阶的完全无向图含有n*(n-1)/2条边;
    一个n阶的完全有向图含有n*(n-1)条边;
  • 稀疏图和稠密图
    稀疏图: 边的数量相对点来说很少
    稠密图 : 边的数量接近于完全图
  • 连通图
    连通图: 在无向图如果任意两个之间都可以相互到达,就是连通图。
    n个点连通图最少需要n-1条边。
    强连通图: 在有向图里面,任意两点可以相互到达,就是强连通。
    n个点的强连通图最少需要n条。
    弱连通图:有向图中,任意两个点,至少有一个点可以到达另外一个点。

阶和度

一个图的阶是指图中顶点的个数。
如果顶点A和B之间有一条边相连,则称A和B是关联的。

顶点的度: 和点相连的边的数量。
有向图的度可以分为入度和出度,A的入度1,A的出度2,A度3。
在这里插入图片描述
定理:1 任何一个图里面顶点的度之和一定是边的数量的2倍
2 有向图中所有顶点的入度之和是等于所有顶点的出度之和。
3 任意一个无向图一定有偶数个奇点.

例题1:

  • 一个无向图有16条边(每个点的度至少是2),其中4个度为3,3个度为4,求这个无向图最多有几个点 ?(2003年普及组问题求解)
  • 答案是11

例题2:

  • 一个无向图有4个结点,其中3个的度数为2,3,3,则第4个结点的度数不可能是___________
    A. 0 B. 1 C. 2 D. 4
  • 答案是B

例题3:

  • 假设我们用d=(a1,a2,….a5)表示无向无自环图G的5个顶点的度数,下面给出的哪组值是可能的?
    A.{3,4,4,3,1}
    B.{4,2,2,1,1}
    C.{3,3,3,2,2}
    D.{3,4,3,2,1}
  • 答案是B

另外,树一个特殊的图,n个点,n-1条边。

图里面边的存储方法

一 相邻矩阵

int a[10][10];
用a[i][j]>0 表示i到j有边
a[i][j]==k 表示i到j的边长

优点:写法简单,能在O(1)得出任意两个点是否有边和边长。
缺点:在稀疏图的时候空间浪费太大,找和i相邻的点需要O(n)的时间。

二 数组模拟邻接表(边表)

int a[10][10];
用a[i][0]表示和i相连的点有几个
a[i][j]表示和i相连的第j个点的编号。

读入:

while(m--) {
	int x,y;
	cin >> x >> y;
	a[x][++a[x][0]]=y;
	a[y][++a[y][0]]=x;
}

查找于x相连的点:

for(int i=1; i<=a[x][0]; i++) cout<<a[x][i];

优点: 查询与x相邻的点时间复杂是O(k) 。(k是相邻的点的数量)
缺点: 空间还是需要很大,需要在O(k)时间知道i和j是否有边。

三 利用stl标准模板库里的动态数组vector(前向星)

定义:

vector<int> a;           //a一维数组动态数组
vector<vector<int> > a;  //a二维数组
vector<int> a[100];      //定义了100个一维

注意:第二种定义方法中>和>间必须加空格,否则会编译错误

使用如果a数组拥有第i个元素,那么直接可以用a[i]表示第i个数,注意a数组从0开始

存放(把x放到a数组的最后):

a.push_back(x);   //把x放到a数组的最后

查找所有和x相邻的点:

for(int i=0; i<a[x].size(); i++) cout << a[x][i];

整合:

int n,m;
vector<int> edge[N];

void init() {
    cin >> n >> m;
    for(int i=0; i<m; i++) {
        int x,y;
        cin >> x >> y;         //边连接的两个顶点
        edge[x].push_back(y);  //添边x->y
        edge[y].push_back(x);  //添边y->x
    }
}

优点 :节省空间,找x相邻的需要O(k)的复杂度
缺点: 判断i和j是否有边需要O(k),比自己写的邻接表要慢一些。

四 前向星邻接表(链式前向星)

定义:

struct edge{
	int to,nt;     //to是边的终点,nt(next)是下一条边的序号
} e[边的数量];

int h[N],cnt;      // h[i]表示i的第一条在e里面序号,cnt是边的总数

建边:

inline void add(int a,int b){
	e[++cnt].to=b; 
	e[cnt].nt=h[a]; 
	h[a]=cnt;
} 

读入:

while(m--) {
	int x,y;
	scanf("%d%d",&x,&y);
	add(x,y);
	add(y,x);
}

枚举与x的相邻的所有点:

for(int i=h[x]; i; i=e[i].nt) cout<<e[i].to;

整合:

struct edge{
	int to,nt; 
} e[边的数量];

int h[N],cnt;

void add(int a,int b) {
	e[++cnt].to=b;
	e[cnt].nt=h[a];
	h[a]=cnt;
}

int main() {
	while (m--) {
		int x,y;
		scanf(%d%d”,&x,&y);
		add(x,y);
		add(y,x);
	}
	for(int i=h[x]; i; i=e[i].nt) cout << e[i].to;
}

图的遍历问题

图的遍历问题是搜索图。
图的搜索分为深度优先搜索和宽度优先搜索两种方法。

深度优先搜索

图的深度优先遍历类似于树的先序遍历。从图中某个顶点Vi出发, 访问此顶点并作已访问标记,然后从Vi的一个未被访问过的邻接点Vj出发再进行深度优先遍历,当Vi的所有邻接点都被访问过时,则退回到上一个顶点Vk,再从Vk的另一个未被访问过的邻接点出发进行深度优先遍历,直至图中所有顶点都被访问到为止。

对下图进行深度优先搜索,写出搜索结果。注意:从A出发。
在这里插入图片描述
从顶点A出发,进行深度优先搜索的结果为:A,B,C,D,E。

对于一个连通图,深度优先遍历的递归过程如下:

void dfs(int i) { //图用邻接矩阵存储
	//访问顶点i;
	visited[i]=1;
	for(int j=1; j<=n; j++)
		if(!visited[j] && a[i][j]) dfs(j)}

以上dfs(i)的时间复杂度为O(n^2)。
对于一个非连通图,调用一次dfs(i),即按深度优先顺序依次访问了顶点i所在的(强)连通分支,所以只要在主程序中加上:

for(int i=1; i<=n; i++)   //深度优先搜索每一个未被访问过的顶点
	if(!visited[i]) dfs(i); 

广度优先搜索(宽度优先搜索)

类似于树的按层次遍历。从图中某个顶点V0出发,访问此顶点,然后依次访问与V0邻接的、未被访问过的所有顶点,然后再分别从这些顶点出发进行广度优先遍历,直到图中所有被访问过的顶点的相邻顶点都被访问到。若此时图中还有顶点尚未被访问,则另选图中一个未被访问过的顶点作为起点,重复上述过程,直到图中所有顶点都被访问到为止。

对下图从A出发进行宽度优先搜索,写出搜索结果。
在这里插入图片描述
从顶点A出发,进行宽度优先遍历的结果为: A,B,C,D,E 。

void bfs(int i) { //宽度优先遍历,图用邻接矩阵表示
	queue<int> q;
	i=q.pop();
	visited[i]=true;
	q.push(i);
	while(!q.empty()) {
		v=q.front();
		q.pop();
		for(int j=1; j<=n; j++) {
			if(!visited[j]) {
  				visited[j]=1;
				q.push(j);
			}
		}
	}
}

时间复杂度是O(n^2).
BFS与DFS的总结:

  • DFS:类似回溯,利用堆栈进行搜索
    BFS:类似树的层次遍历,利用队列进行搜索
  • DFS:尽可能地走“顶点表”
    BFS:尽可能地沿着顶点的“边”进行访问
  • DFS:容易记录访问过的路径
    BFS:不易记录访问过的路径,需要开辟另外的存储空间进行保存路径

图的最短路径算法

分类:

  • 多源最短路径算法:求任意两点之间的最短距离。
    Floyd算法
  • 单源最短路径算法:求一个点到其他所有点的最短路径
    Dijkstra算法,Spfa算法,Bellman-ford算法

Floyd算法

时间复杂度O(n^3)
本质上是一个动态规划
f[i][j]表示i到j最短路径长度
开始的时候,如果i到j有边,那么f[i][j]就是直接的边长,如果没边,f[i][j]就是无穷大。

for(int k=1; k<=n; k++) 
	for(int i=1; i<=n; i++) 
		for(int j=1; j<=n; j++)
			f[i][j]=min(f[i][j],f[i][k]+f[k][j]); 

为什么k循环要写在最外面?

这个状态数组本来是3维的。
f[i][j][0]表示i到j的最短路径中间经过了0个点。
f[i][j][0]=edge[i][j] edge[i][j]表示i到j直接的边长
如果i到j边不存在,f[i][j][0]=无穷大。
f[i][j][k]表示i到j的最短路径中间最多经过了1到k这些点
答案就是f[i][j][n]
f[i][j][k]=min(一定没有经过k,一定经过k)
=min(f[i][j][k-1],f[i][k][k-1] + f[k][j][k-1])
把一维舍掉:
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);

Dijkstra算法

思想 :贪心的思想
步骤:
1 标记所有的点都没有求得最短路径,所有的d[i]=无穷大,除了起点的d值是0。
2 循环n次,每次从没有求得最短路径的点里面找出一个d值最小的点,把他标记,用这个点去更新其他没有求得最短路径的点。

void dijkstra() {
	memset(vis,0,sizeof(vis));
	for(int i=1;i<=n;i++) d[i]=inf;
	d[s]=0;
	for(int i=1;i<=n;i++) {
		int k=-1;
		for(int j=1;j<=n;j++)
			if(!vis[j] && (k==-1 || d[k]>d[j])) k=j;
		vis[k]=1;
		for(int j=1;j<=n;j++)
			if(!vis[j] && d[k]+edge[k][j]<d[j])
				d[j]=d[k]+edge[k][j];
	}
}

Spfa算法

设dist代表s到i点的当前最短距离,fa代表s到i的当前最短路径中i点之前的一个点的编号。开始时dist全部为+∞,只有dist[s]=0,fa全部为0。
维护一个队列,里面存放所有需要进行迭代的点。初始时队列中只有一个点S。用一个布尔数组记录每个点是否处在队列中。
每次迭代,取出队头的点v,依次枚举从v出发的边v->u,设边的长度为len,判断dist[v]+len是否小于dist[u],若小于则改进dist[u],将fa[u]记为v,并且由于s到u的最短距离变小了,有可能u可以改进其它的点,所以若u不在队列中,就将它放入队尾。这样一直迭代下去直到队列变空,也就是S到所有的最短距离都确定下来,结束算法。

int const oo=1e9;
vector<int> a[N],b[N];
queue<int> q;
int s,t;
int v[N],d[N];

int spfa() {
	for(int i=1; i<=n; i++) d[i]=oo;
	q.push(s);
	v[s]=1;
	d[s]=0;
	while(!q.empty()) {
		int x=q.front();
		q.pop();
		v[x]=0;
		for(int i=0; i<a[x].size(); i++) {
			int tp=a[x][i];
			if(d[tp]>d[x]+b[x][i]) {
				d[tp]=d[x]+b[x][i];
				if(!v[tp]) {
					q.push(tp);
					v[tp]=1;
				}
			}
		}
	}
	if(d[t]==oo) d[t]=-1;
	return d[t];
}

最短路径模板题题答

最短路径模板题题答

  • 11
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
抱歉,我是一个自然语言处理模型,无法编写完整的代码。但我可以为您提供伪代码和思路。 1. 的深度优先遍历和广度优先遍历 ```c++ // 的深度优先遍历 void DFS(int v, vector<bool>& visited, vector<vector<int>>& adj_list) { visited[v] = true; cout << v << " "; for (auto i : adj_list[v]) { if (!visited[i]) { DFS(i, visited, adj_list); } } } // 的广度优先遍历 void BFS(int v, vector<bool>& visited, vector<vector<int>>& adj_list) { queue<int> q; visited[v] = true; q.push(v); while (!q.empty()) { int curr = q.front(); cout << curr << " "; q.pop(); for (auto i : adj_list[curr]) { if (!visited[i]) { visited[i] = true; q.push(i); } } } } ``` 2. 最短路径算法 最短路径算法主要有 Dijkstra 算法和 Bellman-Ford 算法。这里提供 Dijkstra 算法的实现思路: ```c++ // 带权有向的Dijkstra算法 void Dijkstra(int src, vector<vector<pair<int, int>>>& adj_list, vector<int>& dist) { int V = adj_list.size(); vector<bool> visited(V, false); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; // 将源点加入到优先队列中 pq.push(make_pair(0, src)); dist[src] = 0; while (!pq.empty()) { int u = pq.top().second; pq.pop(); if (visited[u]) { continue; } visited[u] = true; for (auto i : adj_list[u]) { int v = i.first; int weight = i.second; if (dist[v] > dist[u] + weight) { dist[v] = dist[u] + weight; pq.push(make_pair(dist[v], v)); } } } } ``` 以上是伪代码和思路,您可以根据自己的需求进行具体实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值