DS博客作业04--图
DS博客作业04--图
这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
姓名 | 王鑫 |
0.PTA得分截图
1.本周学习总结
线性结构时一对一,树是一对多,图是多对多
线性结构可以看做是树形结构的特殊结构,树形可以看成是图的特殊结构
生活中的关系往往是多对多的复杂关系,生活的复杂性导致图的结构更具有广泛的实际应用。
1.1图的存储结构
图的存储主要分两种,一种邻接矩阵,另一种邻接表
用不同的方式存储图的信息
1.1.1邻接矩阵
无向图( 如果一个图结构中,所有的边都没有方向性,那么这种图便称为无向图。)
图的结构很明了,由各个顶点V和顶点之间的关系边E构成,
所以我们要构造存储图信息的结构就需要把这两种信息都存进去。
用邻接矩阵来存图,我们用一个二维数组来存储节点之间的边的关系和权重
在邻接矩阵实现中,由行和列都表示顶点,由两个顶点所决定的矩阵对应元素表示这里两个顶点是否相连、如果相连这个值表示的是相连边的权重。
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,弧数
} MGraph;
//另一种表示方式
typedef struct {
int** edges;//邻接矩阵
int n, e;//顶点数,边数
}MGraph;
- 第二种的邻接矩阵初始化
g = new MGraph;
g->edges = new int* [n + 1];
for (i = 1; i <= n; i++)
{
g->edges[i] = new int[n + 1];
for (j = 1; j <= n; j++)
g->edges[i][j] = INF;
}
在逻辑上我们的图就是一个表格来存储
- 无向图,给的 边关系 要处理两次1->2,2->1
因为无向图的特点,所以无向图的矩阵是根据对角线对称
边关系的建立如下
int a, b;//两条边
for (int i = 0 ; i < e; i++)//把图边的关系建起来
{
cin >> a >> b;
g.edges[a][b] = g.edges[b][a] = 1;
}
有向图( 一个图结构中,边是有方向性的,那么这种图就称为有向图)
- 有向图的存储和无向图一样,但边的关系就处理一次。
1.1.2邻接表
链表存储图用链表组来存储边关系。设有V个顶点,我们一般就会设置v+1长度的链表数组,每个数组的代表不同的顶点的边。
如:链表数组的第一个链表就是这个表头所有的邻边。
不同的顶点就有把不同的邻边,这样所有的边关系都能被存储起来。
这样就是链表的存储方式。
链表的结构定义如下所示
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;
无向图的链表
- 和矩阵一样链表遇上无向图也是要给它处理两次边关系,这时无向图的特点
for (i = 1; i <= e; i++)//建立边关系
{
cin >> a >> b;
node = new ArcNode;//要申请两次
node->adjvex = b;
node->nextarc = G->adjlist[a-1].firstarc;
G->adjlist[a-1].firstarc = node;
node = new ArcNode;//第二次
node->adjvex = a;
node->nextarc = G->adjlist[b-1].firstarc;
G->adjlist[b-1].firstarc = node;
}
我们建立链表的时候,一般用头插法
一位用头插法时的头结点我们一直都有,而用尾插法是还要重新申请一个变量来做尾指针,而且这个尾指针没有存在结构体中。
当在别的函数中后期想要插入函数机会很不方便。而我们的头结点就是表头一直都存在图的结构体中。
1.1.3邻接矩阵和连接表表示图的区别
邻接矩阵:它用数字特定的顺序组成一个二维数组。有边的就会有相应位置的数字。用数字的方式来存储边关系,很有计算机的特点。用计算机来处理这样的数据也很合适。
邻接表:用所有的顶点边来存放。这样所有的边都存进去,而每个顶点的邻边也是清楚的。
当数据中的边关系不是很复杂,用矩阵就会更吃亏。有很多的空间是没用的,而这时候用链表就能省下空间。
而当图为稠密图的时候,顶点之间都有边关系,这时候用矩阵更合适。
1.2图遍历
我们之前遍历顺序表或者链表都是直接从头遍历到尾结束条件很明确。当我们进入树的学习时,我们的遍历就有几种方式,先序、中序和后序,不同的遍历方式就会有不同的结果。但是树的遍历的结束条件也是很明显的,当到叶子节点的时候(左右子树都为NULL)我们就可以返回。那么我们的图要怎么遍历呢??图就像是一团圆圆的东西,看着都无法确定头和尾。所以我们要进行遍历的时候也要选择开始的那个节点。
而且为了辨别我们是否遍历完图的结构,我们借助一个数组visited[]来看是否遍历完成。
1.2.1深度优先遍历(DFS)
深度遍历就是图遍历的一种,深度遍历是一直遍历为遍历过的邻边,如果遇到邻边都是已遍历过的就要回溯寻找为遍历过的顶点,知道全部顶点都遍历过,所以我们深度遍历用的是递归的方法。
DFS流程
我们用的图是上面的无向图
我们先选中一个顶点作为深度遍历的开始
再选择现在这个顶点的邻边
最后这样就完成了我们这样的一次深度遍历
我们这一遍深度遍历可以知道根据不同的顶点,对边的不同选择就会有不同的深度遍历
代码
矩阵
void DFS(MGraph g, int v)//深度遍历
{
int flag = 0;
visited[v] = 1;
for (int j = 0; j <g.n; j++)
{
if (visited[j] == 1)
{
flag++;
}
}
if (flag == 1)
cout << v;
else
cout << " " << v;
for (int i = 0; i < g.n; i++)
{
if (g.edges[v][i] != 0 && visited[i] == 0)
{
DFS(g, i);
}
}
}
链表
void DFS(AdjGraph* G, int v)//v节点开始深度遍历
{
int flag = 0;
visited[v] = 1;
for (int j = 1; j <= G->n; j++)
{
if (visited[j] == 1)
{
flag++;
}
}
if (flag == 1)
cout << v;
else
cout << " " << v;
ArcNode* p;
for (int i = 0; i < G->n; i++)
{
p = G->adjlist[v].firstarc;
while (p)
{
if (visited[p->adjvex] == 0&&p!=NULL)
{
DFS(G, p->adjvex);
}
p = p->nextarc;
}
}
}
1.2.2广度优先遍历(BFS)
广度遍历和名字一样时尽量宽和往外走的遍历,和我们在数中学习的层次遍历很相似。都是一层一层的往外扩,直到所有结点都已近走过。
为了实现一层层往外扩的形式,我们要借助队列的结构(用栈结构也可以,结点遍历的先后没有硬性规定)
BFS流程####
选中一个顶点,并且进队列
1 出队列,1的邻边 2 3 6 边进队列
先把我们第一个顶点 2 出队,再找2的邻边,发现 3 已经遍历过,只有 5 进队
继续出队,和上一步的操作一样,把出队的顶点加进来
本次操作中,3 的邻结点都已经遍历过了,本次无元素进队
重复上面的操作,最后直到队列为空,我们的广度遍历完成
在代码上实现
矩阵
void BFS(MGraph g, int v)//广度遍历
{
int front;
queue<int>q;
q.push(v);
visited[v] = 1;
cout << v;
while (!q.empty())
{
front = q.front();
q.pop();
for (int i = 0; i < g.n; i++)
{
if (g.edges[front][i] == 1 && visited[i] == 0)
{
q.push(i);
visited[i] = 1;
cout << " " << i +;
}
}
}
}
链表
void BFS(AdjGraph* G, int v) //v节点开始广度遍历
{
int i, j;
int front;
queue<int>q;
ArcNode* p;
q.push(v);
visited[v] = 1;
cout << v;
while (!q.empty())
{
front = q.front();
q.pop();
p = G->adjlist[front].firstarc;
do
{
if (p != NULL&&visited[p->adjvex]==0)
{
q.push(p->adjvex);
visited[p->adjvex ] = 1;
cout << " " << p->adjvex ;
}
p = p->nextarc;
}while (p != NULL);
}
}
1.3最小生成树
用一个例子来说比较明显,上面的无向图中建一条路,这条路能到达每个顶点,并且是最小的权值,这样的一条路就是这个图的最小生成树。
就是求一条能最小代价且能到达所有顶点的边集。
无向图的最小生成树
把多余的边去掉,倒过来看就像是一颗树一样。
1.3.1Prim算法求最小生成树
普利姆(Prim)算法是一种构造性算法,用于构造最小生成树。过程如下:
- 初始化U={v}。v到其他顶点的所有边为候选边;
- 重复以下步骤n-1次,使得其他n-1个顶点被加入到U中;
从候选边中挑选权值最小的边输出,这该边在V-U中的顶点是k,将k加入u中;
考察当前V-U中的所有顶点j,修改候选边;若(j,k)的权值小于原来和顶点k关联的候选边,则用(k,j)取代或者作为候选边。
使用Prim算法要借助两个数组做工具,一个是lowcost[]//存放候选边,每个顶点到u中最小边。另一个是closet[]//U中顶点的邻边顶点、
伪代码
初始化lowcost,closest数组
for(v=1;v<=n;v++)
遍历lowcost数组//选最小边
若lowcost[i]!=0,找最小边邻接点k
lowcost[k]=0;输出边(closest[k],k);
再次遍历lowcost数组 //修正lowcost
若lowcost[i]!=0&&edges[i][k]<lowcost[k]
修正lowcost[k]=edges[i][k]
end
算法流程
先选中一个顶点,待选边有5 6 7
选中 5
最小的边是6
根据上述规则继续下去,最后得到最小生成树
代码实现
#define INF 32767
//INF表示oo
void Prim(MGraph g, int v)
{
int lowcost[MAXV], min, closest[MAXV], i, j, k;
for (i = 0; i < g.n; i++)//给lowcost[]和lclosest[]置初
{
lowcost[i] = g.edges[v][i; closest[i] = v;
}
for (i = 1; i < g.n; i++)
//找出(n - 1)个顶点
{
min = INF;
for (j = 0; j < g.n; j++)//在(V - U)中我出离U最近的项点k
if (lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j]; k = j; //k记录最近顶点的编号
printf("边(%d,%d)权为:%d\n", closest[k], k, min);
lowocost[k] = 0;// 记已经加入U
for (j = 0; j < g.n; j++)
//修改数组lowcost和closest
if (g.edges[k][j] != 0 && g.edges[k][j] < lowcost[j])
{
lowcost[j] = g.edges[k][j];
{
closest[j] = k;
}
}
}
}
时间复杂度为O(n^2)
1.3.2Kruskal算法求解最小生成树
Kruskal算法也是一种求带权无向图的最小生成树的构造性算法。
按权值的递增次序选择合适的边来构造最小生成树的方法
- 置U的初值等于(即包含G中的全部顶点),TE的初值为空集(即图T中每一个顶点都构成一个连通分量)。
- 将图中的边按权值从小到大顺序一次选取:
若选取的边未使生成树T形成回路,则加入TE;
否则舍弃,直到TE中包含(n-1)条边为止。
思路描述
E中所有边按权重排序。
构造非连通图ST=(V,{});
k=i=0;//k计算选中的边数
while(k<n-1)
{
++i;
检查边集E中第i条权值最小的边(u,v);
若(u,v)加入ST后不使ST中产生回路,则输出边(u,v);
k++;
}
算法流程
选中最小边
再选
一直选,直到。。。
最后一边
完成
代码实现
void Kruskal(AdjGraph * g)
{
int i,j, u1,v1,sn1,sn2,k;
int vset[MAXV];//集合辅助数组
Edge E[MaxSize];//存放所有边
k = 0;
//E数组的下标从0开始计
for (i = 0; i < g.n; i++)// 由g产生的边集E
{
p = g->adjlist[i].firstarc;
while (p != NULL)
{
E[k].u = i; E[k].v = p->adjvex;
E[k].w = p->weight;
k++; p = p->nextarc;
}
InsertSort(E,g.e); //用直接插入排序对E数组按权值递增排序
for (i = 0; i < g.n; i++)I初始化辅助数组
vset[i] = i;
k = 1;
//k表示当前构造生成树的第几条边,初值为1
j = 0;
//E中边的下标,初值为0
while (k < g.n)//生成的顶点数小于n时循环
{
u1 = E[j].u; v1 = E[j].v;
//取一条边的头尾顶点
sn1 = vset[u1];
sn2 = vset[v1];
//分别得到两个顶点所属的集合编号
if (sn1 != sn2)
// 两顶点属于不同的集合
{
printf("(%d,%d): %d\n",u1,v1,E[j].w);
k++;
//生成边数增1
for (i = 0; i < g.n; i++)// 两个集合统一编号
if (vset[i] == sn2) //集合编号为sn2的改为sn1
vset[i] = sn1;
}
j++; //扫描下一条边
}
}
}
改进后,算法的时间复杂度能从O(n^2)->O(eloge)
Prim算法使用适用于稠密图,Kruskal算法适用于稀疏图。
1.4最短路径
有两种最短路径,一种是一个顶点点到其余各顶点之间的最短路劲,另一种是任意两点之间的最小路径。
分别用Dijkstra算法和Floyd算法来求解
1.4.1Dijkstra算法求解最短路径
1. 从T中选取一个其距离值为最小的顶点W,加入S
2. S中加入顶点w后,对T中顶点的距离值进行修改:若加进W作中间顶点,从V到Vj的距离值比不加W的路径要短,则修改此距离值;
3. 重复上述步骤1,直到s中包含所有顶点,即S=V为止。
我们借用两个数组,dis[] path[]
源点V默认,dis[j]表示源点->顶点j的最短路径长度。
这个数组存放不同顶点的最短路径长度。
用path[]来存放一条最短路径,
如从顶点0->5的最短路径为0、4、3、5,表示为path[5]={0,4,3,5}
从源点到其他顶点的最短路径有n-1条,二维数组path[][]存储。
//这个path[]的设计是有一个理论:一条最短路径,对于所有这条路上的所有顶点来说沿着这条路回到源点都是顶点的最短路径
求最短路径如果用贪心算法求最短路径可能无法得到最优解。
因为贪心算法没有对最小边进行修改,就会选中非最小边。这样求解出来的最短路径就不是最短的了
用矩阵的结构来使用Dijkstra会比较方便,直接使用下标就可以找到我们需要的数据,用链表还要设置一个指针。比较繁琐
Dijkstra算法特点
- 不适用带负权值的带权图求单源最短路径
- 不适用求最长路径长度
按Dijkstra算法,找第一个距离源点S最远的点A,这个距离在以后就不会改变。但A与S的最远距离一般不是直连。 - 最短路径长度是递增的
- 顶点u加入S后,不会在修改源点v到u的最短路径长度
主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
1.4.2Floyd算法求解最短路径
Floyd算法求解最短路径是每个顶点之间的,
也可以Dijkstra调用n次,可能达到求解得出每个顶点之间的最短路径。
两个时间的复杂性都是O(n^3)不过,Floyd形式上更简单点。
Floyd用一个二维数组A存放当前顶点最短路径长度,如:A[i][j]代表低你干点i到j之间的最短路径。
还有一个path二维数组,和之前Dijkstra算法中的path的用途一致,用来回溯寻找路径进过的顶点,Floyd中的是每个顶点的集合成二维数组。
- 初始时,有A[i][j]=g.edges[i][j]
- 考虑i->j的最短路径经过编号为K顶点的情况。//k是1->n
1.5拓扑排序
在一个有向无环图中找一个拓扑序列的锅成叫做拓扑排序
只有有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑序列因此拓扑序列可以来判断是否有回路
拓扑序列的求解
1. 从有向图中选取一个没有前驱的顶点,并输出它;
2. 从有向图中删去顶点以及所有以它为尾的弧
3. 冲哪个服上述步骤,直至图空,或者图不空但找不到无前驱的顶点为止。
以上可以知道拓扑序列不止一个,可以有很多
因为拓扑序列对入度有运用所以我们需要求不通顶点的入度,而且为了存放入度,我们需要设计一个结构体专门来存放顶点的入度
用邻接表的方式来存放数据比较好,可以设置一个cout来放置入度
结构体设计
typedef struct//表头节点类型
{
vertex data;//顶点信息
int count; // 存放顶点入度
ArcNode* firstarc;//指向第一条弧
}VNode;
我们这样就能知道这个节点的入度情况,但是没玩是怎么删除这个顶点的入度的?找它的前驱太麻烦,我们在直接把入度-1,这样在逻辑上我们就是把这个前驱删除。而且也可以判断是否为入度为1。
伪代码*
遍历邻接表
把每个顶点的入度算出来
遍历图顶点
度为0,入栈st
while (st不为空)
{
出栈v,访问
遍历v的所有邻接点
{
所有邻接点的度 - 1;
邻接点的度若为0,进栈st
}
}
若我们在进行拓扑序列排序后,发现还有的顶点没有度为0 ,则说明这个图有回路
1.6关键路径
在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,一个工程常被分为多个小的子工程,这些子工程被称为活动(Activity),在有向图中若以顶点表示活动,有向边表示活动之间的先后关系,这样的图简称为AOV网。--百度百科
AOV-网是用顶点表示事件,用有向边e表示活动,边的权c(e)表示活动持续时间。是带权的有向无环图
整个工程完成的时间为:从有向图的源点到汇点的最长路径。又叫关键路径
关键活动指的是:关键路劲中的边
关于AOE网图的意义
- AOE网——带权的有向无环图
- 顶点———事件或状态
- 弧(有向边)——活动及发生的先后关系
- 权——活动持续的时间
- 起点——入度为0的顶点(只有一个)
- 终点——出度为0的顶点(只有一个)
2.PTA实验作业
2.1六度空间
2.1.1
伪代码
int sum;//记录符合的顶点的个数
int level;//记录层数,和源点的关系有几层
根据顶点、边数、边建图
遍历所有顶点
进入函数得到满足六度空间理论的数量
sum/总数=result;
//函数
进队顶点
while (队不为空)
{
出队顶点V
遍历V的所有邻接点
{
找到没有遍历过的顶点,进队列
sum++;
}
如果在遍历的时候发现遍历的节点是这一层的最后一个
{
level++;
更新这层的最后一个
}
如果层数为6 //说明已经到了六层,剩下的就是不符合六度空间理论的顶点
返回我们记录下的sum
}
提交列表
本题知识点
- 广度遍历,借用队列结构
- 用一个last来观察是否为最后一位
- 用函数及时的返回不会浪费多余的时间
2.2村村通
2.2.1伪代码
int* closest;//保存相关顶点的下标
int* lowcost;//保存相关顶点的权值
int cost;//总的路径
根据顶点、边数、边建图
初始化lowcost和closest数组
lowcost赋值1到邻边的值,不是邻边的就赋值无穷大
遍历n - 1次//有n-1和边
{
寻找lowcost的最小的那条边
{
cost += 最小边
lowcost[最小边的邻接点] = 0;//表示已经遍历过
}
遍历n个顶点
{
如果新加入顶点到为遍历过顶点之间的距离比原来小
我们更新lowcost
把closest也跟着更新
}
}
如果没有全都遍历过
不连通返回-1
否则
返回cost
提交列表
本题知识点
- Dijkstra算法,
- 是否连通图的判断,因为我们用Dijkstra算法来遍历这个图,非联通发图才会在最后还有顶点没有遍历