DS博客作业04--图
0.PTA得分截图
1.本周学习总结
1.1 图的存储结构
1.1.1 邻接矩阵
- 结构体定义
//图的邻接矩阵
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,弧数
} MGraph; //图的邻接矩阵表示类型
- 建图
void CreateMGraph(MGraph& g, int n, int e)//建图
{
int a, b;
//初始化矩阵
for (int i = 1; i <= MAXV; i++) {
for (int j = 1; j <= MAXV; j++) {
g.edges[i][j] = 0;
}
}
//填入对应
for (int i = 1; i <= e; i++) {
cin >> a >> b;
g.edges[a][b] = 1;//无向图需要两个边都为一
g.edges[b][a] = 1;
}
g.e = e;
g.n = n;
}
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 a, b;
ArcNode* p;
G = new AdjGraph;
//初始化
for (int i = 0; i < n; i++)G->adjlist[i].firstarc = NULL;
for (int i = 1; i <= e; i++) {
cin >> a >> b;
//建立a与b之间的关系
p = new ArcNode;
p->adjvex = b;
p->nextarc = G->adjlist[a].firstarc;
G->adjlist[a].firstarc = p;
//建立b与a之间的关系
p = new ArcNode;
p->adjvex = a;
p->nextarc = G->adjlist[b].firstarc;
G->adjlist[b].firstarc = p;
}
G->n = n; G->e = e;
}
1.1.3 邻接矩阵和邻接表表示图的区别
- 邻接矩阵:
用二维数组存储内容,故时间复杂度为O(n^2),edge[i][j]即可判断两顶点是否相连,但如果在栈区申请则不能太大,可以使用动态内存申请堆区的空间
邻接矩阵适合稠密图 - 邻接表:
用链表存储数据,故时间复杂度为O(nlogn),适合稀疏图
如果图中边的数目远远小于n^2称作稀疏图,这是用邻接表表示比用邻接矩阵表示节省空间;
如果图中边的数目接近于n^2,对于无向图接近于n*(n-1)称作稠密图,考虑到邻接表中要附加链域,采用邻接矩阵表示法为宜。
1.2 图遍历
1.2.1 深度优先遍历
- 深度优先遍历--DFS
//邻接矩阵
void DFS(MGraph g, int v)//深度遍历
{
int i;
//控制空格输出
if (flag==0) {
cout << v;
flag = 1;
}
else cout << " " << v;
visited[v] = 1;//标记已经走过的点
for (i = 1; i <= g.n; i++) {
if (g.edges[v][i]&&!visited[i]) DFS(g, i);
}
}
//邻接表
void DFS(AdjGraph* G, int v)//v节点开始深度遍历
{
ArcNode* p;
visited[v] = 1;//置已访问标记
//控制空格输出
if (flag == 0) {
cout << v;
flag = 1;
}
else cout << " " << v;
p=new ArcNode;//用于遍历v后面的链表
p = G->adjlist[v].firstarc;
while (p != NULL){
if (!visited[p->adjvex])
DFS(G, p->adjvex);
p = p->nextarc;
}
}
- 深度遍历的应用
- 运用深度优先搜索,对一个有向无回路图DAG进行拓扑排序;
- 用于迷宫求解
- 可以判断是否为强连通图
void DFSTraverse(Graph g){
int count=0;
for(int v=0;v<g.n;v++) visited[v]=FALSE;
for(int v=0;v<g.n;v++){
if(!visited[v]){
DFS(g,v);
count++;//记录有几个连通分量
}
}
}
1.2.2 广度优先遍历
- 广度优先遍历--BFS
//邻接矩阵
void BFS(MGraph g, int v)//广度遍历
{
int f = 0,r=0,k;
int que[MAXV*5];//队列辅助
//控制空格的输出
if (flag) {
cout << v;
flag = 0;
}
visited[v] = 1;//标记已经走过的点
que[r++] = v;
while (f!=r) {
k = que[f++];
for (int j = 1; j <= g.n; j++) {
if (g.edges[k][j] && !visited[j]) {
cout << " " << j;
visited[j] = 1;
que[r++] = j;
}
}
}
}
//邻接表
void BFS(AdjGraph* G, int v) //v节点开始广度遍历
{
queue<int> q;
int w;
ArcNode* p;
q.push(v);//第一个结点入队列
visited[v] = 1;
cout << v;
while (!q.empty()) {
w = q.front();//访问队头
q.pop();
p = new ArcNode;
p = G->adjlist[w].firstarc;//访问w第一条边
while (p != NULL){
w = p->adjvex;//边的邻接点
if (!visited[w]){ // 若当前邻接点未被访问
q.push(w);//该顶点进队
visited[w] = 1;//置已访问标记
cout << " " << w;
}
p = p->nextarc; //找下一个邻接点
}
}
}
广度遍历应用
- 图的BFS算法可以用来求从图中一个顶点到其余各个顶点的最短路径。
如果对图中每个顶点都使用一次BSF,就可以求出从图中每个顶点到其余各个顶点的最短路径
1.3 最小生成树
假设,我们要在n个城市中建立一个通信网络,则连通这n个城市需要布置n-1条通信线路,
这个时候我们需要考虑如何在成本最低的情况下建立这个通信网?---最小生成树
最小生成树就是将每两个顶点之间的权值最小,成本最低,建立图结构
- 三个原则
1.必须只使用该网络中的边来构造最小生成树;
2.必须使用且仅使用n-1条边来连接网络中的n个顶点;
3.不能使用产生回路的边。
1.3.1 Prim算法求最小生成树
- 大致思路
从连通图N={V,E}中的某一顶点U0出发,选择与它关联的具有最小权值的边(U0,v),将其顶点加入到生成树的顶点集合U中。以后每一步从一个顶点在U中,而另一个顶点不在U中的各条边中选择权值最小的边(u,v),把它的顶点加入到集合U中。如此继续下去,直到图中的所有顶点都加入到生成树顶点集合U中为止。
生成的最小生成树的权值是唯一的但是树形可能不唯一
实现Prim算法的辅助数组lowcost与closest - lowcost数组:lowcost[i]存储U-V中 i 顶点到其邻边中的最小边权值
- closest数组:记录最小权值的边对应的顶点
例如
从A开始
{(A,B)}
{(A,B),(B,F)}
{(A,B),(B,F),(F,E)}
{(A,B),(B,F),(F,E),(E,D)}
{(A,B),(B,F),(F,E),(E,D),(D,C)}
void Prim(MGraph g, int v)
{
int lowcost[MAXV], min, closest[MAXV],k;
for (int i = 0; i < g.n; i++) {//初始化lowcost与closest
lowcost[i] = g.edges[v][i];
closest[i] = v;
}
for (int i = 1; i < g.n; i++) { //找出(n-1)个顶点
min = INF;
for (int 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);
lowcost[k] = 0;//标记k已经加入U
for (int 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;
}
}
}
}
- Prime普利姆算法求最小生成树时,和边数无关,只和定点的数量相关,适合求稠密图的最小生成树,其时间复杂度为O(n^2),适合用邻接矩阵建的图
1.3.2 Kruskal算法求解最小生成树
- 大致思路
将所有边按照权值的大小进行升序排序,然后从小到大一一判断,
原则为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。
直到具有 n 个顶点的连通网筛选出来 n-1 条边为止。筛选出来的边和所有的顶点构成此连通网的最小生成树。 - 实现Kruskal算法的辅助数据结构是邻接表,收集边时需要遍历图,使用邻接表可以更快的得到所有边信息
例如
{(F,E)}
{(F,E),(D,C)}
{(F,E),(D,C),(E,D)}
因为(C,E)与(C,F)会使之形成环路,故舍去{(F,E),(D,C),(E,D),(B,F)}
{(F,E),(D,C),(E,D),(B,F),(E,G)}
因为(F,G)与(B,C)会使之形成环路故舍去
void Kruskal(AdjGraph *g)
{
int u1, v1, sn1, sn2, k=1,j;
int vset[MAXV];//集合辅助数组
UFSTree t[MAXV];//并查集,树结构
Edge E[MAXV];//存放所有边
ArcNode* p;
p = new ArcNode;
for (int i = 0; i < g->n; i++) {
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;
}
}
sort(E, E + g->n,cmp);//利用快排sort进行权值递增排序
MakeSet(t, g->n);//初始化并查集树t
k = 1; //k表示当前构造的树是第几条边
j = 1;
while (k < g->n) {
u1 = E[j].u;
v1 = E[j].v;
sn1 = FindSet(t, u1);
sn2= FindSet(t, v1);//得到两个顶点所属集合
if (sn1 != sn2) {//集合不同
printf("(%d,%d):%d\n", u1, v1, E[j].w);
k++;//生成边数+1
Union(t, u1, v1);//将两个顶点合并
}
j++;//进行下一条边
}
}
- Kruskal算法求最小生成树时,需要找到最小边,故与边数有关,适合求稀疏图的最小生成树,其时间复杂度为O(eloge),适合用邻接表建的图
Prim与Kruskal比较 - Prim侧重顶点寻找,Kruskal侧重边的寻找
- Prim适用于稠密图,Kruskal适用于稀疏图
1.4 最短路径
从图的一个点到另一个点到路径不止一条,每条路径的长度可能不同,把路径长度最短的那条叫做最短路径
1.4.1 Dijkstra算法求解最短路径
Dijkstra算法可求得某个顶点到其他顶点的最短路
- Dijkstra算法需要dist[]与path[]辅助,dist[]存最短路径长度,path[]存该点的前驱节点
S | U | dist[] | path[] |
---|---|---|---|
0,7,9,\(\infty\),\(\infty\),14 | 1,1,1,-1,-1,0 |
S | U | dist[] | path[] |
---|---|---|---|
0,7,9,22,\(\infty\),14 | 1,1,1,2,-1,0 | ||
S | U | dist[] | path[] |
---|---|---|---|
0,7,9,20,\(\infty\),11 | 1,1,1,3,-1,3 | ||
S | U | dist[] | path[] |
---|---|---|---|
0,7,9,20,20,11 | 1,1,1,3,6,3 | ||
Dijkstra算法如何解决贪心算法无法求最优解问题?展示算法中解决的代码。
void Dijkstra(MGraph g,int v){
int dist[MAXV],path[MAXV];
int s[MAXV];
int mindis,u;
//dist和path数组初始化
for(int i=0;i<g.n;i++){
dist[i]=g.edges[v][i];
s[i]=0;
if(g.edges[v][i]<INF)path[i]=v;
else path[i]=-1;
}
s[v]=1;//将源点放在S中
for(int i=0;i<g.n;i++){
mindis=INF;
//找最小路径长度顶点u
for(int j=0;j<g.n;j++){
if(s[j]==0&&dist[j]<mindis){
u=j;
mindis=dist[j];
}
}
s[u]=1;//u加入S
for(int 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;
}
}
}
}
Dispath(dist,path,s,g.n,v);//输出
}
- 时间复杂度为O(n^2),采用邻接矩阵表示
1.4.2 Floyd算法求解最短路径
- Floyd算法可以求得任意两个顶点的最短路,
- Floyd算法需要两个二维数组A[][]与path[][]辅助,
- Floyd算法优势:
- Dijkstra不能处理负权图,Flyod能处理负权图;
- Dijkstra需要求dist数组最短路径,Flyod不需要,且代码简短
例如
void Floyd(MGraph g){
int A[MAXV][MAXV];
int path[MAXV][MAXV];
for(int i=0;i<g.n;i++){//初始化A与path
for(int 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;
else path[i][j]=-1;
}
}
for(int k=0;k<g.n;k++){
for(int i=0;i<g.n;i++){
for(int j=0;j<g.n;j++){
if(A[i][j]>A[i][k]+A[k][j]){//找更短路径
A[i][j]=A[i][k]+A[k][j];//进行修改
path[i][j]=k;
}
}
}
}
}
- 该算法时间复杂度为O(N^3),虽然Dijkstra算法也可以求得任意两顶点的最短路,但是Floyd更简洁
- 无负权回路即可,边权可正可负,运行一次算法即可求得任意两点间最短路。
1.4.3 SPFA算法求最短路径
SPFA算法是求解单源最短路径问题的一种算法,
其优于Dijkstra算法的方面是边的权值可以为负数、实现简单,
缺点是时间复杂度过高,高达 O(VE),但算法可以进行若干种优化,提高了效率。
void Spfa(AdjGraph* G,int u) {
ArcNode* p;
int dis[MAXV],vis[MAXV];
for (int i = 1; i <= G->n; i++)dis[i] = INF;//初始化距离为最大值
dis[u] = 0;//起点距离为0
q.push(u);//入队
vis[u] = 1;
while (!q.empty()) {
int k = q.front();
vis[k] = 0;//出队
p = G->adjlist[k].firstarc;
while (p != NULL){
int v = p->adjvex;
if (dis[v] > dis[k] + p->weight) {//松弛
dis[v] = dis[k] + p->weight;
if (!vis[v]) {//没入队就入队
vis[v] = 1;
q.push(v);
}
}
p = p->nextarc;
}
q.pop();
}
}
1.5 拓扑排序
拓扑排序:是一个有向无环图的所有顶点的线性序列。且该序列必须满足下面两个条件:
- 每个顶点出现且只出现一次。
- 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
大致思路:
1.在有向图中选一个没有前驱的顶点并且输出
2.从图中删除该顶点和所有以它为尾的弧(白话就是:删除所有和它有关的边)
3.重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环。
拓扑排序序列为{1,2,4,3,5}
伪代码
void TopSort(AdjGraph* G)
{
将count置初值0
再将所有顶点的入度记录在count中
遍历count
if count==0 进队列
遍历队列
输出顶点
所有点count--
如果 count==0 进队列
}
- 拓扑排序中通过栈或队列将该点移出,实现入度为零的顶点的删除。
结构体
typedef struct ANode
{
int adjvex; //该边的终点编号
struct ANode* nextarc; //指向下一条边的指针
int info; //该边的相关信息,如权重
} ArcNode; //边表节点类型
typedef int Vertex;
typedef struct {
Vertex data;//顶点信息
int count;//存放顶点入度
ArcNode* firstarc;//指向第一条弧
}VNode;
typedef VNode AdjList[MAXV];
typedef struct
{
AdjList adjlist; //邻接表
int n, e; //图中顶点数n和边数e
} AdjGraph;
代码
void TopSort(AdjGraph* G)//邻接表拓扑排序
{
int s[MAXV], top = -1, i,k=0, flag = 0,num[MAXV];
ArcNode* p;
for ( i = 0; i < G->n; i++) G->adjlist[i].count = 0;//入读置初值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) {
s[++top] = i;
}
}
while (top > -1) {
i = s[top--];
flag++;
num[k++] = i;
p = G->adjlist[i].firstarc;
while (p != NULL) {
G->adjlist[p->adjvex].count--;
if (G->adjlist[p->adjvex].count == 0) {//将入度为0的入栈
s[++top] = p->adjvex;
}
p = p->nextarc;
}
}
if (flag != G->n) {
cout << "error!";
return;
}
flag = 0;
for (int j = 0; j < k; j++) {
if (flag == 0) {
cout << num[j];
flag = 1;
}
else cout << " " << num[j];
}
}
用拓扑排序代码检查一个有向图是否有环
最后flag不等于结点数,则没有全部输出所有结点,证明还有结点有入度,则该图有环
1.6 关键路径
AOE-网?
一个工程常被分为多个小的子工程,这些子工程被称为活动,在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间,这样的图简称为AOE网。---带权有向无环图
关键路径
关键路径是从有向图的源点到汇点的最长路径
关键活动
关键路径中的边叫关键活动
2.PTA实验作业
2.1 六度空间
2.1.1 伪代码
void BFS(MGraph& g,int u){
将u顶点标记
初始化dist[u]=0记录距离
将u进队列
cnt++;//用来表示几个人
while q
取出队头,判断距离dist是否大于6
循环矩阵,只要没有被标记并且有边
进队列,距离+1;标记已访问,cnt++
}
2.1.2 提交列表
- 刚开始的多种错误是数组开小了,段错误
- 后来开的过大又错误
- 用指针动态申请,new知识欠缺。。。
2.1.3 本题知识点
- new申请空间:new int* [MAXV + 1]
- dis[]进行距离计算,visited[]进行标记
- 每次每个人都要将数组初始化!!!
2.2 旅游规划
2.2.1 伪代码
void Dijkstra(MGraph g, int v)
{
初始化dist数组、s数组、pay数组,dist数组
遍历图中所有节点
for(i = 0; i < g.n; i++)
若s[i]! = 0,则数组找最短路径,顶点为u
s[u] = 1进s
for(i = 0; i < g.n; i++)
if(g.edges[u][j].len < INF && dist[u] + g.edges[u][j].len < dist[j])
则修正dist[j] = dist[u] + g.edges[u][j].len;
pay[j] = pay[u] + g.edges[u][j].pay;
else if(路径一样长但是花费更少)
则修正pay[j] = pay[u] + g.edges[u][j].pay;
}
2.2.2 提交列表
前面多处错误是因为数组开小了
最后一直不对,我以为是算法出问题了。一直改,结果发现我建的是有向图。。。。(呜呜呜,找一天bug)
2.2.3 本题知识点
- 邻接矩阵,无向图
- 最短路径Dijkstra算法
- 用另一个结构体,存路径长度和费用,并用dist[]与pay[]存储