DS博客作业04--图

DS博客作业04--图


这个作业属于哪个班级 数据结构--网络2011/2012
这个作业的地址 DS博客作业04--图
这个作业的目标 学习图结构设计及相关算法
姓名 姚庆荣

0.PTA得分截图

1.本周学习总结(6分)

1.1 图的存储结构

图的存储主要分两种,一种邻接矩阵,另一种邻接表
用不同的方式存储图的信息

1.1.1 邻接矩阵

无向图: 如果一个图结构中,所有的边都没有方向性,那么这种图便称为无向图。

  • 结构体定义:
typedef struct              //图的定义
{  int edges[MAXV][MAXV];     //邻接矩阵
   int n,e;              //顶点数,弧数
} MGraph;  
//另一种表示方式
typedef struct {
	int** edges;//邻接矩阵
	int n, e;//顶点数,边数
}MGraph;
  • 建图函数:

    void CreateAdj(AdjGraph*& G, int A[MAXV][MAXV], int n, int e)
    {
    	int i, j;ArcNode* p;
    	G = (AdjGraph*)malloc(sizeof(AdjGraph));
    	for (i = 0;i < n;i++)
    		g->adjlist[i].firstarc = NULL;
    	for (i = 0;i < n;i++)
    		for(j=n-1;j>=0;j--)
    			if (A[i][j] != 0 && A[i][j] != INF)
    			{
    				p = (ArcNode*)malloc(sizeof(ArcNode));
    				p->adjvex = j;
    				p->weight = A[i][j];
    				p->nextarc = G->adjlist[i].firstarc;
    				G->adjlist[i].firstarc = p;
    			}
    	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, j, a, b;
    	G = new AdjGraph;
    	for (i = 1; i <= n; i++)//邻接表头结点置零
    	{
    		G->adjlist[i].firstarc = NULL;
    	}
    	for (j = 1; j <= e; j++)//无向图
    	{
    		cin >> a >> b;	
    		ArcNode *p,*q;
    		p = new ArcNode;
    		q = new ArcNode;
    		p->adjvex = b;//用头插法进行插入
    		q->adjvex = a;
    		p->nextarc = G->adjlist[a].firstarc;
    		G->adjlist[a].firstarc = p;
    		q->nextarc = G->adjlist[b].firstarc;
    		G->adjlist[b].firstarc = q;
    	}
    	G->n = n;
    	G->e = e;
    }
    

1.1.3 邻接矩阵和邻接表表示图的区别

  1. 邻接矩阵的空间复杂度为0(n2),而邻接表的空间复杂度为0(n+e)。
  2. 如果图中边的数目远远小于n2称作稀疏图,这时用邻接表表示比用邻接矩阵表示节省空间;
    如果图中边的数目接近于n2,对于无向图接近于n*(n-1)称作稠密图,考虑到邻接表中要附加链域,采用邻接矩阵表示法为宜。因此当数据的边关系较为复杂时用邻接矩阵,稀疏时用领接表,内存空间不会浪费。

1.2 图遍历

1.2.1 深度优先遍历

深度遍历图解
1:需要遍历的图
1:从A点出发,红色代表A点被访问。

2:有A->B,A->C都可行,都可以遍历,这里我选择遍历C。

3:再遍历C之后,也可遍历C->B,C->D 我这里选择遍历C->D。

4:现在由D->E 这里遍历了E。

5:E已经不可以再遍历了。然后返回D,D又不可以遍历B,放回C,然后C->B可以。所以从C->B 就实现遍历。

  • 深度遍历代码

    1. 矩阵

      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);
              }
          }
      }
      
    2. 链表

      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. ABC+DEF=GHI 问题 ;3.二维数组寻找最短路径问题;4.求岛屿的面积.

1.2.2 广度优先遍历

  • 广度遍历代码

    1. 矩阵

      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 +;
                  }
              }
          }
      
      }
      
    2. 链表

      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);
          }
      }
      
  • 广度遍历适用哪些问题的求解。(可百度搜索)

    广度遍历:BFS算法对于解决最短路径问题比较有效。

    问题类型:1.求取最短路径问题;2.求迷宫路径;哪些问题的求解。(可百度搜索)

1.3 最小生成树

有n个顶点的连通网可以建立不同的生成树,每一颗生成树都可以作为一个连通网,当构造这个连通网所花的权值最小时,搭建该连通网的生成树,就称为最小生成树。

