这个作业属于哪个班级 | C语言–网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业04–图 |
这个作业的目标 | 学习图的结构设计及运算操作 |
姓名 | 骆锟宏 |
图的大概的基本框架:
ps:试试自己造一个比较复杂的实例用这个实例来解释图的各种功能。
那么就以这个图为例,大家来先感性认识一下怎么用邻接矩阵来表示一个图趴:
而把图转化成邻接矩阵后就会变成这个亚子:
define INF 32767
(针对int
)接下来就着这张图说说邻接矩阵的一些特点:
邻接矩阵的结构体定义:
typedef struct //图的定义
{
ElementType edges[MAXV][MAXV]; //这样定义的话,如果顶点从0开始编号,则需要进行细节上的转化。
//ElementType edges[MAXV+1][MAXV+1]; //这样不需要。
int n, e; //顶点数,弧数
} MGraph; //图的邻接矩阵表示类型
ElementType
来定义edges是因为,在具体应用中,边这个类,可能不止有权值这样一个属性,这时候,边就是一个含有多个属性的元素,可以定义一个结构体去表示边这一个类,结构体可以允许边有多个属性成员。//需要先对二维指针开辟多个一维指针空间;
int** edge = new *int[n];
//然后再在循环中对每个一维指针动态开辟内存。
for(int i = 0;i < n;i++)
{
edge[i] = new int [numb];
}
//有对顶点转对应下标进行处理的无向图的建树:
void CreateMGraph(MGraph& g, int n, int e)//建图
{
int i, k;
int node1, node2;
//初始化邻接矩阵;
for (i = 0; i < n; i++)
{
for (k = 0; k < n; k++)
{
g.edges[i][k] = 0;
}
}
//建图
for (i = 0; i < e; i++)
{
cin >> node1 >> node2;
g.edges[node1 - 1][node2 - 1] = 1;
g.edges[node2 - 1][node1 - 1] = 1;//这一步是无向图建树特有的对称建树
}
//别忘了给边和结点数赋值!
g.n = n, g.e = e;
}
// MAXV+1型的建树:
void CreateMGraph(MGraph& g, int n, int e)//建图
{
int i, k;
int node1, node2;
//初始化邻接矩阵;
for (i = 1; i <= n; i++)
{
for (k = 1; k <= n; k++)
{
g.edges[i][k] = 0;
}
}
//建图
for (i = 0; i < e; i++)
{
cin >> node1 >> node2;
g.edges[node1][node2] = 1;
g.edges[node2][node1] = 1;//这一步是无向图建树特有的对称建树
}
//别忘了给边和结点数赋值!
g.n = n, g.e = e;
}
我们不妨以刚才已经使用的在邻接矩阵中使用过的图为例:
将该图转化为邻接表表示如下:
然后我们来谈谈邻接表的一些特性:
邻接矩阵的结构体定义:
typedef struct ANode
{
int adjvex; //该边的终点编号
struct ANode* nextarc; //指向下一条边的指针
int info; //该边的相关信息,如权重
} ArcNode; //边表节点类型
typedef int Vertex;
typedef struct Vnode
{
Vertex data; //顶点信息
ArcNode* firstarc; //指向第一条边
} VNode; //邻接表头节点类型
typedef VNode AdjList[MAXV];
typedef struct
{
AdjList adjlist; //邻接表
int n, e; //图中顶点数n和边数e
} AdjGraph;
从上面我们可以很清晰地看到这里就像我们前面对例子图所分析的那样 分别需要表示边结点的结构体,表示顶点的结构体和表示整个图整体的结构体, 这三个结构体来完整地表示出一个图。
需要注意的一个点是,在图的结构体中,各个顶点元素的集合,可以用一个数组来表示,亦可以用一个指针来表示,只不过使用指针来表示的时候需要记得为指针动态开辟内存。
建图函数:
//将顶点和下标有转化的建图
void CreateAdj(AdjGraph*& G, int n, int e)//创建图邻接表
{
int i,k;
int node1, node2;
G = new AdjGraph;
//初始化顶点数组
for (i = 0; i < n; i++)
{
G->adjlist[i].firstarc = NULL;
}
for (k = 0; k < e; k++)
{
cin >> node1 >> node2;
ArcNode* newnode1 = new ArcNode;
newnode1->adjvex = node2;
newnode1->nextarc = G->adjlist[node1 - 1].firstarc;
G->adjlist[node1 - 1].firstarc = newnode1;
//对称建表
ArcNode* newnode2 = new ArcNode;
newnode2->adjvex = node1;
newnode2->nextarc = G->adjlist[node2 - 1].firstarc;
G->adjlist[node2 - 1].firstarc = newnode2;
}
G->e = e, G->n = n;
}
我们依然以这个图为例:
但是首先要把它的图的数据数据化一下,根据该图的具体特征:
8 14
a b 4
a c 3
b c 5
b d 5
b e 9
c d 5
c h 5
d e 7
d f 6
d g 5
d h 4
e f 3
f g 2
g h 6
由此这个顶点关系的输入可以转化为:
8 14
1 2 4
1 3 3
2 3 5
2 4 5
2 5 9
3 4 5
3 8 5
4 5 7
4 6 6
4 7 5
4 8 4
5 6 3
6 7 2
7 8 6
采用邻接表存储结构得到的结果:(以1为初始顶点。)
(以2为初始顶点)
采用邻接矩阵存储结构得到的结果:(以1为初始顶点。)
(以2为初始顶点)
void DFS(MGraph g, int v)//深度遍历
{
int i;
visited[v-1] = 1;//置已经被访问的结点为1
//输出结点。
if (!flag)
{
cout << v;
flag = 1;
}
else
{
cout << " " << v;
}
for (i = 0; i < g.n; i++)//i要从0开始。
{
if (g.edges[v-1][i] != 0 && visited[i] == 0)
{
DFS(g, i + 1);
}
}
}
void DFS(AdjGraph* G, int v)//v节点开始深度遍历
{
ArcNode* ptr;
visited[v - 1] = 1;
if (!flag)
{
cout << v;
flag = 1;
}
else
{
cout << " " << v;
}
ptr = G->adjlist[v - 1].firstarc;
while (ptr != NULL)
{
if (visited[ptr->adjvex-1] == 0)
{
DFS(G, ptr->adjvex);
}
ptr = ptr->nextarc;
}
}
这里给出一个相关总结的博客的链接感谢作者:Fellow@
图的深度优先遍历的应用
————————————————
版权声明:本文为CSDN博主「Fellow@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_40512890/article/details/105696313
void BFS(MGraph g, int v)//广度遍历
{
int i, k;
int cur_node;
//手动建顺序队列
int queue[MAXV];
int front, rear;
front = rear = 0;
//初始化中没有给visited[0]初始化为0
visited[0] = 0;
visited[v - 1] = 1;
queue[rear++] = v;//enqueue
cout << v;
while (front != rear)
{
cur_node = queue[front++];
for (i = 0; i < g.n; i++)
{
if (visited[i] == 0 && g.edges[cur_node-1][i] != 0)
{
cout << " " << i + 1;
queue[rear++] = i + 1;
visited[i] = 1;
}
}
}
}
void BFS(AdjGraph* G, int v) //v节点开始广度遍历
{
int i;
ArcNode* Ptr;
queue que;
int cur_vex;
//给visited[0]初始化
visited[0] = 0;
visited[v - 1] = 1;
que.push(v);
cout << v;
while (!que.empty())
{
cur_vex = que.front();
que.pop();
Ptr = G->adjlist[cur_vex-1].firstarc;
while (Ptr)
{
if (visited[Ptr->adjvex-1] == 0)
{
cout << " "<< Ptr->adjvex;
visited[Ptr->adjvex - 1] = 1;
que.push(Ptr->adjvex);
}
Ptr = Ptr->nextarc;
}
}
}
这里也给出一个相关总结的博客的链接感谢作者:Fellow@
图的广度优先遍历的应用
————————————————
版权声明:本文为CSDN博主「Fellow@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_40512890/article/details/105713455
细节解释:
题目思路梳理:
Prim算法的思路:
1.变量定义。
2.根据初始顶点初始化closest数组和lowcost数组。
3.接下来就是去构造那n-1条边。
3.1 首先是从lowcost数组中找到当前权值最小的值,纳入该边。
3.2 纳完边后,要对lowcost数组和closest数组进行更新。
void Prim(MatGraph G, int vex,int& sum)//vex是起始的结点
{
int* closest = new int[G.n];
int* lowcost = new int[G.n];
int i,j,keep;
int MIN;//最小值;
//初始化两个辅助数组
for (i = 0; i < G.n; i++)
{
lowcost[i] = G.edges[vex-1][i];
closest[i] = vex-1;
}
//找出(n-1)个顶点;
for (i = 1; i < G.n; i++)
{
MIN = INF;
//在lowcost中找出最小值;
for (j = 0; j < G.n; j++)
{
if (lowcost[j] != 0 && lowcost[j] < MIN)
{
MIN = lowcost[j];
//break;//不能中途退出要找完整个数组
keep = j;//保存最小值的下标;
}
}
//如果最小值仍为无限大,代表存在不连通的结点,需要建设更多的公路
if (MIN == INF)
{
sum = -1;
return;
}
lowcost[keep] = 0;//标记结点为已选中;
//计算最低成本:
sum += MIN;
//更新辅助数组,循环控制的变量别用 i !!!
for (j = 0; j < G.n; j++)
{
if (lowcost[j] != 0 && lowcost[j] > G.edges[keep][j])
{
lowcost[j] = G.edges[keep][j];
closest[j] = keep; //修改lowcost数组和closest数组
}
}
}
}
typedef struct
{
int start_u;//起始顶点
int end_v;//结束顶点
int weight;//边的权重值
}Edge;
//顶点编号从0开始;
void Kruskal(MatGraph G)
{
int i, j, u1, v1, sn1, sn2, k;
int* vset = new int[G.n];
Edge* edges = new Edge[G.e];
k = 0; //表示edges数组当前的下标
//初始化边的数据信息
for (i = 0; i < G.n; i++)
{
for (j = 0; j <= i; j++)
{
if (G.edges[i][j] != 0 && G.edges[i][j] != INF)//也即是说存在边
{
edges[k].start_u = i, edges[k].end_v = j, edges[k].weight = G.edges[i][j];
k++;
}
}
}
//按权值从小到大插入排序edges数组;
//InsertSort(edges, G.e); //具体代码不给出
//初始化vset数组
for (i = 0; i < G.n; i++)
{
vset[i] = i;//给每一个顶点进行不同集合打标签
}
int numb = 1;//表示当前构造了生成树的第几条边,默认从1开始。
int index = 0;//表示edges中边的下标,初值从0开始。
while (numb < G.n)
{
//取一条边的两个顶点。
u1 = edges[index].start_u;
v1 = edges[index].end_v;
//取两个顶点对应的标签
sn1 = vset[u1];
sn2 = vset[v1];
if (sn1 != sn2) //如果两个顶点不是同一个集合,代表改边可用。
{
cout << u1 << "->" << edges[index].weight << "->" << v1 << endl; //输出一条边;
numb++;
//统一两个集合的编号:
for ( i = 0; i < G.n; i++)
{
if (vset[i] == sn2)
{
vset[i] = sn1;
}
}
}
index++;//继续构造下一条边。
}
}
我们依然以这个图为例:
列出表格后得到的从a顶点到其他顶点的最短路径如下图:
Dijkstra算法如何解决贪心算法无法求最优解问题?展示算法中解决的代码。
从代码上来讲就是:
void Dijkstra(MGraph g, int v)//源点v到其他顶点最短路径
{
int* S = new int[g.n];
int* dist = new int[g.n];
int* path = new int[g.n];
int i,j,k,MINdis;
//初始化各个数组
for (i = 0; i < g.n; i++)
{
S[i] = 0;
dist[i] = g.edges[v][i];//
//不需要进行分类,因为不存在边的权值已经初始化为INF
if (g.edges[v][i] < INF)
{
path[i] = v;
}
else
{
path[i] = -1;
}
}
S[v] = 1, dist[v] = 0, path[v] = 0;
for (i = 0; i < g.n-1; i++)
{
//根据dist中的距离从未选顶点中选择距离最小的纳入
MINdis = INF;
for (j = 0; j < g.n; j++)
{
if (S[j] == 0 && dist[j] < MINdis)
{
MINdis = dist[j];
k = j;
}
}
S[k] = 1;
//纳入新顶点后更新dist信息和path信息
for (j = 0; j < g.n; j++)
{
if (S[j] == 0)//针对还没被选中的顶点
{
if (g.edges[k][j] < INF //新纳的顶点到未被选中的顶点有边
&& dist[k] + g.edges[k][j] < dist[j])//源点到k的距离加上k到j的距离比当前的源点到j的距离短
{
dist[j] = dist[k] + g.edges[k][j];
path[j] = k;
}
}
}
}
//接下来是输出的函数:
Dispath(dist, path, S, g.n, v);
}
对于从一个顶点到其他所有顶点的最短路径问题,我们可以通过Dijkstra算法来解决,那么如果从一扩展到多呢?我们可以简单粗暴地通过多次调用dikstra算法来求解,这显然是可以行得通的,并且我们可以明显知道它的时间复杂度是O(n3),但是有没有可以简化的点呢?回答是肯定的。
Floyd算法就是针对这种思路所进行的改进,同样也是用来解决每对顶点之间的最短路径问题。
Floyd算法需要一个辅助二维数组A[][]用来存储每个顶点到邻近顶点的路径长度。
Floyd算法需要一个辅助二维数组path[][]用来存储每个顶点的前驱顶点。
这两个二维数组,本质上是把dijkstra算法的每个顶点出发的dist[]数组和path[]数组整合起来,合成了一个二维数组。
Floyd算法优势
Floyd算法的前驱数学逻辑:
Floyd算法的伪代码:
1.遍历邻接矩阵,为A矩阵和path矩阵初始化。
//对A矩阵进行初始化的含义是先载入当前所有的顶点之间,如果存在边的话,那它们之间直接连接的长度是多少,先记录到矩阵A当中。
2.依次检验从任意顶点a到任意顶点k再到任意顶点b的最短路径,如果存在更短的就更新,知道列举结束。
//从数学含义来讲这一步的操作就是一步一步对每两个顶点中间不断插入顶点来连接两个顶点,而在所有的可能中只保留最短的那种情况。
3.借用path矩阵输出每个顶点对应的最短路径。
最短路径算法还有其他算法,这里有一篇博客列举出其他的3种不同的算法。
感谢作者:qq_35710556
图的五种最短路径算法
————————————————
版权声明:本文为CSDN博主「qq_35710556」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_35710556/article/details/79583229
typedef struct Vnode
{
Vertex data; //顶点信息
int count; //入度 //这就是新加的变量也是TopSort中需要特别注意的点。
ArcNode* firstarc; //指向第一条边
} VNode; //邻接表头节点类型
定义辅助栈
for()
初始化所有顶点的入度为0
for()
遍历邻接表,修改每个顶点的入度信息。
先让当前入度为0的顶点入栈
while(栈不空)
{
取栈顶元素输出
while(指针不空)
{
让已出栈的栈顶顶点指向的顶点入度减一 //这就是删除入度为0的顶点的办法。
if(入度减一后出现入度为0的顶点)
让该顶点入栈
}
}
六度空间代码
公路村村通代码
采用邻接表存储结构。
首先先根据无权图建好图。
while (level < 6)//控制广度遍历往外遍历6层
{
if (visited[cur_vex] == 0)
{
count++;(从1开始,1是本身,本身为与本身距离为0,也包含在满足条件的范围内)
}
}
输出:
for(依次遍历每个顶点)
{
BFS(每个顶点)
输出: (与源顶点距离小于等于6的顶点总数) * 100.00 / 总顶点数 '%';
}
level = 0
来控制的话,则条件的设置应该变化为(level < 6
) 这样当level为6的时候,正好向外广度遍历了6次。本题为最小生成树问题,需要直接访问权值,故选择邻接矩阵存储结构。
首先要根据提供的带权无向图的信息来建树。
然后是采用Prim算法
初始化closest数组:和初始顶点有边的置为初始顶点,没有的置-1
初始化lowcost数组:和初始顶点有边的置为权值,没有的置为INF(无限大)
while(from 0 to n-1) //接下来就是去构造那n-1条边。
{
从lowcost数组中找到当前权值最小的值,纳入该边。
if (MIN == INF)
{
说明存在不连通的现象直接返回ERROR
}
对lowcost数组和closest数组进行更新。
}
知道是Prim算法后就没什么大问题了,因为本题上课老师有讲过。
其中一个值得讲的点是,判断是否存在非连通情况的话,正常思路是用一个visited[]数组来统计,如果存在没有被访问过的顶点就视为存在,但是这样时间开销大,所以按伪代码中的思路更好。