这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | [DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
姓名 | 杨振鹏 |
0.PTA得分截图
1.本周学习总结
1.1 图的存储结构
- 1.1.1 邻接矩阵
图的邻接矩阵是一种采用邻接矩阵数组表示顶点之间相邻关系的存储结构。设G=(V,E)是含有n个顶点的图,则G的邻接矩阵数组是n阶方阵。使用二维数组edges[][]来保存两个顶点的信息,edges[i][j]表示从第i个顶点到第j个顶点的边信息。
结构体
#define MAX 100
typedef struct
{
int edges[MAX][MAX];//邻接矩阵
int n, e;//n:顶点数,e:边数;
VertexType vexs[MAX];//存放顶点信息
}
函数
无向图
void CreateMGraph(MGraph& g, int n, int e)//建图
{
int i, j, a, b;
/*初始化*/
for (i = 1;i <= n;i++)
for (j = 1;j <= n;j++)
g.edges[i][j] = 0;
/*有边,赋值为1*/
for (i = 1;i <= e;i++)
{
cin >> a >> b;
//无向图,两顶点相互连边
g.edges[a][b] = 1;
g.edges[b][a] = 1;
}
g.n = n;
g.e = e;
}
有向图
void CreateMGraph(MGraph& g, int n, int e)//建图
{
int i, j, a, b;
/*初始化*/
for (i = 1;i <= n;i++)
for (j = 1;j <= n;j++)
g.edges[i][j] = 0;
/*有边,赋值为1*/
for (i = 1;i <= e;i++)
{
cin >> a >> b;
g.edges[a][b] = 1;
}
g.n = n;
g.e = e;
}
- 1.1.2 邻接表
结合数组和链表的方法来存储。每个顶点有一个单链表,连接这个顶点的所有邻接点,然后将这些顶点的单链表的头结点存到一个数组中。
结构体
//声明边结点类型
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;
int a, b;
ArcNode* p;
G = new AdjGraph;
//G->adjlist = new VNode[n];//申请空间
for (i = 0; i < n; i++)
G->adjlist[i].firstarc = NULL;
for (i = 0; i < e; i++)
{
cin >> a >> b;
//用头插法创建邻接表
p = new ArcNode;
p->adjvex = b;
p->nextarc = G->adjlist[a].firstarc;
G->adjlist[a].firstarc = p;
//无向图,另一个节点也存在相同的关系
p = new ArcNode;
p->adjvex = a;
p->nextarc = G->adjlist[b].firstarc;
G->adjlist[b].firstarc = p;
}
G->e = e;
G->n = n;
}
- 1.1.3 邻接矩阵和邻接表表示图的区别
一般来说,邻接矩阵适合于存储边的数目较多的稠密图,邻接表适合于存储边数目更少的稀疏图
图的邻接矩阵容易求得顶点的度,权值,是否有边问题
图的邻接表易于查找顶点i所相连的边,顶点的所有邻接点问题
邻接矩阵存储空间为O(n^2),邻接表存储空间为O(n+e)
邻接矩阵构造时间复杂度为O(n^2),邻接表构造时间复杂度为O(n+e)
1.2 图遍历
- 1.2.1 深度优先遍历
图中的一个顶点出发,每次遍历当前访问顶点的临界点,一直到访问的顶点没有未被访问过的临界点为止。然后采用依次回退的方式,查看来的路上每一个顶点是否有其它未被访问的临界点。访问完成后,判断图中的顶点是否已经全部遍历完成,如果没有,以未访问的顶点为起始点,重复上述过程。是一个不断回溯的过程。
邻接矩阵
void DFS(MGraph g, int v)//邻接矩阵深度遍历
{
if (flag == 0)
{
cout << v;
flag = 1;
}
else
cout << " " << v; //输出顶点
visited[v] = 1;//标记已访问该节点
for (int i = 1; i <= g.n; i++)
{
if(g.edges[v][i] == 1 && visited[i] == 0)
{
DFS(g, i); //当前顶点与 i 顶点邻接且未被访问,递归搜索
}
}
}
邻接表
void DFS(AdjGraph* G, int v)//v节点开始深度遍历
{
ArcNode* p;
p = G->adjlist[v].firstarc;
if (flag == 0)
{
cout << v;
flag = 1;
}
else
cout << " " << v;
visited[v] = 1; //标记已访问
while (p)
{
if (!visited[p->adjvex])//未被访问过
DFS(G, p->adjvex);
p = p->nextarc;
}
}
适用问题
-
两点间是否存在路径
-
走迷宫所有路径
-
1.2.2 广度优先遍历
类似于树的层次遍历。从图中的某一顶点出发,遍历每一个顶点时,依次遍历其所有的邻接点,然后再从这些邻接点出发,同样依次访问它们的邻接点。按照此过程,直到图中所有被访问过的顶点的邻接点都被访问到。最后还需要做的操作就是查看图中是否存在尚未被访问的顶点,若有,则以该顶点为起始点,重复上述遍历的过程。
无向
有向
邻接矩阵
void BFS(MGraph g, int v)//邻接矩阵广度遍历
{
int t;
queue<int>q;
if (visited[v] == 0)
{
cout << v;
visited[v] = 1;
q.push(v);
}
while (!q.empty())
{
t = q.front();
q.pop();
for (int j = 1; j <= g.n; j++)
{
if (g.edges[t][j] == 1 && visited[j] == 0)
{
cout << " " << j;
visited[j] = 1;
q.push(j);
}
}
}
}
邻接表
void BFS(AdjGraph* G, int v) //v节点开始广度遍历
{
queue<int>q;
ArcNode* node;
int n;//边的序号
int j;
visited[v] = 1;//表示已访问
cout << v ;
q.push(v);//入队
while (!q.empty())//队不空
{
j = q.front();
q.pop();
node = G->adjlist[j].firstarc;
while (node)//按邻接表输出头结点后的所有节点
{
if (!visited[node->adjvex])
{
visited[node->adjvex] = 1;
cout << " " << node->adjvex;
q.push(node->adjvex);
}
node = node->nextarc;
}
}
}
适用问题
- 求解最短路径
- 走迷宫最短路径
1.3 最小生成树
一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。最小生成树可以用kruskal(克鲁斯卡尔)算法或Prim(普里姆)算法求出。
对于带权连通图G可能有多课不同树,每棵生成树的所有边的权值之和可能不同,其中权值之和最小的生成树称为图的最小生成树。
- 1.3.1 Prim算法求最小生成树
普里姆(Prim)算法是一种构造性算法,用于构造最小生成树。过程如下:
(1)初始化U={v}。v到其他顶点的所有边为候选边;
(2)重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
考察当前V-U中的所有顶点j,修改候选边:若(j,k)的权值小于原来和顶点k关联的候选边,则用(k,j)取代后者作为候选边
代码
int Prim(MGraph* g)//最小生成树prim算法
{
int* lowcost;//边权重
int* clostest;//顶点编号
int min, i, j, k;
int cost = 0;
lowcost = new int[g->n + 1];
clostest = new int[g->n + 1];
/*初始化,给lowcost和clostest置初值*/
for (i = 1;i <= g->n;i++)
{
lowcost[i] = g->edges[1][i];
clostest[i] = 1;
}
lowcost[1] = 0;//从顶点1开始
for (i = 1;i <= g->n;i++)
{
min = INF;
/*寻找权值最小的点*/
for (j = 1;j <= g->n;j++)
{
if (lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j];
k = j;//k记录权值最小的编号
}
}
cost += lowcost[k];//计算花费
lowcost[k] = 0;//标记,访问过
/*修改数组lowcost和clostest*/
for (j = 1;j <= g->n;j++)
{
if (lowcost[j] != 0 && g->edges[k][j] < lowcost[j])
{
lowcost[j] = g->edges[k][j];
clostest[j] = k;
}
}
}
/*判断是否连通*/
for (i = 1;i <= g->n;i++)
{
if (lowcost[i] != 0) return -1;
}
return cost;
}
时间复杂度:O(n^2)
适合邻接矩阵结构
- 1.3.2 Kruskal算法求解最小生成树
一种求带权无向图的最小生成树的构造性算法,按权值的递增次序选择合适的边来构造最小生成树的方法
(1)置U的初值等于V(即包含有G中的全部顶点),TE(表示最小生成树的边集)的初值为空集(即图T中每一个顶点都构成一个连通分量)。
(2)将图G中的边按权值从小到大的顺序依次选取:
若选取的边未使生成树T形成回路,则加入TE;
否则舍弃,直到TE中包含(n-1)条边为止。
代码
typedef struct
{
int u; //边的起始顶点
int v; //边的终止顶点
int w; //边的权值
} Edge;
void Kruska1(Adj Graph* g)
{
inti, j, k, u1, v1, sn1, sn2;
UFSTree t[MAXSize]; //并查集,树结构
ArcNode* P;
Edge E[MAXSize];
k = 1; //e数组的下标从1开始计
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;
}
Heapsort(E, g.e);//采用堆排序对E数组按权值递增排序
MAKE_SET(t, g.n);//初始化并 查集树t
k = 1;//k表示当前构造生成树的第几条边,初值为1
j = 1;//E中边的下标,初值为1
while (k < g.n)//生成的边数小于n时循环
{
u1 = E[j].u;
v1 = E[j].v;//取一一条边的头尾顶点编号u1和v2
sn1 = FIND_SET(t, u1);
sn2 = FIND_SET(t, v1);//分别得到两个顶点所属的集合编号
if (sn1 != sn2)//两顶点属不同集合
{
printf(" (%d, %d) : %d
", u1, v1, E[j].w);
k++;//生成边数增1
UNION(t, u1, v1); //将u1和v1两个顶点合并
}
j++;//下一条边
}
}
辅助数据结构是数组vest[],集合应用,用来判断该条边加入后是否会形成回路
该算法采用邻接表结构更合适
该算法适用于稀疏图
该算法时间复杂度:O(n^2)-->O(eloge)
1.4 最短路径
- 1.4.1 Dijkstra算法求解最短路径
Dijkstra算法算是贪心思想实现的,首先把起点到所有点的距离存下来找个最短的,然后松弛一次再找出最短的,所谓的松弛操作就是,遍历一遍看通过刚刚找到的距离最短的点作为中转站会不会更近,如果更近了就更新距离,这样把所有的点找遍之后就存下了起点到其他所有点的最短距离。
Dijkstra算法流程
代码
void Dijkstra(Graph G,int v)
{
int dist[MAXV],path[MAXV];
int s[MAXV];
int mindis,i,j,u;
for (i=0;i<G.n;i++)
{
dist[i]=G.edges[v][i]; //距离初始化
s[i]=0; //s[]置空
if (G.edges[v][i]<INF) //路径初始化
path[i]=v; //顶点v到i有边时
else
path[i]=-1;
}
s[v]=1; //源点v放入S中
for (i=0;i<G.n;i++) //循环n-1次
{
mindis=INF;
for (j=0;j<G.n;j++)//找最小路径长度顶点u
{
if (s[j]==0 && dist[j]<mindis)
{
u=j;
mindis=dist[j];
}
}
s[u]=1; //顶点u加入S中
for (j=0;j<G.n;j++) //修改不在s中的顶点的距离
{
if (s[j]==0)
{
if (G.edges[u][j]<INF && dist[u]+G.edges[u][j]<dist[j])
{
dist[j]=dist[u]+G.edges[u][j];
path[j]=u;
}
}
}
}
//输出最短路径
}
数组dist[]:源点v到每个终点的最短路径长度
数组path[]:最短路径序列的前一顶点的序号,初值或无路径用-1表示
数组s[]:表示最短路径顶点集合
适合适用邻接矩阵存储
算法时间复杂度为O(n^2)
- 1.4.2 Floyd算法求解最短路径
每一个顶点都是出发访问点 ,所以需要将每一个顶点看做被访问顶点,求出从 每一个顶点到其他顶点的最短路径 。
void Floyd(MatGraph g)//求每对顶点之间的最短路径
{
int A[MAXVEX][MAXVEX]; //建立A数组
int path[MAXVEX][MAXVEX]; //建立path数组
int i, j, k;
for (i = 0;i < g.n;i++)
for (j = 0;j < g.n;j++)
{
A[i][j] = g.edges[i][j];
if (i != j && g.edges[i][j] < INF)
path[i][j] = i; //i和j顶点之间有一条边时
else
path[i][i] = -1;//i和j顶点之间没有一条边时
}
for (k = 0;k < g.n;k++) // 求A[i][j]
{
for (i = 0;i < g.n;i++)
for (j = 0;j < g.n;j++)
if (A[i][j] > A[i][k] + A[k][i])//找到更短路径
{
A[i][j] = A[i][k] + A[k][j];//修改路径长度
path[i][j] = k;//修改经过顶点k
}
}
}
A[i][j]表示顶点i到j经由某顶点时的最短路径长度
path[]数组保存最短路径
算法时间复杂度O(n^3)
最短路径算法还有其他算法,可以自行百度搜索,并和教材算法比较。
1.5 拓扑排序
将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。
在一个有向图中找一个拓扑序列的过程称为拓扑排序。序列必须满足条件:
每个顶点出现且只出现一次。
若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
有向无环图才有拓扑排序,图中有回路,无法拓扑排序.拓扑排序可以用来检测图中是否有回路.
代码
typedef struct //表头结点类型
{
Vertex data; //顶点信息
int count; //存放顶点入度
ArcNode *firstarc; //指向第一条边
}VNode;
void TopSort(AdjGraph *G) //拓扑排序算法
{
int i,j;
int St[MAXV],top=-1; //栈St的指针为top
ArcNode *p;
for (i=0;i<G->n;i++) //入度置初值0
G->adjlist[i].count=0;
for (i=0;i<G->n;i++) //求所有顶点的入度
{
p=G->adjlist[i].firstarc;
while (p!=NULL)
{
G->adjlist[p->adjvex].count++;
p=p->nextarc;
}
}
for (i=0;i<G->n;i++) //将入度为0的顶点进栈
if (G->adjlist[i].count==0)
{
top++;
St[top]=i;
}
while (top>-1) //栈不空循环
{
i=St[top];top--; //出栈一个顶点i
printf("%d ",i); //输出该顶点
p=G->adjlist[i].firstarc; //找第一个邻接点
while (p!=NULL) //将顶点i的出边邻接点的入度减1
{
j=p->adjvex;
G->adjlist[j].count--;
if (G->adjlist[j].count==0) //将入度为0的邻接点进栈
{
top++;
St[top]=j;
}
p=p->nextarc; //找下一个邻接点
}
}
}
1.6 关键路径
AOE网(Activity On Edge Network)是边表示活动的网,AOE网是带权有向无环图。边代表活动,顶点代表所有指向它的边所代表的活动 均已完成这一事件。由于整个工程只有一个起点和一个终点,网中只有一个入度为0的点(源点)和一个出度为0的点(汇点)。
用顶点表示事件,用有向边e表示活动,边的权表示活动时间,是一个带权的有向无环图。在AOE网中不应该出现有向环。
关键路径是指有向图中从源点到汇点的最长路径。其中,关键路径中的边叫做关键活动。
步骤:
对图进行拓扑排序。
在拓扑排序得到的序列的基础上,计算出边的最早开始时间和最晚开始时间,分别得到ve和vl数组。ve是当前点到起始点的最长路径,vl有点像是找当前点到终点的最长路径,然后用ve[终点]-最长路径;
计算所有边的e和l,其中,对边,e=ve[i],l=vl[l]-边的权值
如果e=l,那么它就是关键活动,而所有的关键活动相连,就是它的关键路径。
关键词含义
- AOE网一一带权的有向无环图
- 顶点--事件或状态
- 弧(有向边)---活动及发生的先后关系权---活动持续的时间
- 起点--入度为0的顶点(只有一一个)终点--出度为0的顶点( 只有一一个)
2.PTA实验作业
2.1 六度空间
思路:题目要求对每个节点计算符合“六度空间”理论的结点占结点总数的百分比,即计算每个结点中所连接的与该结点距离不超过6的结点数占结点总数的百分比。所以,最主要的就是计算每个节点所连接的距离不超过6的结点个数。
因为六度空间图可当作稀疏图来处理,所以我选择使用邻接表存储结构进行解题。又因为对每个结点距离为6以内结点进行计算,更适合使用BFS广度遍历,一层一层向外遍历,在遍历到第6层时跳出该节点的遍历,可得该距离内的结点数。
伪代码
使用邻接表存储结构,建图
for i = 1 to n 遍历每个结点
{
使用BFS遍历计算距离为6以内的结点个数BFS(G,i);
}
使用BFS遍历int BFS(AdjGraph* G, int v)
{
新建立一个队列q;
定义lastnode, node记录每一层的最后结点,visited[MAX]是否访问的标志,level记录层次,count记录结点个数;
对visited初始化为0
v结点入队q.push(v),改变visited[v]状态标记已访问;count数量++;
while 队列不为空
{
if 距离level超过6 break;
end if;
取队首元素,进入该节点单链表,遍历链表;
while p不为空
{
if 该节点未被访问
该节点入队,count++;
更新node结点;
end if;
继续遍历p = p->nextarc;
}
if 结点等于最后结点,一层遍历结束
层次增加level++;
最后结点更新为下一层最后一个lastnode = node;
end if;
}
}
提交列表
知识点
- 邻接表的建图和广度遍历BFS
- 使用node和lastnode两个变量来记录每一层次结束的最后结点,从而使level在一层遍历结束后增加
- 使用level记录遍历层次来控制广度遍历所遍历的层次,在达到规定距离时退出循环,得到距离内结点个数
2.2 村村通或通信网络设计或旅游规划
该题求公路连通村庄所需要的最低成本,即建造最小生成树,并求最小生成树中权值的和。使用邻接矩阵更利于得到两点顶点之间的关系,因此我选择本题使用邻接矩阵和Prime算法来计算最小生成树所得最低成本。
伪代码
邻接矩阵建图;
最小生成树Prime算法
int Prim(MGraph* g)
{
建立边权值lowcost,顶点编号clostest两个数组;
给lowcost和clostest初始化置初值;
从顶点1开始lowcost[1] = 0;
for i = 0 to n
{
将最小值min置初值INF;
if 顶点i未被访问过且二者最小边小于最小值min
最小值min等于该最小边;
k记录最小边对应点编号;
end if;
花费cost等于最小边结点lowcost相加;该节点置为已标记状态;
//修正数组lowcost和clostest
for i = 1 to n
{
if 未被访问且结点i与k权值小于二者最小边lowcost
修正lowcost等于edges[i][k];
end if;
}
}
//判断是否连通
for i = 1 to n
{
if lowcost[i] != 0
不连通 return-1;
end if;
}
连通 return cost;
}
提交列表
知识点
- 结构体中用指针edge,后面动态申请空间为 g->edges = new int* [n + 1];
- 本题为最小生成树问题--采用Prim算法,若用floyd算法,不能保证任一两点之间是最短路径
- Prim算法需要两个辅助数组,closest[i]为最小生成树的边依附在U中顶点编号,lowcost[i]表示顶点i到U中顶点的边权重