图✨
知识点:
-
图🤩
-
基本概念
-
主要性质
-
1、线形表可以是空表,树可以是空树,但图不能是空图。图中顶点集V一定非空,但边集E可以为空 2、完全图中,无向图n(n-1)/2条边,有向图n(n-1)条边 3、子图中,并非V和E的任何子集都构成G的子图,因为这样的子集可能不是图,即某些边,关联的顶点不在子图中 4、无向图的连通图,有向图的强连通图,不要主观臆断
-
针对2性质:考察需固定多少边,非连通图无向图至少需要多少个顶点,即求出该边数完全图的顶点+1即可; 若一个图有n个顶点,若边数小于n-1,则此图必是非连通图;
-
确保为n个顶点的图为一个连通图,先去保证n-1为强连通图,然后加一条边即必连通了
-
-
针对4性质:如果强连通图,某顶点只有出度,一定为单独一个强连通分量
-
难题,求森林的树的个数,即根的个数,由于每棵树除根节点外的每个节点对应一条边连接双亲节点,即用结点数-边数=根结点数
-
-
-
-
存储结构
-
邻接矩阵
-
主要性质,王道P211,6题
-
-
邻接表
-
主要性质
-
1、若邻接表中有奇数个边表节点,则图为有向图 2、在有向图邻接表中,顶点v在边表出现的次数,等于v的入度
-
-
-
-
图的遍历
-
深度优先遍历
-
类比树
-
类似于树的先根遍历 不同于,有重复访问,需要设置visited[](只要多边就有可能出现多次访问)
-
-
性质
-
形成的深度生成树个数等于连通分量,等于调用DFS的次数
-
具有工作栈的性质,深度为V
-
可以用于判断有向图中是否存在回路(通过每次递归查看栈中是否存在相同元素)
-
-
-
广度优先遍历
-
类比树
-
类似于树的层次遍历 不同于,有重复访问,需要设置visited[](只要多边就有可能出现多次访问)
-
-
性质
-
形成的广度生成树个数等于连通分量,等于调用BFS的次数
-
需要内存为V的辅助队列
-
当各边的权值相等时,可以解决单源最短路径问题
-
-
-
-
图的应用
-
MST最小生成树
-
prim算法
-
性质
-
1、只能使用带权无向图,而带权有向图由于两个节点之间来和回的权重不一样,无法确定
-
-
思路
-
1、从一个点出发,寻找一颗权值和最小的树,即每次选者权值最小的边,直至所有的顶点都已经加入
-
-
-
Kruskal算法
-
思路
-
1、每次从最小边出发,所有顶点不形成回路
-
-
-
性质⚠️
-
1、只要无向连通图中没有权值相同的边,则其最小生成树唯一 2、若最小生成树不唯一,一定存在权值相等的边,但未必是权值最小的边(设想图有n-1条边且权值最小,即唯一),逆命题:如果存在权值相等的边,最小生成树不一定不唯一(即n-1条相等的边,MST唯一) 3、MST的点n-1条边不能保证是图中的权值最小的n-1条边,因为该n-1条边未必能使得图连通
-
-
-
最短路径
-
Dijkstra
-
prime算法的比较:都是基于贪心算法 。得到的最小生成树,从某点出发到达另外一点的路径并不是最短的
-
思路
-
每次找出到原点距离最近且未加入集合的点,把它归入集合,同时以这个点为基础更新从源点到其他各点的距离
-
-
性质
-
1、适用于带权有向图,带权无向图 2、单源最短路径,(扩展:访问每个顶点即可得到每队顶点的路径长度,时间复杂度为v的3次方)
-
-
-
Floyd
-
每次更新数组
-
可求带权图各顶点的最短路径长度,时间复杂度v的3次方
-
-
-
BFS
-
单源最短路径
-
-
-
关键路径
-
AOE网中的边有权值
-
性质
-
1、由于关键路径运行前提,必须是无环图,并且在求关键路径的第一步,是拓扑排序,即可以判断是否存在环路(存在争议) 2、是从源点到汇点的路径长度最长的路径
-
-
注意⚠️
-
掌握步骤,然后认真算,可能出现多条关键路径
-
-
-
有向无环图(DAG)
-
描述含有公共子式的表达式
-
在表达式的有向无环图表示中,不可能出现重复的操作数顶点
-
-
-
拓扑序列
-
概念
-
1、从AOV网中选者一个没有前驱的顶点(入度为0)输出。(生成的序列不唯一) 2、从网中删除该顶点和所有以它为起点的有向边 3、重复1、2,直到当前AOV网为空或当前的网中不存在无前驱的顶点为止。后一种情况说明图中有环。
-
-
注意⚠️
-
可以使用DFS实现逆拓扑排序 P258,7题
-
1、强连通图一定存在环,不能拓扑排序 2、有向图顶点不能排成一个拓扑排序,可以判断这个有向图,含有顶点数大于1的强连通分量
-
有向无环图的拓扑排序唯一,并不能确定该图唯一
-
使用邻接矩阵存储,上(下)三角矩阵,证明该拓扑序列存在,不一定唯一
-
-
-
-
Q&A:
1、设计一个算法计算邻接表表示的图中各个顶点的出度
//利用一个数组对应各个结点,保存出度的个数
void OutDegree(int *outdegree,graph *g){
int i;
ENode *p
for(i=0;i<g->n;i++){
outdegree[i]=0; //初始化
}
for(i=0;i<g->n;i++){
for(p=g->a[i];p;p=p->nextArc)
outdegree[i]++;
}
2、设计一个算法计算邻接表表示的图中各个顶点的入度
void InDegree(int *inDegree,LGraph *g)
{
int i;
ENode *p;
for(i=0;i<g->n;i++){
inDegree[i]=0;
}
for(i=0;i<g->n;i++)
for(p=g->a[i];p;p=p->nextArc)
inDegree[p->adjVex]++;
}
3、设计一个算法计算邻接表表示的图中任意顶点u的入度
int InDegree(int u,LGraph *g)
{
int i;
ENode *p;
int count;
if(u<0||u>g->n-1) return -1;
for(i=0;i<g->n;i++){
for(p=g->a[i];p;p=p->nextArc){
if(p->adjVex==u)
count++;
}
}
return count;
}
4、设计一个算法计算邻接表表示的图中任意顶点u的出度
struct ENode {
int adjVex; // 指向邻接点的编号
ENode* nextArc; // 指向下一个邻接点
};
struct LGraph {
int n; // 图的顶点数
ENode* a[]; // 邻接表数组
};
int OutDegree(int u, LGraph *g) {
if (u < 0 || u >= g->n-1) {
return -1; // 错误的顶点编号
}
int count = 0;
ENode* p = g->a[u];
while (p != null) {
count++; //遍历该数组,即是特定顶点的出度
p = p->nextArc;
}
return count;
}
5、设图G以邻接矩阵表示,设计一个算法根据图G的邻接矩阵构建图G的邻接表
// 向邻接表中添加边
void AddEdge(GraphAdjList *G, int i, int j) {
EdgeNode *e = (EdgeNode *)malloc(sizeof(EdgeNode));
e->adjvex = j;
e->next = G->adjList[i].firstedge; //头插法
G->adjList[i].firstedge = e;
// 注意:如果是无向图,还需要添加G->adjList[j].firstedge到i的边
// 这里只处理有向图或单向邻接表的情况
//e->adjvex = i;
//e->next = G->adjList[j].firstedge;
//G->adjList[j].firstedge = e;
}
// 根据邻接矩阵构建邻接表
void BuildAdjList(GraphAdjList *G, int adjMatrix[100][100], int numVertices) {
InitGraph(G, numVertices);
for (int i = 0; i < numVertices; i++) {
for (int j = 0; j < numVertices; j++) {
if (adjMatrix[i][j] == 1) { // 假设1表示存在边
AddEdge(G, i, j);
// 如果是无向图,还需要AddEdge(G, j, i);
}
}
}
}
6、设图G以邻接表表示,设计一个算法根据图G的邻接表构建图G的邻接矩阵
void BuildAdjMatrix(EdgeNode adjList[][MAX_VERTICES], int numVertices, int adjMatrix[MAX_VERTICES][MAX_VERTICES]) {
for (int i = 0; i < numVertices; i++) {
for (int j = 0; j < numVertices; j++) {
adjMatrix[i][j] = 0; // 初始化邻接矩阵
}
}
for (int i = 0; i < numVertices; i++) {
EdgeNode *p = adjList[i]; // 获取顶点i的边表头指针
while (p != NULL) {
adjMatrix[i][p->adjvex] = 1; // 标记边i->p->adjvex
// 对于无向图,还需要执行:adjMatrix[p->adjvex][i] = 1;
p = p->next; // 移动到下一个邻接点
}
}
}
7、设计一个算法求给定无向图的全部连通分量
void findConnectedComponents(Graph *graph) {
int *visited = (int*)malloc(graph->numVertices, sizeof(int));
if (!visited) return; //申请数组出错,直接返回
for (int i = 0; i < graph->numVertices; i++) {
if (!visited[i]) {
printf("Connected component: "); //输出连通分量
DFS(graph, i, visited); //DFS调用
for (int j = 0; j < graph->numVertices; j++) {
if (visited[j]) {
printf("%d ", j); //在遍历一次输出一趟DFS后visited为true的数
visited[j] = 0; //重置visited数组,以便复用(可选,本就从未访问的循环)
}
}
printf("\n");
}
}
free(visited);
}
8、设图G以邻接矩阵表示,设计一个算法实现对图G的深度优先遍历
void DFS1(Graph *graph, int vertex, int *visited) {
visited[vertex] = 1;
EdgeNode *temp = graph->adjLists[vertex];
while (temp != NULL) { //不等于NULL即有边
int neighbor = temp->vertex;
// 如果该邻接顶点没有被访问过,则递归进行DFS
if (!visited[neighbor]) {
DFS(graph, neighbor, visited);
}
// 移动到下一个邻接顶点
temp = temp->next;
}
} //为邻接表存储结构
void DFS2(int G[MaxVertices][MaxVertices], int visited[], int vertex, int numVertices) {
// 访问当前顶点并标记为已访问
visited[vertex] = 1;
// 遍历与当前顶点相邻的所有顶点
for (int i = 0; i < numVertices; i++) {
// 如果存在一条边并且邻接顶点还没有被访问
if (G[vertex][i] == 1 && !visited[i]) {
DFS(G, visited, i, numVertices); // 递归调用 DFS 访问该顶点
}
}
}
9、设图G以邻接矩阵表示,设计一个算法实现对图G的宽度优先遍历
void BFS(int adjMatrix[][100], int startVertex, int numVertices) {
// visited 数组,用于标记顶点是否已经访问过
int visited[100] = {0}; // 假设最多有100个顶点,也可以动态分配
queue q; // 定义队列,存放待访问的顶点
// 标记起始顶点已访问,并将其加入队列
visited[startVertex] = 1;
q.Enqueue(startVertex);
// 当队列不为空时,继续进行广度优先搜索
while (!q.empty()) {
// 取出队列头的顶点进行处理
int currentVertex = q.Dequeue();
// 遍历当前顶点的所有邻接顶点
for (int i = 0; i < numVertices; ++i) {
// 如果有一条边连接当前顶点和顶点 i,并且 i 还没有被访问过
if (adjMatrix[currentVertex][i] && !visited[i]) {
visited[i] = 1; // 标记顶点 i 为已访问
q.Enqueue(i); // 将顶点 i 加入队列
}
}
}
}
10、设带权无向图G以邻接矩阵表示,设计一个算法实现Prim算法
#include <iostream>
#include <climits> // for INT_MAX
using namespace std;
const int MaxVertices = 100; // 假设图的最大顶点数为 100
// 查找未访问顶点中,离已生成树最近的顶点
int findMinVertex(int key[], bool mstSet[], int numVertices) {
int minKey = INT_MAX, minVertex = -1;
// 遍历所有顶点,找到权值最小的顶点
for (int v = 0; v < numVertices; v++) {
if (!mstSet[v] && key[v] < minKey) {
minKey = key[v];
minVertex = v;
}
}
return minVertex;
}
// Prim 算法实现,生成最小生成树并返回总权值
void primMST(int G[MaxVertices][MaxVertices], int numVertices) {
int parent[MaxVertices]; // 记录最小生成树的父节点
int key[MaxVertices]; // 记录每个顶点到生成树的最小边权值
bool mstSet[MaxVertices]; // 标记顶点是否已经包含在 MST 中
// 初始化
for (int i = 0; i < numVertices; i++) {
key[i] = INT_MAX; // 初始化所有顶点权值为无穷大
mstSet[i] = false; // 初始时所有顶点都不在 MST 中
}
// 从第 0 个顶点开始构造 MST
key[0] = 0; // 将第一个顶点的权值设为 0
parent[0] = -1; // 第 0 个顶点没有父节点(即起始点)
// 构建最小生成树
for (int count = 0; count < numVertices - 1; count++) {
// 找到未被加入 MST 的顶点中,key 值最小的顶点
int u = findMinVertex(key, mstSet, numVertices);
mstSet[u] = true; // 将顶点 u 加入 MST
// 更新所有与 u 相邻的顶点的权值
for (int v = 0; v < numVertices; v++) {
// G[u][v] != 0 表示 u 和 v 有边
// mstSet[v] 为 false 表示 v 尚未加入 MST
// G[u][v] < key[v] 表示 u-v 边的权值小于 v 当前的 key 值
if (G[u][v] && !mstSet[v] && G[u][v] < key[v]) {
parent[v] = u;
key[v] = G[u][v];
}
}
}
// 输出最小生成树的边及其权值
cout << "Edge \tWeight\n";
for (int i = 1; i < numVertices; i++) {
cout << parent[i] << " - " << i << " \t" << G[i][parent[i]] << "\n";
}
}
int main() {
// 例子:带权无向图的邻接矩阵表示
int G[MaxVertices][MaxVertices] = {
{0, 2, 0, 6, 0},
{2, 0, 3, 8, 5},
{0, 3, 0, 0, 7},
{6, 8, 0, 0, 9},
{0, 5, 7, 9, 0}
};
int numVertices = 5; // 图的顶点数
primMST(G, numVertices); // 运行 Prim 算法
return 0;
}