• DS博客作业04--图


    0.PTA得分截图

    1.本周学习总结(0-5分)

    1.1 总结图内容

    一、图的基本概念

    1.定义和基本术语

    定义:顶点集 V 和顶点间的关系:边集合E组成的数据结构。

    类别:
    <1>有向图:由顶点集和弧集构成的图
    <2>无向图:没方向边

    基本术语:

    <1>端点和邻接点
    
    无向图:若存在一条边(i,j),则称顶点i和顶点j互为邻接点。
    
    有向图:存在一条边<i,j>,则称此边是顶点i的一条出边,同时也是顶点j的一条入边;称顶点i 和顶点j 互为邻接点。
    
    <2>顶点的度、入度和出度
    
    无向图:以顶点i为端点的边数称为该顶点的度。   
    
    有向图:以顶点i为终点的入边的数目,称为该顶点的入度。以顶点i为始点的出边的数目,称为该顶点的出度。一个顶点的入度与出度的和为该顶点的度。 
        
    <3>完全图
    
    无向图:每两个顶点之间都存在着一条边,称为完全无向图, 包含有n(n-1)/2条边。
    
    有向图:每两个顶点之间都存在着方向相反的两条边,称为完全有向图,包含有n(n-1)条边。
    
    <4>稠密图、稀疏图
    
    当一个图接近完全图时,则称为稠密图。
    
    相反,当一个图含有较少的边数(即当e<<n(n-1))时,则称为稀疏图。
     
    <5>子图
    
    设有两个图G=(V,E)和G'=(V',E'),若V'是V的子集,即V'属于V,且E'是E的子集,即E'属于E,则称G'是G的子图。
    
    <6>路径和路径长度
    
    路径长度是指一条路径上经过的边的数目
    
    简单路径:一条路径上除开始点和结束点可以相同外,其余顶点均不相同
    
    <7>回路或环
    
    回路或环:一条路径上的开始点与结束点为同一个顶点。
    
    简单回路或简单环:开始点与结束点相同的简单路径。
    
    <8>连通、连通图和连通分量
    
    无向图:若从顶点i到顶点j有路径,则称顶点i和j是连通的。   
     
    连通图:若图中任意两个顶点都连通,否则称为非连通图。
    
    连通分量:无向图G中的极大连通子图。
    * 任何连通图的连通分量只有一个,即本身
    * 而非连通图有多个连通分量。
    
    有向图:
    * 若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。
    * 否则,其各个强连通子图称作它的强连通分量。
    
    <9>权和网  
    
    图中每一条边都可以附有一个对应的数值,这种与边相关的数值称为权。
    
    边上带有权的图称为带权图,也称作网。
    

    2.存储结构和基本运算算法

    <1>存储结构

    ①邻接矩阵(二维数组)

    存储表示:

    • 顶点信息:记录各个顶点信息的顶点表。

    • 边或弧信息:各个顶点之间关系的邻接矩阵。

    结构体定义:

    #define  MAXV  <最大顶点个数>	
    typedef struct 
    {    int no;			//顶点编号
         InfoType info;		//顶点其他信息
    } VertexType;
    typedef struct  			//图的定义
    {    int edges[MAXV][MAXV]; 	//邻接矩阵
         int n,e;  			//顶点数,边数
         VertexType vexs[MAXV];	//存放顶点信息
    }  MatGraph;
     MatGraph g;//声明邻接矩阵存储的图
    

    图像表示:

    <2>邻接表

    存储表示:

    • 对图中每个顶点i建立一个单链表,将顶点i的所有邻接点链起来。

    • 每个单链表上添加一个表头结点(表示顶点信息)。并将所有表头结点构成一个数组,下标为i的元素表示顶点i的表头结点。

    结构体定义:

    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
    

    图像表示:

    3.基本运算算法设计

    ①创建图的运算算法

    void CreateAdj(AdjGraph *&G,int n,int e) //创建图邻接表
    {   
        int i,j,a,b;
        ArcNode *p;
        G=new AdjGraph;
        for (i=0;i<n;i++)   G->adjlist[i].firstarc=NULL;
    //给邻接表中所有头结点的指针域置初值
        for (i=1;i<=e;i++)		 //根据输入边建图 
          {    cin>>a>>b;	
    	   p=new ArcNode;	//创建一个结点p
               p->adjvex=b;		 //存放邻接点
    	   p->nextarc=G->adjlist[a].firstarc;  //采用头插法插入结点p
               G->adjlist[a].firstarc=p;
           }
        G->n=n; G->e=n;
    }
    

    ②输出图的运算算法

    void DispAdj(AdjGraph *G)	//输出邻接表G
    {      int i;
           ArcNode *p;
           for (i=0;i<G->n;i++)
           {    p=G->adjlist[i].firstarc;//访问第一个节点
    	    printf("%3d: ",i);
    	while (p!=NULL)
    	{    printf("%3d[%d]→",p->adjvex,p->weight);
    	     p=p->nextarc;
    	}
    	printf("∧
    ");
           }
    }
    

    ③销毁图的运算算法

    void DestroyAdj(AdjGraph *&G)   //销毁邻接表
    {    int i; ArcNode *pre,*p;
         for (i=0;i<G->n;i++)		//扫描所有的单链表
         {    pre=G->adjlist[i].firstarc;//p指向第i个单链表的首结点
    	  if (pre!=NULL)
    	  {    p=pre->nextarc;
    	       while (p!=NULL)	//释放第i个单链表的所有边结点
    	      {    free(pre);
    		   pre=p; p=p->nextarc;
    	      }
    	      delete pre;
    	  }
        }
        delete G;			//释放头结点数组
    }
    

    二、图的遍历及应用

    1.定义

    定义:从给定图中任意指定的顶点(称为初始点)出发,按照某种搜索方法沿着图的边访问图中的所有顶点,使每个顶点仅被访问一次。

    类别:深度优先遍历(DFS)。广度优先遍历(BFS)。

    2.遍历方法及算法

    ①DFS

    过程:

    • 从图中某个初始顶点v出发,首先访问初始顶点v。

    • 选择一个与顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,直到图中与当前顶点v邻接的所有顶点都被访问过为止。  

    实质:对每个顶点查找其邻接点的过程

    算法思路:

    void DFS(ALGraph *G,int v)  
    {    ArcNode *p;
         visited[v]=1;                   //置已访问标记
         printf("%d  ",v); 		
         p=G->adjlist[v].firstarc;      	
         while (p!=NULL) 
         {
              if (visited[p->adjvex]==0)  DFS(G,p->adjvex);    
    	  p=p->nextarc;              	
         }
    }
    

    图像表示:

    ②BFS

    过程:

    • 访问初始点v,接着访问v的所有未被访问过的邻接点。

    • 按照次序访问每一个顶点的所有未被访问过的邻接点。
       

    • 依次类推,直到图中所有顶点都被访问过为止。

    算法思路:

    建一个访问队列q
    访问v节点,加入队列q
    while(队列不空)
        取队头元素w
        遍历w的邻接表    
             取邻接点j
             若j未被访问,则加入队列q,并访问j。
    end while
    

    图像表示:

    3.图遍历算法的应用

    ①判断无向图G是否连通

    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); 	//调用前面的中DSF算法,从顶点0开始深度优先遍历
          for (i=0;i<G->n;i++)
              if (visited[i]==0)
               {    flag=false;
    	        break;
               }
          return flag;
    }
    

    ②判断顶点u->v是否有简单路径

    void ExistPath(AGraph *G,int u,int v,bool &has)
    {  //has表示u到v是否有路径,初值为false
           int w;  ArcNode *p;
           visited[u]=1;		//置已访问标记
           if (u==v)		//找到了一条路径
           {  has=true;	//置has为true并结束算法
    	  return;
           }
           p=G->adjlist[u].firstarc;	//p指向顶点u的第一个相邻点
           while (p!=NULL)
           {    w=p->adjvex;		//w为顶点u的相邻顶点
    	    if (visited[w]==0)	//若w顶点未访问,递归访问它
    	    ExistPath(G,w,v,has);
    	    p=p->nextarc;	      	//p指向顶点u的下一个相邻点
          }
    } 
    

    ③求不带权无向连通图G中从顶点u->v的一条最短路径

    typedef struct
    {      int data;	//顶点编号
           int parent;	//前一个顶点的位置
    } QUERE;	
    	
    void ShortPath(AdjGraph *G,int u,int v)
    {   //输出从顶点u到顶点v的最短逆路径
           qu[rear].data=u;//第一个顶点u进队
           while (front!=rear)//队不空循环
           {      front++;		//出队顶点w
                  w=qu[front].data;
                  if (w==v)   根据parent关系输出路径break; 
                  while(遍历邻接表)   
                  {    rear++;//将w的未访问过的邻接点进队
    		   qu[rear].data=p->adjvex;
    		   qu[rear].parent=front;
    	      }
           }	      
    }
    

    三、最小生成树

    1.概念

    一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的(n-1)条边。不能回路。  

    • 对于带权连通图G ,n个顶点,n-1条边

    • 根据深度遍历或广度遍历生成生成树,树不唯一

    • 其中权值之和最小的生成树称为图的最小生成树

    2.构造最小生成树算法

    ①普里姆(Prim)算法

    思路过程:

    • 初始化U={v}。v到其他顶点的所有边为候选边;

    • 重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:

      • 从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
      • 考察当前V-U中的所有顶点j,修改候选边:若(j,k)的权值小于原来和顶点k关联的候选边,则用(k,j)取代后者作为候选边。

    辅助数组:

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

    算法设计:

    #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;
    	   } }}
    

    图像表示:

    ②克鲁斯卡尔(Kruskal)算法

    思路过程:

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

    辅助结构体

    typedef struct 
    {    int u;     //边的起始顶点
         int v;      //边的终止顶点
         int w;     //边的权值
    } Edge; 
    

    算法设计:

    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++;			   //扫描下一条边
            }
    }
    

    图像表示:

    3.两种算法的比较

    普里姆算法:O(n2)、适用于稠密图

    克鲁斯卡尔算法:O(eloge)、适用于稀疏图

    实现Prim算法,选择图的邻接矩阵存储结构

    实现克鲁斯卡尔算法,选择图的邻接表存储结构

    四、最短路径

    1.定义

    在带权有向图中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。

    2.单源最短路径—Dijkstra(迪杰斯特拉)算法

    问题描述:

    给定一个带权有向图G与源点v,求从v到G中其他顶点的最短路径,并限定各边上的权值大于或等于0。

    设计思路:

    • 初始化

    • S={入选顶点集合,初值V0},T={未选顶点集合}。

      • 若存在<V0,Vi>,距离值为<V0,Vi>弧上的权值

      • 若不存在<V0,Vi>,距离值为∞

      • 从T中选取一个其距离值为最小的顶点W, 加入S

    • 从T中选取一个其距离值为最小的顶点W, 加入S

    • S中加入顶点w后,对T中顶点的距离值进行修改:

      • 若加进W作中间顶点,从V0到Vj的距离值比不加W的路径要短,则修改此距离值;
    • 重复上述步骤,直到S中包含所有顶点,即S=V为止。

    辅助数组:

    数组dist[]:源点V0到每个终点的最短路径长度。

    数组path[]:最短路径序列的前一顶点的序号;初值或无路径用-1表示

    算法展示:

    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);	//输出最短路径
    }
    

    图像表示:

    算法特点:

    • 不适用带负权值的带权图求单源最短路径。

    • 不适用求最长路径长度。

      • 最短路径长度是递增

      • 顶点u加入S后,不会再修改源点v到u的最短路径长度

    3.所有顶点间的最短路径—Floyd(弗洛伊德)算法

    设计思路:

    • 有向图G=(V,E)采用邻接矩阵存储

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

    • 递推产生一个矩阵序列A0,A1,…,Ak,…,An-1,Ak+1[i][j]表示从顶点i到顶点j的路径上所经过的顶点编号k+1的最短路径长度。

    算法展示:

    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.拓扑排列

    定义:

    在一个有向图中找一个拓扑序列的过程称为拓扑排序。序列必须满足条件:

    • 每个顶点出现且只出现一次。

    • 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。

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

    • 有向无环图才有拓扑排序,非DAG图没有拓扑排序

    设计思路:

    • 从有向图中选取一个没有前驱的顶点,并输出之;

    • 从有向图中删去此顶点以及所有以它为尾的弧;

    • 重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。

    辅助结构体:

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

    算法展示:

    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;		//找下一个邻接点
    	}
           }
    }
    

    2.关键路径

    定义:关键路径为源点到汇点的最长路径。

    基本模块:

    ①事件的最早开始和最迟开始时间

    事件v最早开始时间ve(v):v作为源点事件最早开始时间为0。

    ve(v)=0  			当v为初始源点时
    ve(v)=MAX{ve(x)+a,ve(y)+b,ve(z)+c}	否则
    

    v为源点事件最早开始时间一定是前驱事件x,y,z已完成。

    事件v的最迟开始时间vl(v):定义在不影响整个工程进度的前提下,事件v必须发生的时间称为v的最迟开始时间

    vl(v)=ve(v)			当v为终点时
    vl(v)=MIN{vl(x)-a,vl(y)-b,vl(z)-c}	否则
    

    最迟时间要保证后继事件能完成,取最小

    ②活动:边 的最早开始时间和最迟开始时间

    活动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为关键活动。

    关键路径上的活动都是关键活动

    设计思路:

    • 对有向图拓扑排序

    • 根据拓扑序列计算事件(顶点)的ve,vl数组

      • ve(j) = Max{ve(i) + dut(<i,j>)}

      • vl(i) = Min{vl(j) - dut(<i,j>)}

    • 计算关键活动的e[],l[]。即边的最早、最迟时间

      • e(i) = ve(j)

      • l(i) = vl(k) - dut(<j, k>)

    • 找e=l边即为关键活动

    • 关键活动连接起来就是关键路径

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

    经过数周的学习,我发现图是继顺序结构后略为抽象的一种非顺序结构

    正因为图的复杂性,也正好适用于解决很多复杂的问题,在关系越复杂的场景中,就越能体现图结构的优势所在

    能够熟练地掌握并在问题中应用图知识,是现阶段的要求,也是学习的目标

    图能够很好地适用于生活中的各种场景,例如社交网络关系、路由器的路径搜索、GIS求最短路径的问题等

    图即是数据结构较难的一部分,也是最重要的一部分,在学习完图的相关知识后,思维又有了一定的创新和拓展

    2.阅读代码(0--5分)

    2.1 题目及解题代码

    题目

    解题代码

    int largestRectangleArea(vector<int>& heights)
    {
        int ans = 0;
        vector<int> st;
        heights.insert(heights.begin(), 0);
        heights.push_back(0);
        for (int i = 0; i < heights.size(); i++)
        {
            while (!st.empty() && heights[st.back()] > heights[i])
            {
                int cur = st.back();
                st.pop_back();
                int left = st.back() + 1;
                int right = i - 1;
                ans = max(ans, (right - left + 1) * heights[cur]);
            }
            st.push_back(i);
        }
        return ans;
    }
    

    2.1.1 该题的设计思路

    时间复杂度: O(n^2)

    空间复杂度: O(n)

    2.1.2 该题的伪代码

    int largestRectangleArea(vector<int>& heights)
    {
        vector<int> st;
        if(对于一个高度,能得到向左和向右的边界)
            对每个高度求一次面积
        for(遍历所有高度)
        {
            得出最大面积
            使用单调栈
            在出栈操作时得到前后边界并计算面积
        }
        返回ans
    }
    

    2.1.3 运行结果

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

    解题优势:

    使用单调栈。单调栈分为单调递增栈和单调递减栈,单调递增栈即栈内元素保持单调递增的栈,同理单调递减栈即栈内元素保持单调递减的栈。
    如果新的元素比栈顶元素大,就入栈,如果新的元素较小,那就一直把栈内元素弹出来,直到栈顶比新元素小。
    加入这样一个规则之后, 栈内的元素是递增的。
    当元素出栈时,说明这个新元素是出栈元素向后找第一个比其小的元素。
    当元素出栈后,说明新栈顶元素是出栈元素向前找第一个比其小的元素。

    难点:

    如果输入是递增的话,这个代码最后都无法弹出计算面积。需要在Heights数组的后面再加上一个0,这样才可以强迫栈内元素出栈计算面积。

    2.2 题目及解题代码

    题目

    解题代码

    class Solution {
    public:
        static constexpr int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
        static constexpr int MAX_N = 100 + 5;
    
        struct Coordinate {
            int x, y, step;
        };
    
        int n, m;
        vector<vector<int>> a;
    
        bool vis[MAX_N][MAX_N];
    
        int findNearestLand(int x, int y) {
            memset(vis, 0, sizeof vis);
            queue <Coordinate> q;
            q.push({x, y, 0});
            vis[x][y] = 1;
            while (!q.empty()) {
                auto f = q.front(); q.pop();
                for (int i = 0; i < 4; ++i) {
                    int nx = f.x + dx[i], ny = f.y + dy[i];
                    if (!(nx >= 0 && nx <= n - 1 && ny >= 0 && ny <= m - 1)) continue;
                    if (!vis[nx][ny]) {
                        q.push({nx, ny, f.step + 1});
                        vis[nx][ny] = 1;
                        if (a[nx][ny]) return f.step + 1;
                    }
                }
            }
            return -1;
        }
        
        int maxDistance(vector<vector<int>>& grid) {
            this->n = grid.size();
            this->m = grid.at(0).size();
            a = grid;
            int ans = -1;
            for (int i = 0; i < n; ++i) {
                for (int j = 0; j < m; ++j) {
                    if (!a[i][j]) {
                        ans = max(ans, findNearestLand(i, j));
                    }
                }
            }
            return ans;
        }
    };
    

    2.2.1 该题的设计思路

    时间复杂度: O(n^4)

    空间复杂度: O(n^2)

    2.2.2 该题的伪代码

    int findNearestLand(int x, int y)
    {
        把所有的陆地都入队
        从各个陆地开始,一圈一圈的遍历海洋
        最后遍历到的海洋即离陆地最远的海洋
    }
    int maxDistance(vector<vector<int>>& grid) 
    {
        取出队列的元素,将其四周的海洋入队
        没有陆地或者没有海洋,返回-1
        得到最后一次遍历到的海洋的距离
        返回ans
    }
    

    2.2.3 运行结果

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

    解题优势:

    当我们搜索到一个新入队的区域它的grid值为1,
    即这个区域是陆地区域的时候我们就可以停止搜索。
    BFS能保证当前的这个区域是最近的陆地区域(BFS的性质决定了这里求出来的一定是最短路)。

    难点:

    考虑最坏情况所有的区域都是海洋,那么每一个区域都会进行BFS对于每一次 BFS。
    最坏的情况是找不到陆地区域,我们只能遍历完剩下的 n^2 - 1个海洋区域。
    由于 vis 数组确保每个区域只被访问一次,所以单次BFS的渐进时间复杂度是O(n^2),程序的总的渐进时间复杂度较大。

    2.3 题目及解题代码

    题目:

    解题代码:

    int trap(vector<int>& height)
    {
        int ans = 0, current = 0;
        stack<int> st;
        while (current < height.size()) {
            while (!st.empty() && height[current] > height[st.top()]) {
                int top = st.top();
                st.pop();
                if (st.empty())
                    break;
                int distance = current - st.top() - 1;
                int bounded_height = min(height[current], height[st.top()]) - height[top];
                ans += distance * bounded_height;
            }
            st.push(current++);
        }
        return ans;
    }
    

    2.3.1 该题的设计思路

    时间复杂度: O(n)

    空间复杂度: O(n)

    2.3.2 该题的伪代码

    int trap(vector<int>& height)
    {
        使用栈来存储条形块的索引下标。
        遍历数组
        当栈非空且height[current]>height[st.top()]
            意味着栈中元素可以被弹出,弹出栈顶元素top。
            计算当前元素和栈顶元素的距离,准备进行填充操作
                distance=current−st.top()−1
            找出界定高度
                bounded_height=min(height[current],height[st.top()])−height[top]
            往答案中累加积水量
                ans+=distance×bounded_height
        将当前索引下标入栈
        将current移动到下个位置
        返回ans
    }
    

    2.3.3 运行结果

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

    解题优势:

    不用存储最大高度,而是用栈来跟踪可能储水的最长的条形块。使用栈就可以在一次遍历内完成计算。

    难点:

    在遍历数组时维护一个栈。
    如果当前的条形块小于或等于栈顶的条形块,将条形块的索引入栈,即当前的条形块被栈中的前一个条形块界定。
    如果发现一个条形块长于栈顶,可以确定栈顶的条形块被当前条形块和栈的前一个条形块界定,因此可以弹出栈顶元素并且累加答案到ans 。
    但同时栈内元素的存放和图形的动态变化过程较为抽象,在理解层面需要一定的时间。

  • 相关阅读:
    IOS回调机制总结
    2.25~当svn服务器ip地址变了怎么办?
    ubuntu硬件信息,内存DDR详细信息
    关于JS相等比较算法(==)的原理
    ubuntu更改鼠标滚轮方向为自然方向(运动方向和滚轮滚动方向一致)
    C#模拟js的Json对象创建,操作
    关于json返回日期格式化的解决方案
    js定时器 timer
    ubuntu GUI界面复制文件没权限的解决方案
    CodeSmith 模板
  • 原文地址:https://www.cnblogs.com/yushanbaiyi/p/12820912.html
Copyright © 2020-2023  润新知