• DS博客作业04--图


    0.PTA得分截图

    1.本周学习总结

    1.1 总结图内容

    图的基本概念

    1.有向图:

    • 有向图称由顶点集和弧集构成的图。“弧”是有方向的边,用尖括号表示。
    • 若存在一条边(i,j),则称顶点i和顶点j互为邻接点。
    • 每两个顶点之间都存在着一条边,称为完全无向图, 包含有n(n-1)/2条边。
    • 若从顶点i到顶点j有路径,则称顶点i和j是连通的。若图中任意两个顶点都连通称为连通图,否则称为非连通图。无向图G中的极大连通子图称为连通分量。

    2.无向图:

    • 没有方向边。边用圆括号表示。

    • 存在一条边<i,j>,则称此边是顶点i的一条出边,同时也是顶点j的一条入边;称顶点i 和顶点j 互为邻接点。

    • 每两个顶点之间都存在着方向相反的两条边,称为完全有向图,包含有n(n-1)条边。

    • 若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。 否则,其各个强连通子图称作它的强连通分量。
      3.带权图(网)

    • 图中每一条边都可以附有一个对应的数值,这种与边相关的数值称为权。

    • 边上带有权的图称为带权图,也称作网。

    图存储结构

    邻接矩阵

    1.有向图
    设顶点i,j,若有一条边从i指向j,则将edges[i][j]赋为1,否则为0


    2.无向图
    设顶点i,j,若有一条边连接i和j,则将edges[i][j]和edges[j][i]赋为1,否则为0


    3.带权图
    设顶点i,j,若有一条边从i指向j或连接i和j,则按照有向图或无向图的方法将相应权值赋给二维数组,若i=j,则相应值为0,若i!=j,且不相连,则相应值为无穷大


    4.结构体定义

    typedef struct  			//图的定义
    {    int edges[MAXV][MAXV]; 	//邻接矩阵
         int n,e;  			//顶点数,边数
         VertexType vexs[MAXV];	//存放顶点信息
    }  MatGraph;
     MatGraph g;//声明邻接矩阵存储的图
    

    5.创建图

    void CreateMGraph(MGraph &g, int n, int e)
    {
    	int i, j;
    	g.n = n;
    	g.e = e;
    	int a, b;
    	for (i = 1;i <= n;i++)
    	{
    		for (j = 1;j <= n;j++)
    		{
    			g.edges[i][j] = 0;
    		}
    	}
    	for (i = 0;i < e;i++)
    	{
    		cin >> a >> b;
    		g.edges[a][b] = g.edges[b][a] = 1;
    	}
    }
    
    • 时间复杂度为O(n²)
    • 优点:可以快速查找到两个顶点之间有无边
    • 缺点:浪费空间
    • 若顶点数量过多,可以将edges[][]换为**edges。

    邻接表

    用链存储,将每个顶点所连接的顶点构成一条链,用一维数组将每个顶点存储起来,即将所有链集合
    1.有向图

    2.无向图

    3.带权图

    4.结构体定义

    typedef struct Vnode
    {    Vertex data;			//顶点信息
         ArcNode *firstarc;		//指向第一条边
    }  VNode;
    
    typedef struct ANode
    {     int adjvex;			//该边的终点编号
          struct ANode *nextarc;	//指向下一条边的指针
          InfoType info;		//该边的权值等信息
    }  ArcNode;
    
    typedef struct 
    {     VNode adjlist[MAXV] ;	//邻接表
           int n,e;			//图中顶点数n和边数e
    } AdjGraph;
    AdjGraph *G;//声明一个邻接表存储的图G
    

    5.创建图

    void CreateAdj(AdjGraph *&G, int n, int e) //创建图邻接表
    {
    	int i, j, a, b;
    	ArcNode *p;
    	G = new AdjGraph;
    	for (i = 1;i < n;i++)
    	{
    		G->adjlist[i].data = i;
    		G->adjlist[i].firstarc = NULL;
    	}
    	for (i = 1;i <= e;i++)
    	{
    		cin >> a >> b;
    		ArcNode *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;*/无向图
    	}
    }
    
    • 时间复杂度O(n+e)
    • 优点:空间利用率较高
    • 缺点:不利于判断顶点之间是否有边

    邻接表转邻接矩阵

    void ListToMat(ALGraph *G,MGraph &g)
      {  int i,j,n=G->n;ArcNode *p;
         for (i=0;i<n;i++) 
         {  p=G->adjlist[i].firstarc;
            while (p!=NULL) 
    	  {   g.edges[i][p->adjvex]=1;
    	      p=p->nextarc;
    	  }
         }
         g.n=n;g.e=G->e;
      } 
    

    图遍历及应用

    深度优先遍历

    1.过程

    • 从图中某个初始顶点v出发,首先访问初始顶点v。
    • 选择一个与顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,直到图中与当前顶点v邻接的所有顶点都被访问过为止。
      2.邻接矩阵的深度优先遍历
    void DFS(MGraph g, int v)//深度遍历 
    {
    	visited[v] = 1;
    	if (!flag)
    	{
    		flag = 1;
    		cout << v;
    	}
    	else cout << " " << v;
    	for (int i = 1;i <= g.n;i++)
    	{
    		if (g.edges[v][i] == 1 && (!visited[i]))
    			DFS(g, i);
    	}
    }
    

    3.邻接表的深度优先遍历

    void DFS(AdjGraph *G, int v)//v节点开始深度遍历 
    {
    	ArcNode *p;
    	visited[v] = 1;                   //置已访问标记
    	if(!flag)
        {
            cout<<v;
            flag=1;
        }
        else cout<<" "<<v;
    	p = G->adjlist[v].firstarc;
    	while (p != NULL)
    	{
    		if (visited[p->adjvex] == 0)  DFS(G, p->adjvex);
    		p = p->nextarc;
    	}
    }
    

    4.非连通图
    图还存在非连通的可能,所以考虑该情况的代码

    void DFSTraverse(Graph G) {
       // 对非连通图 G 作深度优先遍历。
      for (v=0; v<G.vexnum; ++v) 
         visited[v] = FALSE; // 访问标志数组初始化
      for (v=0; v<G.vexnum; ++v) 
         if (!visited[v])  DFS(G,v);  // 对尚未访问的顶点调用DFS
    }
    

    广度优先遍历

    1.过程

    • 访问初始点v,接着访问v的所有未被访问过的邻接点。
    • 按照次序访问每一个顶点的所有未被访问过的邻接点。
    • 依次类推,直到图中所有顶点都被访问过为止。
      2.邻接矩阵的广度优先遍历
    void BFS(MGraph g, int v)//广度遍历 
    {
    	int num, front, rear;
    	int Q[MAXV];
    	front = rear = -1;
    	cout << v;
    	visited[v] = 1;
    	Q[++rear] = v;
    	while (front != rear)
    	{
    		v = Q[++front];
    		for (int i = 1;i <= g.n;i++)
    		{
    			if (g.edges[v][i] == 1 && (!visited[i]))
    			{
    				cout << " " << i;
    				visited[i] = 1;
    				Q[++rear] = i;
    			}
    		}
    	}
    }
    

    3.邻接表的广度优先遍历

    void BFS(AdjGraph *G, int v) //v节点开始广度遍历  
    {
    	ArcNode *p;
    	queue<int>qu;
    	visited[v] = 1;
    	cout << v;
    	qu.push(v);
    	while (!qu.empty())
    	{
    		v = qu.front();
    		qu.pop();
    		p = G->adjlist[v].firstarc;
    		while (p != NULL)
    		{
    			if (visited[p->adjvex] == 0)
    			{
    				cout << " " << p->adjvex;
    				visited[p->adjvex] = 1;
    				qu.push(p->adjvex);
    			}
    			p = p->nextarc;
    		}
    	}
    }
    

    4.非连通图

    void  BFS1(AdjGraph *G)
    {      int i;
            for (i=0;i<G->n;i++)     //遍历所有未访问过的顶点
                 if (visited[i]==0) 
                      BFS(G,i);
    }
    

    判断图是否连通

    1.过程

    • 采用某种遍历方式来判断无向图G是否连通。这里用深度优先遍历方法,先给visited[]数组(为全局变量)置初值0,然后从0顶点开始遍历该图。
    • 在一次遍历之后,若所有顶点i的visited[i]均为1,则该图是连通的;否则不连通。
      2.代码
    int  visited[MAXV];
    bool Connect(AdjGraph *G) 	//判断无向图G的连通性
    {     int i;
          bool flag=true;
          for (i=0;i<G->n;i++)		 //visited数组置初值
    	visited[i]=0;
          DFS(G,0); 	
          for (i=0;i<G->n;i++)
                if (visited[i]==0)
               {     flag=false;
    	   break;
               }
          return flag;
    }
    

    找到一条简单路径

    1.过程

    • 采用深度优先遍历的方法。
    • 增加path[i],存放路径。
    • 递归函数添加形参d,表示目前递归深度。path[d]=图结点
    • 当从顶点u遍历到顶点v后,输出path并返回。
      2.代码
    void FindaPath(AGraph *G,int u,int v,int path[],int d)
    { //d表示path中的路径长度,初始为-1
           int w,i;  ArcNode *p;
           visited[u]=1;
           d++; path[d]=u;		//路径长度d增1,顶点u加入到路径中
           if (u==v)			//找到一条路径后输出并返回
           {        printf("一条简单路径为:");
    	  for (i=0;i<=d;i++)  printf("%d ",path[i]);
      	  printf("
    ");
    	  return;         		//找到一条路径后返回
           }
           p=G->adjlist[u].firstarc;  	//p指向顶点u的第一个相邻点
           while (p!=NULL)
           {      w=p->adjvex;		//相邻点的编号为w
    	if (visited[w]==0)
    	     FindaPath(G,w,v,path,d);
    	p=p->nextarc;   		//p指向顶点u的下一个相邻点
           }
    }
    

    找两个顶点中最短路径

    1.过程

    • 采用广度遍历
    • 利用parent记录前驱
    • 输出时逆向,利用parent关系输出
      2.代码
    typedef struct  
    {  
        int data;                   //顶点编号  
        int parent;                 //前一个顶点的位置  
    } QUERE;                        //非环形队列类型  
      
    void ShortPath(ALGraph *G,int u,int v)  
    {  
        //输出从顶点u到顶点v的最短逆路径  
        ArcNode *p;  
        int w,i;  
        QUERE qu[MAXV];             //非环形队列  
        int front=-1,rear=-1;       //队列的头、尾指针  
        int visited[MAXV];  
        for (i=0; i<G->n; i++)      //访问标记置初值0  
            visited[i]=0;  
        rear++;                     //顶点u进队  
        qu[rear].data=u;  
        qu[rear].parent=-1;  
        visited[u]=1;  
        while (front!=rear)         //队不空循环  
        {  
            front++;                //出队顶点w  
            w=qu[front].data;  
            if (w==v)               //找到v时输出路径之逆并退出  
            {  
                i=front;            //通过队列输出逆路径  
                while (qu[i].parent!=-1)  
                {  
                    printf("%2d ",qu[i].data);  
                    i=qu[i].parent;  
                }  
                printf("%2d
    ",qu[i].data);  
                break;  
            }  
            p=G->adjlist[w].firstarc;   //找w的第一个邻接点  
            while (p!=NULL)  
            {  
                if (visited[p->adjvex]==0)  
                {  
                    visited[p->adjvex]=1;  
                    rear++;             //将w的未访问过的邻接点进队  
                    qu[rear].data=p->adjvex;  
                    qu[rear].parent=front;  
                }  
                p=p->nextarc;           //找w的下一个邻接点  
            }  
        }  
    }  
    

    应用

    六度空间



    伪代码

    int BFS(int v, int N, int ** snap)
    {
    	将第一个顶点v入队
        最后顶点last=v
    	while  队列不空
    	
    		出队顶点temp
    		for  j=1toN
    			if  temp和j之间有边且未访问过
    			
    				入队j节点
    				记录访问过j顶点
    				该六度空间内人数+1
    				记录该节点为本层最后一个顶点tail=j
    			end if
                       end for
    		if  出队顶点等于该层最后一个顶点
    	         
    			层数加一 
    			记录最后一个顶点last=tail 
    		end if
    		if  层数等于6
    			结束循环
                       end if
    	end while
    	返回六度空间内的人数
    }
    

    代码:


    最小生成树相关算法及应用

    1.概念

    1.一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的(n-1)条边。不能回路。 
    2.由深度优先遍历得到的生成树称为深度优先生成树。由广度优先遍历得到的生成树称为广度优先生成树。
    3.对于带权连通图G ,n个顶点,n-1条边。根据深度遍历或广度遍历生成生成树,树不唯一。其中权值之和最小的生成树称为图的最小生成树。
    4.非连通图:需多次调用遍历过程。每个连通分量中的顶点集和遍历时走过的边一起构成一棵生成树。所有连通分量的生成树组成非连通图的生成森林。

    普里姆算法(Prim)

    1.过程:

    • 初始化U={v}。v到其他顶点的所有边为候选边;
    • 从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
    • 考察当前V-U中的所有顶点j,修改候选边:若(j,k)的权值小于原来和顶点k关联的候选边,则用(k,j)取代后者作为候选边。
    • 重复后两个步骤骤n-1次,使得其他n-1个顶点被加入到U中


      2.具体设置2个辅助数组。
    • closest[i]:最小生成树的边依附在U中顶点编号。
    • lowcost[i]表示顶点i(i ∈ V-U)到U中顶点的边权重,取最小权重的顶点k加入U。并规定lowcost[k]=0表示这个顶点在U中
    • (closest[k],k)构造最小生成树一条边。

    3.代码

    #define INF 32767		//INF表示∞
    void Prim(MGraph g,int v)
    {  int lowcost[MAXV],min,closest[MAXV],i,j,k;
       for (i=0;i<g.n;i++)	//给lowcost[]和closest[]置初值
       {  lowcost[i]=g.edges[v][i];closest[i]=v;}
        for (i=1;i<g.n;i++)	  //找出(n-1)个顶点
       {	min=INF;
    	for (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
    ",closest[k],k,min);
    	lowcost[k]=0;		//标记k已经加入U
              for (j=0;j<g.n;j++)	//修改数组lowcost和closest
              if (lowcost[j]!=0 && g.edges[k][j]<lowcost[j])
    	{	lowcost[j]=g.edges[k][j];
    	         closest[j]=k;
    	} 
       }
    }
    

    4.贪心算法:

    • 算法原理:以当前情况为基础作最优选择,而不考虑各种可能的整体情况,所以贪心法不要回溯。
    • 算法优点:因为省去了为寻找解而穷尽所有可能所必须耗费的大量时间,因此算法效率高。
    • 贪心算法的精神就是“只顾如何获得眼前最大的利益”,有时不一定是最优解。

    克鲁斯卡尔算法(Kruskal)

    1.过程

    • 置U的初值等于V(即包含有G中的全部顶点),TE的初值为空集(即图T中每一个顶点都构成一个连通分量)。
    • 将图G中的边按权值从小到大的顺序依次选取: 若选取的边未使生成树T形成回路,则加入TE;否则舍弃,直到TE中包含(n-1)条边为止。

      2.类型
    typedef struct 
    {    int u;     //边的起始顶点
         int v;      //边的终止顶点
         int w;     //边的权值
    } Edge; 
    Edge E[MAXV];
    

    3.代码

    void Kruskal(AdjGraph *g)
    {     int i,j,u1,v1,sn1,sn2,k;
          int vset[MAXV]; //集合辅助数组
          Edge E[MaxSize];	//存放所有边
          k=0;			//E数组的下标从0开始计
        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;
    	  }
         }
         Sort(E,g.e);	//用快排对E数组按权值递增排序
          for (i=0;i<g.n;i++) 	//初始化集合
    	vset[i]=i;
    	k=1;		//k表示当前构造生成树的第几条边,初值为1
    	j=0;		//E中边的下标,初值为0
    	while (k<g.n)	//生成的顶点数小于n时循环
    	{ 
                        u1=E[j].u;v1=E[j].v;	//取一条边的头尾顶点
    	      sn1=vset[u1];
    	      sn2=vset[v1];	//分别得到两个顶点所属的集合编号
     	      if (sn1!=sn2)  	//两顶点属于不同的集合
    	      {	printf("  (%d,%d):%d
    ",u1,v1,E[j].w);
    		k++;		   	//生成边数增1
    		for (i=0;i<g.n;i++)  	//两个集合统一编号
    		      if (vset[i]==sn2) 	//集合编号为sn2的改为sn1
    			vset[i]=sn1;
    	     }
    	     j++;			   //扫描下一条边
                }
    }
    

    4.采用并查集的方法

    void Kruskal(AdjGraph *g)
    { int i,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-1
       {  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++;   		//扫描下一条边
      }
    } 
    

    本算法的时间复杂度是O(elog2e)。由于它与n无关,只与e有关,所以克鲁斯卡尔算法适合于稀疏图
    5.比较

    • 普里姆算法:O(n2)、适用于稠密图,克鲁斯卡尔算法:O(eloge)、适用于稀疏图
    • 实现普里姆算法,用邻接矩阵。实现克鲁斯卡尔算法,用邻接表。

    应用

    1.伪代码

    用vest数组作为集合辅助数组
    while  k不等于顶点数n
          遍历寻找最小边
          if 找到最小边权值为INF
             非连通 输出-1
          end if
          if 该边两顶点集合不一
             并查集改为同一集合
             ans加上该边权值
             记录边个数+1
          end if
          将该边权值修正为INF
    end while
    

    2.代码



    最短路径相关算法及应用

    狄克斯特拉(Dijkstra)算法

    1.单源最短路径问题:Dijkstra算法

    • S={入选顶点集合,初值V0},T={未选顶点集合}。
    • 若存在<V0,Vi>,距离值为<V0,Vi>弧上的权值
    • 若不存在<V0,Vi>,距离值为∞
    • (1)从T中选取一个其距离值为最小的顶点W, 加入S
    • (2)S中加入顶点w后,对T中顶点的距离值进行修改:重复上述步骤1,直到S中包含所有顶点,即S=V为止。
    • (3)重复上述步骤1,直到S中包含所有顶点,即S=V为止。
      2.最短路径证明
    • 采用邻接矩阵存储
    • 数组dist[]:源点V0到每个终点的最短路径长度。
    • 数组path[]:最短路径序列的前一顶点的序号;初值或无路径用-1表示
    • 数组s[]:表示最短路径顶点集合S。
    • 算法过程

      3.代码
    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;			//s[]置空
    	if (g.edges[v][i]<INF)	//路径初始化
    	       path[i]=v;		//顶点v到i有边时
    	else
    	      path[i]=-1;		//顶点v到i没边时
          }
          s[v]=1;	 		//源点v放入S中
           for (i=0;i<g.n;i++)	 	//循环n-1次
           {      mindis=INF;
    	for (j=0;j<g.n;j++)
    	     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;
    	          }
          }
          Dispath(dist,path,s,g.n,v);	//输出最短路径
    }
    
    

    4.缺点及其他:

    • 不适用带负权值的带权图求单源最短路径。
    • 不适用求最长路径长度:最短路径长度是递增,顶点u加入S后,不会再修改源点v到u的最短路径长度
    • 求每一对顶点之间的最短路径时,每次以一个顶点为源点,重复执行Dijkstra算法n次。时间复杂度为O(n3)。也可以用弗洛伊德(Floyd)算法:时间复杂性也是O(n3),但形式上简单些。

    弗洛伊德(Floyd)算法

    1.过程:

    • 有向图G=(V,E)采用邻接矩阵存储
    • 二维数组A用于存放当前顶点之间的最短路径长度,分量A[i][j]表示当前顶点i到顶点j的最短路径长度。
    • 递推产生一个矩阵序列A0,A1,…,Ak,…,An-1。Ak+1[i][j]表示从顶点i到顶点j的路径上所经过的顶点编号k+1的最短路径长度。


    2.代码

    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			 //i和j顶点之间没有一条边时
    	          path[i][j]=-1;
         }
       for (k=0;k<g.n;k++)		//求Ak[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][j])	//找到更短路径
    	    {    A[i][j]=A[i][k]+A[k][j];	//修改路径长度
    	             path[i][j]=k; 	//修改经过顶点k
            }
       }
    }	
    
    

    应用


    1.伪代码

    建图
    
    定义将结构体edges为距离,cost为花费资金
    定义dist数组为到首顶点距离,costs数组为花费资金,s数组记录是否访问过
    初始化dist,costs,s数组
    for 1 to n
         找到最短路径,记录距离、花费及下标
         相应s数组值修改为1
         for 0 to n
             if  未访问且路径长小于之前的最短路径	
                 更新最短路径及最少花费
                  dist[t] = min1 + g.edges[k][t];
                  costs[t] = min2 + g.cost[k][t];
             else if  未被访问,路径长与之前相等且花费少
                  更新最少花费
          end for
    end for
    输出路径长及花费
    

    2.代码

    拓扑排序、关键路径

    拓扑排序

    1.在一个有向无环图中找一个拓扑序列的过程称为拓扑排序。条件:

    • 每个顶点出现且只出现一次。
    • 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
      2.过程
    • 从有向图中选取一个没有前驱的顶点,并输出之;
    • 从有向图中删去此顶点以及所有以它为尾的弧;
    • 重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。

    3.结构体

    typedef struct 	       //表头节点类型
    {  vertex data;         //顶点信息
       int count;           //存放顶点入度
       ArcNode *firstarc;   //指向第一条弧
    } VNode;
    

    4.代码

    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.基本概念

    • 用顶点表示事件,用有向边e表示活动,边的权c(e)表示活动持续时间。是带权的有向无环图
    • 整个工程完成的时间为:从有向图的源点到汇点的最长路径。又叫关键路径(critical path)
    • “关键活动(key activity)”指的是:关键路径中的边
      2.AOE网关键词
    • AOE网——带权的有向无环图
    • 顶点--事件或状态
    • 弧(有向边)--活动及发生的先后关系
    • 权--活动持续的时间
    • 起点--入度为0的顶点(只有一个)
    • 终点--出度为0的顶点(只有一个)
      3.过程
    • 事件的最早开始和最迟开始时间
      事件v最早开始时间ve(v):v作为源点事件最早开始时间为0。

      v为源点事件最早开始时间一定是前驱事件x,y,z已完成。
    • 事件v的最迟开始时间vl(v):定义在不影响整个工程进度的前提下,事件v必须发生的时间称为v的最迟开始时间

      4.活动:边
    • 活动a(边)的最早开始时间e(a)指该活动起点x事件的最早开始时间,即:e(a)=ve(x)
    • 活动a的最迟开始时间l(a)指该活动终点y事件的最迟开始时间与该活动所需时间之差,即:l(a)=vl(y)-c
    • 关键活动:d(a)=l(a)-e(a),若d(a)为0,则称活动a为关键活动。
    • 关键活动不存在富余时间,适当增加对关键活动的投资,减少关键活动的持续时间,缩短工程工期。
      5.例题
      试对图所示的AOE-网:
      ① 求这个工程最早可能在什么时间结束;
      ② 求每个活动的最早开始时间和最迟开始时间;表格表示。
      ③ 确定哪些活动是关键活动。

    1.2.谈谈你对图的认识及学习体会。

    对于图的学习时间也比较长,但感觉图的内容很多,而且比较复杂。虽说相较于树来说,递归算法比较少,但算法比较多,刚学的时候一时间难以分清楚,在写代码的时候也不能很准确地完成,总是需要看课件。其实感觉这些算法比较固定,就像是一个模板,题目有浅有深,浅显的题目,直接套用算法,复杂的题目,在算法的基础之上拓展出满足题意的代码即可。其实仔细想想,大的知识点也就几个,缺少的是仔细钻研和总结。实验课上,看同学们集思广益,有各种巧妙的算法,不禁有些惭愧。五一假期没有规划好学习的时间,感觉理解还是很浅显,之后还要继续学习相关内容。

    2阅读代码

    2.1题目及解题代码


    2.1.1该题的设计思路


    时间复杂度:O(n²)
    空间复杂度:O(n)

    2.1.2该题的伪代码

    定义flag判断数据走向
    逻辑处理是一样的,x,y的上限值是相反的
    for  0 to  m+n
         判断序列走向
         找到该次序列起点
         循环将序列存入vector容器
         改变flag,下次反向
    end for
    返回该序列
    

    2.1.3运行结果

    2.1.4分析该题目解题优势及难点

    优势:将两种情况融合在了一起解题,还有将不同方向的情况分开来讲。这样使代码更精简,时间和空间上也节约了不少。
    难点:不能一眼看出来各个点之间以及输出序列之间的关系,需要仔细分析。而且将两种情况结合也是比较难的。

    2.2题目及解题代码


    2.2.1该题的设计思路

    1.图中一个节点可以拥有任意数量的邻接点。为了避免在复制时陷入死循环,需要以某种方式跟踪已经复制的节点。
    2.从给定节点开始遍历图。使用used数组存放已被访问过的节点
    3.如果当前访问的节点不在used中,创建他的克隆节点存进used中
    4.递归调用每个节点的邻接点。每个节点递归调用的次数等于邻接点的数量,每一次调用返回其对应邻接点的克隆节点,最终返回这些克隆邻接点的列表,将其放入对应克隆节点的邻接表中。这样就可以克隆给定的节点和其邻接点。
    

    时间复杂度:O(n)
    空间复杂度:O(n)

    2.2.2伪代码

    如果是空指针,返回空
    若该节点已拷贝,返回该节点指针
    创建拷贝节点
    将拷贝后的指针放入used数组used[node->val]=p;
    for  0 to 邻接点数量,将该节点的邻接节点放入拷贝节点邻接数组
    递归实现每个节点的更新
    返回该节点
    

    2.2.3运行结果

    2.2.4分析该题目解题优势及难点

    优势:典型的深度优先遍历,不过是换了一种“克隆”方式,加上了创建节点等
    难点:本题题意乍一看有点不知所云,感觉就是把输入的东西输出,实际上就是考查图的遍历的,容易被题目忽悠住。

    2.3题目及解题代码


    2.3.1该题的设计思路

    使用广度优先搜索的方法得到从 start 开始能够到达的所有位置,如果其中某个位置对应的元素值为 0,那么就返回 True。
    具体的将start加入队列。每次搜索过程中,取出队首的节点 u,它可以到达的位置为 u + arr[u] 和 u - arr[u]。
    如果某个位置落在数组的下标范围 [0, len(arr)) 内,并且没有被搜索过,则将该位置加入队尾。只要搜索到一个对应元素值为 0 的位置,我们就返回 True。在搜索结束后,如果仍然没有找到符合要求的位置,我们就返回 False。
    时间复杂度:O(n)
    空间复杂度:O(n)
    

    2.3.2伪代码

    若初始节点为0,直接返回true
    定义vector数组
    定义队列q
    初始节点入队,并记录
    while 队不空
         出队队头
         if  跳跃后节点:后跳下标小于n,前跳下标大于等于0,且节点未访问过
            if 节点值为0 返回true    end if
            入队节点,记录节点
         end if
    end while
    返回 false
    

    2.3.3运行结果

    2.3.4分析该题目解题优势及难点

    优势:巧妙运用广度优先搜索解决问题,将数组的问题转化为类似于图的问题
    难点:有经验的话这题看来就是深度或广度优先搜索问题,但不熟练的话,刚开始会不知道如何下手

  • 相关阅读:
    Thrift中实现Java与Python的RPC互相调用
    Thrift介绍以及Java中使用Thrift实现RPC示例
    Netty中集成Protobuf实现Java对象数据传递
    ProtoBuf的介绍以及在Java中使用protobuf将对象进行序列化与反序列化
    ProtoBuf在使用protoc进行编译时提示: Required fields are not allowed in proto3
    Netty中使用WebSocket实现服务端与客户端的长连接通信发送消息
    Netty中实现多客户端连接与通信-以实现聊天室群聊功能为例(附代码下载)
    Netty的Socket编程详解-搭建服务端与客户端并进行数据传输
    Gradle项目在IDEA中运行时提示:Unnecessarily replacing a task that does not exist is not supported. Use create() or register() directly instead.
    Windows下curl的下载与使用
  • 原文地址:https://www.cnblogs.com/xingyufen/p/12830015.html
Copyright © 2020-2023  润新知