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算法优势:
  1. Dijkstra不能处理负权图,Flyod能处理负权图;
  2. 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 拓扑排序

拓扑排序:是一个有向无环图的所有顶点的线性序列。且该序列必须满足下面两个条件:

  1. 每个顶点出现且只出现一次。
  2. 若存在一条从顶点 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 本题知识点

  1. new申请空间:new int* [MAXV + 1]
  2. dis[]进行距离计算,visited[]进行标记
  3. 每次每个人都要将数组初始化!!!

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[]存储
posted @ 2021-05-23 20:23  w4  阅读(88)  评论(2编辑  收藏  举报