zoukankan      html  css  js  c++  java
  • DS博客作业04--图

    这个作业属于哪个班级 数据结构-网络20
    这个作业的地址 DS博客作业04--图
    这个作业的目标 学习图结构设计及相关算法
    姓名 余智康

    目录

    0. 展示PTA总分

    1. 本章学习总结

    2. PTA实验作业



    0.PTA得分截图


    1.本周学习总结

    本次所有总结内容,请务必自己造一个图(不在教材或PPT出现的图),围绕这个图展开分析。建议:Python画图展示。图的结构尽量复杂,以便后续可以做最短路径、最小生成树的分析。

    该图引自被B站 "《空洞骑士》:人物关系图谱",地址:https://www.bilibili.com/read/cv6403674/

    修改后的关系图(部分):



    1.1 图的存储结构

    1.1.1 邻接矩阵

    造一个图,展示其对应邻接矩阵
    邻接矩阵的结构体定义
    建图函数

    • 结构体:
      • 1)顶点的结构体:顶点编号、其它信息
      • 2)图的结构体:二维数组(存放权值)、一维数组(存放顶点信息)、整型变量(存放顶点数、边数)
      #define MAXV 100   //最大顶点数
      typedef struct
      {
           int no;          //顶点编号
           InfoType info;   //其它信息
      }VertexType;          //顶点结构体
    
      typedef struct
      {
          int weight;  //边
          string relation;   //关系
      }EDGES;
    
      typedef struct 
      {
            EDGES edges[MAXV][MAXV];  //邻接矩阵
            int n,e;        //顶点数、边数
            VertexType vexs[MAXV];  //存放顶点信息
      }MatGraph;            //图的结构图
    
    • 存储:

      • 以 “空洞骑士关系图(部分)” 为例
    • 申请空间:
      1)因为0不使用,所以申请 node+1 个。
      2)先给二级指针申请 node+1 个存放一级指针的空间,再 循环 给每一个一级指针申请 node+1个 存放结点数据的空间

      	g.edges = new EdgesType * [node+1];
    	for (i = 0; i <= node; i++)
    	{
    		g.edges[i] = new EdgesType[node+1];
    	}
    
    
    • 建图(有向图):
      • void CreatNode(),输入结点的信息:编号、名称,存放在 g.vexs[]数组中
      • void CreatMGraph(),
        1)创建邻接矩阵 :①邻接矩阵初始化 -> ②输入结点编号,(输入权值),修改对应位置的权值 -> ③ g.e = e, g.n = n;
        1)建邻接矩阵的循坏条件,for i = 0 to e-1
      void CreatNode(MGraph &g, int n)  //输入结点信息
      {
          int No;
    
          for(int i=1; i<=n; i++)
          {
              cin >> No;
              g.vexs[No].no = No;
        
              cin >> g.vexs[No].info;
          }
      }
    
      void CreatMGraph(MGraph &g, int n, int e)
      {
            int i,j;  //循环计数变量
            int a,b;  
            
            //二维数组初始化
            for(i=0; i<=n; i++)
               for(j=0; j<=n; j++)
                  g.edges[i][j] = 0;
      
            for(i=0; i<e; i++)
            {
               cin >>a >>b ;
               cin >>g.edges[a][b].relation >>g.edges[a][b].weight;
            }
            
            g.n = n;
            g.e = e;
      }  
    

    1.1.2 邻接表

    造一个图,展示其对应邻接表(不用PPT上的图)
    邻接矩阵的结构体定义
    建图函数

    • 存储:

      • 以 “空洞骑士关系图(部分)” 为例
    • 结构体:

        typedef string InfoType,Vertex;
        typedef struct ANode  //边的结构体
        {
            int adjvex;  //终点编号
            struct ANode *nextarc; //下一条边的指针
            InfoType info;  //该边信息
            double weight;  //该边权值
        }ArcNode;   
    
        typedef struct Vnode  //结点的结构体
        {
            Vertex data;  //顶点信息
            ArcNode *firstarc;  //第一条边
        }VNode;
    
        typedef struct
        {
            VNode adjlist[MAXV];  //邻接表
            int n, e;
        }AdjGraph;
    
    • 建图:
        void CreatAdj(AdjGraph *&G, int n, int e)
        {
              定义 p 为 指向ArcNode数据类型的指针  
              G = new AdjGraph  G动态申请内存空间
              for i=o to n  do
                   G->adjlist[i].firstarc = NULL  初始化
              end for
          
              for i=0 to e-1 do
                    输入结点编号,a,b
                    p 申请内存空间, 
                    p -> adjvex = b;  b为终点编号
                    头插法将 p 插入 G->adjlist[a].fistarc
              end for
       
              G->n = n; G->e = e;
         }
    
    

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

    各个结构适用什么图?时间复杂度的区别。

    • 邻接矩阵:
      • 适用于稠密图,稀疏图使用邻接矩阵会有较大的空间浪费
      • 因为需要遍历二维数组,时间复杂度为 O(n^2)
    • 邻接矩阵:
      • 适用于稀疏图
      • 时间复杂度为 O(n+e)

    1.2 图遍历

    1.2.1 深度优先遍历

    选上述的图,继续介绍深度优先遍历结果
    深度遍历代码
    深度遍历适用哪些问题的求解。(可百度搜索)

    • "Wyrm"开始深度遍历结果:

      • Wyrm(1) -> The Pale King(3) -> Herrah the Beast(2) -> Hornet(5) -> The White Lady(4) -> Dryya(6) -> Hegemol(7) -> False Knight(13) -> Maggot(12) -> Isma(8) -> Ogrim(9) -> Ze'mer(10) -> The daught of The Lord(11)
    • 程序运行截图:

    • 深度遍历代码:

      void DFS(MGraph &g, int v)    
      {
          if(v > g.n)  return;      
    
          cout输出结点信息
          visited[v] = 1;  //标记为已读
    
          while (未遍历完 v 的出度结点)
          {
    	   if 未访问过的结点 && 有权值  do
    		DFS(g, i) 进行递归
               end if
              
               移动到 v 的下一个出度结点
          }
      }
    
    

    1.2.2 广度优先遍历

    选上述的图,继续介绍广度优先遍历结果
    广度遍历代码
    广度遍历适用哪些问题的求解。(可百度搜索)

    • "Wyrm"开始广度遍历结果:

      • Wyrm(1) -> The Pale King(3) -> Herrah the Beast(2) -> The White Lady(4) -> Hornet(5) -> Dryya(6) -> Heqemol(7) -> Isma(8) -> Ogrim(9) -> Ze'mer(10) -> False Knight(11) -> The daught of The Lord(13) -> Maggot(12)
    • 程序运行截图:

    • 广度遍历伪代码:

    void BFS(MGraph g, int v)
    {
        if (v 结点没有出度 )  do 输出 v结点的信息  return
    
        queue<int>que;   //邻接表和临界矩阵中,队列类型均为 int型,之前邻接表犯过将其设为 ArcNode* 型的错误
    
        v 入队列
        记录 v 已经访问
    
        while(队列不空)
        {
            取队头,赋值到 p中;  并出队;
            cout 输出 p的信息
        
            now 赋值为 p第一个出度结点(邻接表中ArcNode*)
    
            while( p 的出度结点 未遍历 完)  do
              if 该出度结点未访问过 && 有权值 do
                  该结点 编号 入队列;并记录为已访问
              end if
            end while
        }          
    }
    
    • 问题的求解
        1. 只要找到问题的一种解决方式时,深度遍历比较快速,但不一定找到问题的最优解;
        1. 在问题存在多种解决方式时,广度遍历能给出全部可行的解决方案,可用于比较,从而选出最可能的解决方案;
        1. 深度遍历如本学期学习数据结构时,在线性表章节的迷宫寻路问题,一开始便是深度搜索找到一条路径;当不仅仅要找到走出迷宫的路径,还要比较多种路径,从而找出最短路径时,则使用了广度搜索。

    1.3 最小生成树

    用自己语言描述什么是最小生成树。

    • 自己的语言解释最小生成树:
      • 生成树:由图的 n 个顶点去掉多余的边由(n-1)条边连接的树。
      • 最小生成树:权值之和最小的生成树
      • 生成树是树,不是图,没有箭头。

    1.3.1 Prim算法求最小生成树

    基于上述图结构求Prim算法生成的最小生成树的边序列
    实现Prim算法的2个辅助数组是什么?其作用是什么?Prim算法代码。
    分析Prim算法时间复杂度,适用什么图结构,为什么?

    • Prim算法:

      • Prim算法构造最小生成树的过程就是将右边圆圈中“候选区”的结点不断选中并放入左边矩形的“选中区”中;
    • 辅助数组:

      • lowcost[i]: lowcost也就是“候选区”,表示以i为出度的边的最小权值。当lowcost[i] = 0时,表示 i 结点已经加入到了最小生成树中;
      • closet[i]: closet[i] 对应lowcost[i]的入度,则<closet[i],i>是最小生成树的一条边。其中 closet[i] = 0 表示以 i 结点作为树的根节点。
    • Prim算法操作:

    1. 初始化lowcost[], 其中lowcost[i]的值为v为入度,i为出度的权
      值,权值为0 则置为INF(∞);

    2. 初始化closet[],其中closet[i] = v。

    3. 从lowcost[]中找到最小的点 j ,进入最小生成树。并将 lowcost[j] = 0;

    4. 遍历顶点,若 weight<j,i>小于 lowcost[i] 则修改 lowcost[i],并修改closet[i]的入度为j。

    5. 循环3、4 总共进行 n-1 次,则n个顶点全部进入最小生成树;

      • Prim代码:
    void Prim(MGraph g, int v)
    {
    	int* lowcost = new int[g.n + 1];
    	int* closet = new int[g.n + 1];
    	int min, node_min;
    	int i, k;
    
    	//1. 初始化lowcost[], 其中lowcost[i]的值为v为入度,i为出度的权值,权值为0 则置为INF(∞);
    	//2. 初始化closet[],其中closet[i] = v。
    	for (i = 0; i <= g.n; i++)
    	{
    		if (g.edges[v][i].weight != 0)
    		{
    			lowcost[i] = g.edges[v][i].weight;
    		}
    		else
    		{
    			lowcost[i] = INF;
    		}
    
    		closet[i] = v;
    	}
    	lowcost[v] = 0;						
    					  
    	for (k = 0; k < g.n - 1; k++)
    	{
    		//3.  从lowcost[]中找到最小的点 j ,进入最小生成树。并将 lowcost[j] = 0;
    		min = INF;
    		for (i = 0; i <= g.n; i++)
    		{
    			if (lowcost[i] > 0 && lowcost[i] < min)
    			{
    				min = lowcost[i];
    				node_min = i;
    			}
    		}
    		lowcost[node_min] = 0;
    		cout << closet[node_min] << " and " << node_min << " weight-> " << min << endl;
    
    		//4. 遍历顶点,若 weight<j, i>小于 lowcost[i] 则修改 lowcost[i],并修改closet[i]的入度为j。
    		for (i = 0; i <= g.n; i++)
    		{
    			if (g.edges[node_min][i].weight != 0 && g.edges[node_min][i].weight < lowcost[i])
    			{
    				lowcost[i] = g.edges[node_min][i].weight;
    				closet[i] = node_min;
    			}
    		}
    	}
    	//5.循环3、4 总共进行 n - 1 次,则n个顶点全部进入最小生成树; 
    
    	delete[] lowcost;
    	delete[] closet;
    }
    
    • 敲Prim代码时的 错误日志

      • 错误一: 333 行 if (lowcost[i] != 0 && lowcost[i] < min) 误写为 if (lowcost[i] != 0 && min < lowcost[i])
      • 错误二: 误将 330行的 min = INF放入 for循环中,导致找不到最小值
      • 错误三: 339行、340行的 i 改为 node_min
      • 错误四: 343行-351行的 for循环 内使用了未初始化的 j,改为node_min
      • 错误五: 304、305行 new的空间 误写成 new lowcost = int[v+1],导致申请的空间不足,出错。改为new lowcost = int[g.n + 1]
      • 错误六: 345行 if语句无需判断 lowcost[i] 是否为0, 改为判断 g.edges[node_min][i].weight 是否为0
      • 错误七: 312行 lowcost[g.n]最后一个结点没有初始化
    • Prim算法得到的边序列

    • Prim时间复杂度:

      • 两层for循环嵌套,时间复杂度 O(n^2)
    • Prim适用的图:

      • 由于Prim算法和边数无关,执行的次数和顶点个数 n 有关,故比较适合稠密图。

    1.3.2 Kruskal算法求解最小生成树

    基于上述图结构求Kruskal算法生成的最小生成树的边序列
    实现Kruskal算法的辅助数据结构是什么?其作用是什么?Kruskal算法代码。
    分析Kruskal算法时间复杂度,适用什么图结构,为什么?

    • Kruskal算法:

      • 1)ST 为空集
      • 2)按边的权值 从小到大 放入边集 E 中
      • 3)依次从 E 选择边放入 ST 中
      • 3)筛选时,若所选择的边放入 ST 中构成了回路,则 舍弃 该边
      • 4)返回步骤 3),直到 ST 中包含(n-1)条边
      • 备注:工具树 并查集 同一个根 回路
    • 辅助数组:

      • 1)边集合 E,按权值 从小到大 存放边,E的结构体里的数据项: 初始顶点、终止顶点、边的权值
      • 2)vset数组,用于并查集。存放每个结点的根,判断两个结点的根是否相同,从而判断是否在 同一个工具树 上,若在,则这两个结点会构成回路,舍弃该边。
    • Kruskal伪代码:

      for i=0 to G.n do
        vset[i] = i;  //初始化
      end for
    
      for i=1 to G.n do
        p = G.adjlist[i].firstacr;
        while p!=NULL do
            将 p 中的终点编号 和 权值 以及 i(起始结点编号)放入 E[]中
            p = p-> nextarc;    
        end while
      end for
    
      调用函数,将 E[]排序
    
      重置i,j未0
      while i < G.n-1 do
        取出 E[j]的结点编号,权值
    
        检查两个结点是否存在于同一个集合
        if vset[node_1] != vset[node_2]
            for k=0 to G.n
                修改集合编号
            end for
            输出
         end if
      end while        
    
    
    • Kruskal算法得到的边序列

    • Kruskal时间复杂度:

      • 上面那个的 Kruskal 代码的时间复杂度应该为 O(n^2)。
      • 若将排序改为 堆排序,而且使用 并查集 ,则时间复杂度为 O(e log2 e)
    • Kruskal适用的图:

      • Kruskal 算法的时间复杂度为 O(e log2 e),与图的结点数 n 无关,仅与边数 e 有关。则适合于稀疏图

    1.4 最短路径

    1.4.1 Dijkstra算法求解最短路径

    基于上述图结构,求解某个顶点到其他顶点最短路径。(结合dist数组、path数组求解)
    Dijkstra算法需要哪些辅助数据结构
    Dijkstra算法如何解决贪心算法无法求最优解问题?展示算法中解决的代码。
    Dijkstra算法的时间复杂度,使用什么图结构,为什么。

    • **基于上图,求 结点 “Wyrm(1)”到 “Ogrim(9)”的最短路径。(图选的不成,这最短路径0.0

      • Dijkstra:
        • S 已经选入的点的集合; T 为选入点的集合;
        • 1)若存在 <V0,vi>, 距离的值为<V0,Vi>弧上的权值;
        • 2)若不存在,则距离为 INF(无穷);
        • 3)从 T 中选择一个距离值最小的顶点W,加入 S;
        • 4)在 W 加入 S 后,修改 T 中的距离值;
        • 重复 3)4)直到 S 中包含所有顶点;
    • 辅助数组:

      • 存放最短路径长度:一维数组 dist[],源点 v 默认
        )其中dist[j] 表示源点 -> 顶点 j 的最短路径长度。
        )dist[2]=12 表示源点 -> 顶点 2 的最短路径长度为12

      • 存放最短路径:一维数组 path[]
        ) 一条最短路径用一个一维数组表示,如顶点1 -> 9的 最短路径为 1、3、9。表示为 path[] = {1,3,9}
        )最短路径序列的前一项的顶点编号,无路径用 -1 表示

    • 结果: Wyrm(1) -> The Pale King(3) -> Ogrim(9)

    • 过程:

    • Dijkstra如何解决贪心算法的问题

      • 通过修改 dist[]数组,使每次选中的结点到源点的距离均是最短;
      • 代码:
      for(j=0;j<g.n;j++)
      {
        if s[j] == 0 
          if g.edges[u][j] < INF && dist[u]+g.edges[u][j] < dist[j] do
            { 
              dist[j]=dist[u]+g.edges[u][j];
              path[j]=u;
            }
       }   
    
    
    • Dijkstra的时间复杂度,适合的图结构:
      • 两层 for循环 嵌套, O(n^2)
      • 适合 邻接矩阵 存储
      • 备注:Dijkstra算法 不适用于带负数权值的带权图求最短;
        —————不适用求最长路径:由于找到一个当前距离源点S最远的A点,则该段路径固定,但可能存在源点 S 到 B点,B点到A点,这两段加起来比 直接 S到A点更远。

    1.4.2 Floyd算法求解最短路径

    Floyd算法解决什么问题?
    Floyd算法需要哪些辅助数据结构
    Floyd算法优势,举例说明。
    最短路径算法还有其他算法,可以自行百度搜索,并和教材算法比较。

    • Floyd算法的辅助数组:

      • 二维数组 A 存放当前顶点之间的最短路径长度,如 A[i][j]表示当前顶点i到顶点j的最短路径长度
      • 完了这个蛤子Floyd看不懂,来不及了,来不及了,这个就放着吧(;′⌒`)
    • Floyd算法解决哪些问题:

    • Floyd算法的优势:

    • 其它最短路径的算法:


    1.5 拓扑排序(

    找一个有向图,并求其对要的拓扑排序序列
    实现拓扑排序代码,结构体如何设计?
    书写拓扑排序伪代码,介绍拓扑排序如何删除入度为0的结点?
    如何用拓扑排序代码检查一个有向图是否有环路?

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

    • 拓扑排序序列:

      • 1)有向图中删去一个 没有前驱 的顶点,输出;
      • 2)删去 以该顶点为 的弧;
      • 3)重复 1)2)直至 图空 ,或 图不空但找不到无前驱顶点 为止
      • 备注: 若图不空 但找不到无前驱顶点,则是 存在环
    • 上图 拓扑排序 结果:

    • 拓扑排序结构体:

      • !表头结点增加了 顶点入度的数据项
      typedef struct
      {
          vertex data;  //顶点信息
          int count;    //入度
          ArcNode* firstarc;   //第一条弧
      }VexNode;
    
    • 拓扑排序伪代码:
      遍历邻接表
        计算每个顶点的入度,存放在 表头结点的 count 中
      遍历图顶点
        将 入度为0 的顶点,入栈, 删除该结点
      while 栈不空 do
        出栈,v 结点 p = G->adjlist[v].firstarc
        while p != NULL do
          v 所有邻接点的入度 -1;
          若 此时 入度为0 则入栈, 并在图中删去该结点
          p = p->nextarc;
        end while
      end while
    
    • 拓扑排序删去结点:

      • 类似链表的删除操作
      • 1)先判断 v 结点的第一条弧是否为空
      • 2)判断第一个邻接点 的入度在 -1后 是否为 0
      • 2) ① 若 -1后入度为0,则将该邻接点入栈。 并修改表头结点的 firstarc 为 该邻接点的nextarc,再delete掉该邻接点。最后重复 2)直到第一个邻接点的入度不为 0 或是 第一个邻接点不存在
      • 2) ② 若 -1后入度不为0,则 p = firstarc,以 while(!p->nextarc)为循环条件,if(G->adjlist[p->nextarc.adjvex].count == 0) 则 用 temp存放 p->nextarc, 再 p = p->nextarc->nextarc,最后delete temp
    • 检查一个有向图是否有环路:
      * 1)在拓扑排序时,把有向图的顶点 真正上删除 ,然后再拓扑排序完遍历一下,看看图是否为空。若图不为空,则有环。
      * 1)或者,假的删除,将拓扑排序中删去的顶点的 count 项赋值为 -1.拓扑排序完,统计是否有结点的 count 项不为 -1,则有环


    1.6 关键路径(

    什么叫AOE-网?
    什么是关键路径概念?
    什么是关键活动?

    • AOE-网:

      • AOE网 为带权值的有向无环图;
      • 顶点 为事件或状态;
      • 弧 为活动发生的先后关系;
      • 权值 为活动持续的时间
      • 起点 入度为 0 的顶点 (仅有一个)
      • 终点 出度为 0 的顶点 (仅有一个)
    • 关键路径概念:

      • 关键路径 为源点到汇点的 最长 路径, -> 转变为图的最长路径问题
      • 备注: 求图的最长路径 不能 使用求最短路径的 Dijkstra算法实现。
    • 关键活动:

      • 关键活动: 在关键路径上的活动 都是关键活动
      • 关键活动不存在富余时间


    2.PTA实验作业

    2.1 六度空间

    选一题,介绍伪代码,不要贴代码。请结合图形展开分析思路。

    2.1.0 思路

    • 计数,距离不超过6
    • 遍历
    • 层次遍历! 如树的层次遍历并输出高度, 队列!lastNode!

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

      建图;
      for i=1 to G->n do
        //CountNode(G,6,i)用于依次求得 每个顶点距离6以内的顶点数,并返回
        proportion = (double)CountNode(G, 6, i) * 1.0 / node * 100;
        输出
      end for
    
      int CountNode(AdjGraph* G, int distance, int v)
        v 入队列,并标记为已读,同时count++ //(每次有数据进入队列,则count++)
    
        while 队列不空 do
          取队头,出队
          取队头 的第一条弧 // p = firstarc
          while p 不为空
            若p->No结点未访问过,入队列,已访问,count++
            p移动到 p->nextarc
          end while
        end while
    

    2.1.2 提交列表

    2.1.3 本题知识点

    • 图的层次遍历
    • 层次遍历中使用 lastNode 确定距离

    2.2 旅游规划

    2.2.1 思路:

    • 最短路径 -> Dijkstra算法 -> 邻接矩阵 -> 无向图还是有向图 (思考:先试用有向图)
    • 相同距离 取费用最低 -> Dijkstra算法使用时遇到多个相同的路径长度,path用二维数组(思考:string是否有一维数组的数据类型,可否使用),dist使用二维数组,与path对应。同时新引入一维cost数组,用于比较费用 -> 得出适合路径。
    • (思考:邻接矩阵的二维数组是否可以使用结构体,用来存放距离和花费)

    2.2.1 伪代码(贴代码,本题0分)
    伪代码为思路总结,不是简单翻译代码。

    部分结构体
    typedef struct
    {
        int dist;
        int cost;
    }DIST_COST;
    
    typedef struct
    {
        DIST_COST** edges; 
        int n,e;
    }MGraph;
    
    建图
    
    ......
    

    2.2.2 提交列表
    2.2.3 本题知识点

  • 相关阅读:
    MyBatis中Like语句使用总结
    使用dom4j解析xml为json对象
    实体类里布尔类型在数据库里可以用整型映射
    Java枚举根据key获取value
    Oracle和Mysql中mybatis模糊匹配查询区别
    解决3 字节的 UTF-8 序列的字节 3 无效
    git上传代码到github
    OpenShift 3.11离线环境的jenkins演示
    OpenShift下的JVM监控
    OpenShift 4.1 演示
  • 原文地址:https://www.cnblogs.com/welcome-to-tomorrow/p/14770993.html
Copyright © 2011-2022 走看看