1.3.1 Prim算法求最小生成树

  • Prim算法的步骤(引用)

    第一步:随意选取起点

​ 图中有9个顶点v1-v9,集合表示为:V={v1,....,V9},每条边的边权值都在图上;在进行prim算法时,我们先选择v1作为起始点,现在我们设U集合为当前所找到最小生成树里面的顶点,TE集合为所找到的边。

​ 状态如下:U={v1}; TE={};

普利姆算法(prim)求最小生成树(MST)过程详解

​ 第二步:在前一步的基础上寻找最小权值

​ 查找一个顶点在U={v1}集合中,另一个顶点在V-U集合中的最小权值,如下图,在红线相交的线上找最小值。

​ 通过图中我们可以看到边v1-v8的权值最小为2,那么将v8加入到U集合,(v1,v8)加入到TE。

​ 状态如下:U={v1,v8}; TE={(v1,v8)};

普利姆算法(prim)求最小生成树(MST)过程详解

​ 第三步:继续寻找最小权值

​ 查找一个顶点在U={v1,v8}集合中,另一个顶点在V-U集合中的最小权值,如下图,在红线相交的线上找最小值。 通过图中我们可以看到边v8-v9的权值最小为4,那么将v9加入到U集合,(v8,v9)加入到TE。

​ 状态如下:U={v1,v8,v9}; TE={(v1,v8),(v8,v9)};

普利姆算法(prim)求最小生成树(MST)过程详解

​ 第四步:在前一步的基础上,继续寻找最小权值

​ 查找一个顶点在U={v1,v8,v9}集合中,另一个顶点在V-U集合中的最小权值,如下图,在红线相交的线上找最小值。 通过图中我们可以看到边v9-v2的权值最小为1,那么将v2加入到U集合,(v9,v2)加入到TE。

​ 状态如下:U={v1,v8,v9,v2};

​ TE={(v1,v8),(v8,v9),(v9,v2)};

普利姆算法(prim)求最小生成树(MST)过程详解

​ 第五步:继续在前一步的基础上,寻找最小权值

​ 查找一个顶点在U={v1,v8,v9,v2}集合中,另一个顶点在V-U集合中的最小权值,如下图,在红线相交的线上找最小值。通过图中我们可以看到边v2-v3的权值最小为3,那么将v3加入到U集合,(v2,v3)加入到TE。

​ 状态如下:U={v1,v8,v9,v2,v3};

​ TE={(v1,v8),(v8,v9),(v9,v2),(v2,v3)};

普利姆算法(prim)求最小生成树(MST)过程详解

​ 第五~九步:继续在前一步的基础上,寻找最小权值

​ 如此循环一下直到找到所有顶点为止。到这大家应该对普利姆算法求解最小生成树的过程有所知晓,但需注意以下三点:

(1)每次都选取权值最小的边,但不能构成回路,构成环路的边则舍弃。如图中的(v1,v9),(v1,v2)等构成回路舍弃

(2)遇到权值相等,又均不构成回路的边,随意选择哪一条,均不影响生成树结果。如图中的(v3,v4),(v6,v5)权值均为9,选择哪一条在先均不影响最小生成树的生成结果。

(3)选取n-1条恰当的边以连通n个顶点。

  • Prim算法代码

    #define INF 32767
    void Peim(MGraph g, int v)
    {
    	int lowcost[MAXV];
    	int min;
    	int closest[MAXV];
    	int i, j, k;
    	for (i = 0; i < g.n; i++)
    	{
    		lowcost[i] = g.edges[v][i];//置初值,放入顶点v和所有顶带你的权值
    		closest[i] = v;
    	}
    	for (i = 1; i < g.n; i++)//n-1条边,进行n-1次
    	{
    		min = INF;
    		for (j = 0; j < g.n; j++)//遍历找到权值最小的
    		{
    			if (lowcost[j] != 0 && lowcost[j] < min)
    			{
    				min = lowcost[j];
    				k = j;//记录下标
    			}
    		}
    		lowcost[k] = 0;//lowcost为0表示该顶点已使用
    		for (j = 0; i < g.n; j++)//遍历所有顶点,比较找到的顶点与其他顶点的权值是否比原来小
    		{
    			if (lowcsost[j] != 0 && g.edges[k][j] < lowcost[j])
    			{
    				lowcost[j] = g.edges[k][j];
    				closest[j] = k;//改变权值和相邻的顶点
    			}
    		}
    	}
    }
    
  • Prim算法的两个辅助数组是:closest和lowcost。

  • 时间复杂度为O(n^2),prim算法适合稠密图。

