1.图的存储结构
- 邻接矩阵
使用二维数组来存储图的边的信息和权重,如下图所示的4个顶点的无向图:
从上面可以看出,无向图的边数组是一个对称矩阵。所谓对称矩阵就是n阶矩阵的元满足aij = aji。即从矩阵的左上角到右下角的主对角线为轴,右上角的元和左下角相对应的元全都是相等的。
如果换成有向图,则如图所示的五个顶点的有向图的邻接矩阵表示如下:
- 邻接表
邻接矩阵是一种不错的图存储结构,但是对于边数较少时,这种结构存在空间上的极大浪费,因此找到一种数组与链表相结合的存储方法称为邻接表。
邻接表的处理方法是这样的:
(1)图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过,数组可以较容易的读取顶点的信息,更加方便。
(2)图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以,用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表
无向图的邻接表表示:
从图中可以看出,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。
有向图的邻接表表示:
2.图的遍历
- 深度优先遍历(DFS)
堆栈的思想,对于不能访问后,一定要原路返回;
基本实现思想(类似树的先序遍历):
(1)访问顶点v;
(2)从v的未被访问的邻接点中选取一个顶点w,从w出发进行深度优先遍历;
(3)重复上述两步,直至图中所有和v有路径相通的顶点都被访问到。
伪代码表示:
(1)访问顶点v;visited[v]=1;//算法执行前visited[n]=0
(2)w=顶点v的第一个邻接点;
(3)while(w存在)
if(w未被访问)
从顶点w出发递归执行该算法;
w=顶点v的下一个邻接点;
- 广度优先遍历(BFS)
队列的思想,入队,出队;
基本实现思想(类似树的层序遍历):
(1)顶点v入队列。
(2)当队列非空时则继续执行,否则算法结束。
(3)出队列取得队头顶点v;访问顶点v并标记顶点v已被访问。
(4)查找顶点v的第一个邻接顶点col。
(5)若v的邻接顶点col未被访问过的,则col入队列。
(6)继续查找顶点v的另一个新的邻接顶点col,转到步骤(5)。
直到顶点v的所有未被访问过的邻接点处理完。转到步骤(2)。
(1)初始化队列Q;visited[n]=0;
(2)访问顶点v;visited[v]=1;顶点v入队列Q;
(3) while(队列Q非空)
v=队列Q的对头元素出队;
w=顶点v的第一个邻接点;
while(w存在)
如果w未访问,则访问顶点w;
visited[w]=1;
顶点w入队列Q;
w=顶点v的下一个邻接点。
完整代码:
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
const int N = 10010; // 图的顶点最大个数
int e[N][N]; // 储存图信息的邻接矩阵
int book[N]; // 标记顶点是否被访问
// 对第 n 个顶点进行深度优先遍历
void dfs(int n, int sum) {
if(n != 0) { // 输出格式控制
cout << " ";
}
cout << n+1; // 输出顶点信息
for(int i = 0; i < sum; i++) { // 对当前所有的顶点进行讨论
// 如果顶点 i 和顶点 n 之间存在边直接相连,并且顶点 i 未被访问
if(e[n][i] == 1 && book[i] == 0) {
book[i] = 1; // 标记这个顶点已经被访问
dfs(i, sum); // 对这个顶点继续进行深度优先遍历
}
}
}
// 对图进行广度优先遍历
void bfs(int n) {
queue<int> que;
book[0] = 1; // 标记第一个顶点已经被访问
que.push(0);
int s;
while(!que.empty()) {
s = que.front(); // 获取队头元素
que.pop(); // 队头元素出队
if(s != 0) { // 输出格式控制
cout << " ";
}
cout << s+1; // 输出顶点信息
for(int i = 0; i < n; i++) {
// 如果顶点 i 和顶点 n 之间存在边直接相连,并且顶点 i 未被访问
if(e[s][i] == 1 && book[i] == 0) {
book[i] = 1; // 标记这个顶点已经被访问
que.push(i); // 这个顶点入队尾
}
}
}
}
int main() {
int n, m; // 图的顶点个数和边的条数
cin >> n >> m;
int x, y; // 边的开始顶点和结束顶点
for(int i = 0; i < m; i++) {
cin >> x >> y;
e[--x][--y] = e[y][x] = 1; // 因为是无向图,所以要双向储存
}
cout << "深度优先遍历结果:" << endl;
book[0] = 1; // 标记第一个顶点已经被访问
dfs(0, n); // 从第一个顶点开始深度优先遍历
memset(book, 0, sizeof(book)); // 重置访问标记
cout << endl << "广度优先遍历结果:" << endl;
bfs(n);
return 0;
}
有关DFS与BFS应用模板可参考:https://blog.csdn.net/BillCYJ/article/details/78976932
参考链接:
https://blog.csdn.net/moshenglv/article/details/72954701
https://blog.csdn.net/hacker_zhidian/article/details/61260543
http://blog.51cto.com/ahalei/1387799
https://blog.csdn.net/qq_35644234/article/details/60870719
3.最短路径问题-迪杰斯特算法(Dijkstra)
该算法采用了贪心的思想,每次都查找与该点距离最的点,也因为这样,它不能用来解决存在负权边的图。解决的问题大多是这样的:有一个无向图G(V,E),边E[i]的权值为W[i],找出V[0]到V[i]的最短路径。
算法步骤:
- 引进两个集合S和U。S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。初始时,S只包含起点s;
- 从U中选出”距离最短的顶点k”,并将顶点k加入到S中;同时,从U中移除顶点k。(也可以理解为按最短路径长度的递增次序依次把U中的顶点加入S中);
- 以k为新考虑的中间点,更新U中各顶点到起点s的距离。例如:若从起点s到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值是顶点k的距离加上k到u的距离;
- 重复上述两个步骤,直到所有顶点都包含在S中。
为什么不能解决负权边的图?
当把一个节点选入集合S时,即意味着已经找到了从源点到这个点的最短路径,(在S中以后加入的新顶点不会影响之前顶点到源点的距离值),但若存在负权边,就与这个前提矛盾,可能会出现得出的距离加上负权后比已经得到S中的最短路径还短。
完整代码:
// 邻接矩阵
typedef struct _graph
{
char vexs[MAX]; // 顶点集合
int vexnum; // 顶点数
int edgnum; // 边数
int matrix[MAX][MAX]; // 邻接矩阵
}Graph, *PGraph;
// 边的结构体
typedef struct _EdgeData
{
char start; // 边的起点
char end; // 边的终点
int weight; // 边的权重
}EData;
/*
* Dijkstra最短路径。
* 即,统计图(G)中"顶点vs"到其它各个顶点的最短路径。
*
* 参数说明:
* G -- 图
* vs -- 起始顶点(start vertex)。即计算"顶点vs"到其它顶点的最短路径。
* prev -- 前驱顶点数组。也可理解为路径保存,prev[i]的值是"顶点vs"到"顶点i"的最短路径所经历的全部顶点中,位于"顶点i"之前的那个顶点。
* dist -- 长度数组。即,dist[i]是"顶点vs"到"顶点i"的最短路径的长度。
*/
void dijkstra(Graph G, int vs, int prev[], int dist[])
{
int i,j,k;
int min;
int tmp;
int flag[MAX]; // flag[i]=1表示"顶点vs"到"顶点i"的最短路径已成功获取。
// 初始化
for (i = 0; i < G.vexnum; i++)
{
flag[i] = 0; // 顶点i的最短路径还没获取到。
prev[i] = 0; // 顶点i的前驱顶点为0。
dist[i] = G.matrix[vs][i];// 顶点i的最短路径为"顶点vs"到"顶点i"的权。
}
// 对"顶点vs"自身进行初始化
flag[vs] = 1;
dist[vs] = 0;
// 遍历G.vexnum-1次;每次找出一个顶点的最短路径。
for (i = 1; i < G.vexnum; i++)
{
// 寻找当前最小的路径;
// 即,在未获取最短路径的顶点中,找到离vs最近的顶点(k)。
min = INF;
for (j = 0; j < G.vexnum; j++)
{
if (flag[j]==0 && dist[j]<min)
{
min = dist[j];
k = j;
}
}
// 标记"顶点k"为已经获取到最短路径
flag[k] = 1;
// 修正当前最短路径和前驱顶点
// 即,当已知"顶点k的最短路径"之后,更新"未获取最短路径的顶点的最短路径和前驱顶点"。
for (j = 0; j < G.vexnum; j++)
{
tmp = (G.matrix[k][j]==INF ? INF : (min + G.matrix[k][j])); // 防止溢出
if (flag[j] == 0 && (tmp < dist[j]) ) //判断d(s,k)+d(k,j)与d(s,j)的大小,如果小就更新
{
dist[j] = tmp;
prev[j] = k;
}
}
}
// 打印dijkstra最短路径的结果
printf("dijkstra(%c): \n", G.vexs[vs]);
for (i = 0; i < G.vexnum; i++)
printf(" shortest(%c, %c)=%d\n", G.vexs[vs], G.vexs[i], dist[i]);
}
详细过程可参考:
https://blog.csdn.net/heroacool/article/details/51014824
https://blog.csdn.net/mu399/article/details/50903876
4.最小生成树问题
- 连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
- 生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
- 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
1)克鲁斯卡算法(Kruskal)
此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
1. 把图中的所有边按代价从小到大排序;
2. 把图中的n个顶点看成独立的n棵树组成的森林;
3. 按权值从小到大选择边,所选的边连接的两个顶点ui,viui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
4. 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
2)普里姆算法(Prim)
此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
- 图的所有顶点集合为VV;初始令集合u={s},v=V−uu={s},v=V−u;
- 在两个集合u,vu,v能够组成的边中,选择一条代价最小的边(u0,v0)(u0,v0),加入到最小生成树中,并把v0v0并入到集合u中。
- 重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
由于不断向集合u中加点,所以最小代价边必须同步更新;需要建立一个辅助数组closedge,用来维护集合v中每个顶点与集合u中最小代价边信息,:
struct
{
char vertexData //表示u中顶点信息
UINT lowestcost //最小代价
}closedge[vexCounts]
转载链接:勿在浮沙筑高台http://blog.csdn.net/luoshixian099/article/details/51908175
https://blog.csdn.net/qq_27256783/article/details/78640063