1.3.2 Kruskal算法求解最小生成树

  • 实现Kruskal算法的辅助数据结构是什么?其作用是什么?Kruskal算法代码。

    void Kruskal(AdjGraph *g)
    {
     int i,j,u1,v1,sn1,sn2,k;
     int vest[MAXV];//集合辅组数组
     Edge E[MaxSize];//存放所有边
     k=0;//E数组的下标从0开始计
     for(i=0;i<g.n;i++)
       {
          p=g->adjlist[i].firstarc;
          while(p!=NULL)
          {
            E[k].u=i;E[k].v=p->adjlist;
            E[k].w=p->weight;
            k++;p=p->nextarc;
           }
        InsertSort(E,g.e);
        for(i=0;i<g.n;i++)
            vest[i]=i;
        k=1;
        j=0;
        while(k<g.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++;
               for(i=0;i<g.n;i++)
                  if(vest[i]==sn2)
                     vest[i]=sn1;
              }
             j++;
          }
    }
    
  • 分析Kruskal算法时间复杂度,适用什么图结构,为什么?时间复杂的为O(elog2e),由于其与n无关,只与e有关,适用于稀疏图。

1.4 最短路径

有两种最短路径,一种是一个顶点点到其余各顶点之间的最短路径,另一种是任意两点之间的最小路径,分别用Dijkstra算法和Floyd算法来求解

1.4 最短路径

1.4.1 Dijkstra算法求解最短路径

  • Dijkstra算法需要哪些辅助数据结构
    借助最小索引堆作为辅助数据结构
  • Dijkstra算法如何解决贪心算法无法求最优解问题?展示算法中解决的代码
void Dijkstra(MatGraph 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;
if(g.edges[v][I]<INF)
path[I]=v;
else
path[I]=-1;

}
S[v]=1;path[v]=0;
for(I=0;i<g.n-1;i++)
{
MINdis=INF;
for(j=0;j<g.n;j++)
if(S[j]==0&&dist[j]<MINdis)
{
u=j;
MINdis=dist[j];
}
S[u]=1;
for(j=0;j<g.n;j++)
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(g,dist,path,S,v);
}
  • 输出单源最短路径的Dispath()函数如下:
void Dispath(MatGraph g;int dist[],int path[],int S[],int v)
{
int I,j,k;
int apath[MAXV],d;
for(I=0;i<g.n;i++)
if(S[I]==1&&I!=v)
{
printf("从顶点%d到顶点%d的路径长度为:%d\t路径为:",v,i,dist[I]);
d=0;apath[d]=I;
k=path[I];
if(k==-1)printf("无路径\n");
else
{
while(k!=v)
{
d++;apath[d]=k;
k=path[k];
}
d++;apath[d]=v;
printf("%d",apath[d]);
for(j=d-1;j>=0;j--)
printf(",%d",apath[j]);
printf("\n");
}
}
}

4、Dijkstra算法的时间复杂度
Dijkstra算法的时间复杂度为o(n^2),其中n为图中的顶点数。

1.4.2 Floyd算法求解最短路径

  • Floyed算法

    Floyd算法求解最短路径是每个顶点之间的,也可以Dijkstra调用n次,可能达到求解得出每个顶点之间的最短路径。两个时间的复杂性都是O(n^3),不过Floyd形式上更简单点。

    Floyd用一个二维数组A存放当前顶点最短路径长度,如:A[i][j]代表低你干点i到j之间的最短路径。
    还有一个path二维数组,和之前Dijkstra算法中的path的用途一致,用来回溯寻找路径进过的顶点,Floyd中的是每个顶点的集合成二维数组。

  • Floyd算法需要哪些辅助数据结构

    二维数组A用于存放当前顶点之间的最短路径长度,即分量A[i][j]表示当前i->j的最短路径长度。

  • Floyd算法优势,举例说明

    Floyd算法的优势:它能一次求得任何两个节点之间的最短路径,而Dijkstra算法只能求得以特定节点开始的最短路径。

最短路径算法还有其他算法,可以自行百度搜索,并和教材算法比较。

  • SPFA算法(Shortest Path Fast Algorithm的缩写)

    int SPFA(int s, int t) {
    
        int dist[maxn], inq[maxn];
    
        for(int i = 0; i < n; i ++ ) {
    
            dist[i] = inf, inq[i] = 0;
    
        }
    
        queue<int>que;
    
        que.push(s), inq[s] = 1, dist[s] = 0;
    
        while(!que.empty()) {
    
            int now = que.front();
    
            que.pop();
    
            inq[now] = 0;
    
            for(int i = first[now]; ~i; i = edge[i].next) { //每次拿出一个点开始松弛。
    
                int to = edge[i].to, w = edge[i].w;
    
                if(dist[to] > dist[now] + w) { //这个if看下面的图
    
                    dist[to] = dist[now] + w;
    
                    if(!inq[to]) { //松弛过的点dist变换了,可能影响其他的点。需要继续松弛
    
                        inq[to] = 1;
    
                        que.push(to);
    
                    }
    
                }
    
            }
    
        }
    
        return dist[t] == inf ? -1 : dist[t];
    
    }
    
  • 与其他两种算法的比较:

    PFA也叫bellman ford的队列优化。但是bellman ford的复杂度比较高。SPFA的平均复杂度是O(n*log2n),复杂度不稳定,在稠密图(边多的图)跑的比dijkstra慢,稀疏图(边少的图)跑的比Dijkstra快。在完全图达到最坏的平方级复杂度。(引用博文

1.5 拓扑排序

  • 找一个有向图,并求其对要的拓扑排序序列

上有向图的一种拓扑序列1->2->4->3->5;

  • 实现拓扑排序代码,结构体如何设计?

    结构体定义

    typedef struct
    {
    Vertex data;
    int count;
    ArcNode *firstarc;
    }VNode;
    

    代码示例

     void TopSort(AdjGraph *G)	
    {      
            int i,j;
            int St[MAXV],top=-1;	
            ArcNode *p;
            for (i=0;i<G->n;i++)		
    	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++)	
    	 if (G->adjlist[i].count==0)
    	 {	
                top++;
    	    St[top]=i;
    	 }
             while (top>-1)			
             {	  
                i=St[top];top--;			
    	    printf("%d ",i);		
    	    p=G->adjlist[i].firstarc;		
    	    while (p!=NULL)	
    	    {      
                     j=p->adjvex;
    	         G->adjlist[j].count--;
    	         if (G->adjlist[j].count==0)	
    	         {      
                       top++;
    		   St[top]=j;
    	         }
    	         p=p->nextarc;		
    	    }
            }
    }
    

1.6 关键路径

  • 什么叫AOE-网?

在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,一个工程常被分为多个小的子工程,这些子工程被称为活动(Activity),在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间,这样的图简称为AOE网。——百度百科

  • 什么是关键路径概念?

    路径长度:路径上各活动持续时间的总和(即路径上所有权之和)。
    完成工程的最短时间:从工程开始点(源点)到完成点(汇点)的最长路径称为完成工程的最短时间。

    关键路径:路径长度最长的路径称为关键路径。

  • 什么是关键活动?

关键活动:在关键路径上的活动称为关键活动。

2.PTA实验作业(4分)

2.1 六度空间(2分)

2.1.1 伪代码(贴代码,本题0分)

 while (队不为空)
  {
       出队顶点V
       遍历V的所有邻接点
       {
           找到没有遍历过的顶点,进队列
           sum++;
       }

       如果在遍历的时候发现遍历的节点是这一层的最后一个
       { 
           level++;
           更新这层的最后一个
       }
           如果层数为6 //说明已经到了六层,剩下的就是不符合六度空间理论的顶点
           返回我们记录下的sum
  }

2.1.2 提交列表

2.1.3 本题知识点

2.2 村村通(2分)

2.2.1 伪代码(贴代码,本题0分)

Prim算法
 {

    初始化lowcost,closest数组
    for(遍历lowcost数组) 
        if(lowcost[i]!=0 找最下边邻接点node 若权值不为 0 且小于 min)
        计算总费用
        end if
     检查新加入生成树的顶点和其他顶点是否存在更小的边
        if(lowcost[i]!=0&& edges[i][node]<=lowcost[node])
           lowcost[node]=edges[i][node]
        end if
    end for
    if(是否连通)
    for i=1 to g->n
    end if
 }

2.2.2 提交列表

2.2.3 本题知识点

posted on 2021-05-30 23:35  姚庆荣  阅读(101)  评论(1编辑  收藏  举报