DS博客作业04--图

0.PTA得分截图

1.本周学习总结

1.1 总结图内容

1.1.1 图的基本概念

图的定义

图G由顶点集合N和边集合E构成

  • 在图G中,如果代表边的顶点对是无序的,则称G为无向图。用圆括号"()"表示无向边

  • 如果表示边的顶点对是有序的,则称G为有向图。用尖括号"<>"表示有向边(又称,表示有方向的边)

图的基本术语

  • 端点和邻接点:

    • 无向图:若存在一条边(i,j)那么顶点i和顶点j为端点,它们互为邻接点

    • 有向图:若存在一条边<i,j>那么顶点i为起点,顶点j为终点,它们互为邻接点

  • 顶点的度:

    • 无向图:以顶点i为端点的边数为该顶点的度

    • 有向图:以顶点i为终点的入边的数目为该顶点的入度;以顶点i为始点的出边的数目为该顶点的出度;一个顶点的入度与出度的和为该顶点的度

  • 完全图:

    • 无向图:每两个顶点之间都存在着一条边,称为完全无向图, 有n(n-1)/2条边

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

  • 稠密图、稀疏图:

    • 当一个图接近完全图时,则称为稠密图;当一个图含有较少的边数时,则称为稀疏图
  • 子图:

    • 设有两个图G=(V,E)和G'=(V',E'),若V'是V的子集,且E'是E的子集,那么G'为G的子图
  • 路径:

    • 从一个顶点到另一个顶点所经过的顶点序列
  • 路径长度:

    • 一条路径上经过的边的数目
  • 简单路径:

    • 一条路径除了开始顶点和结束顶点可以相同之外,其他顶点都不相同,则为简单路径
  • 回路或环:

    • 若一条路径上的开始顶点与结束顶点为同一顶点,则该路径称为回路或环;开始顶点与结束顶点相同的简单路径被称为简单回路或简单环
  • 连通、连通图、连通分量、强连通图和强连通分量:

    • 连通:图中若从顶点a到顶点b有路径,则称顶点a和b是连通的

    • 连通图:若图中任意两个顶点都连通,则称为连通图,否则称为非连通图

    • 连通分量:无向图G中的极大连通子图称为G的连通分量。连通图的连通分量只有一个,即它们本身;而非连通图存在多个连通分量

    • 强连通图:若图G中的任意两个顶点a,b都连通,即从顶点a到b和从顶点b到a都存在路径,n那么图G是强连通图

    • 强连通分量:有向图G中的极大强连通子图称为G的强连通分量。强连通图的强连通分量只有一个,即它们本身;而非强连通图存在多个强连通分量

  • 权和网:

    • 权:图中每一条边都可以附带一个值,称为权

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

1.1.2 图的存储结构

邻接矩阵

定义:对于有n个顶点的图,用一维数组vertex[n]存储顶点信息,用二维数组G[n][n]存储顶点之间的关系,则此二维数组成为邻接矩阵

图的邻接矩阵存储类型定义如下

#define MAXV 最大顶点个数
typedef struct {
	int number;/*顶点编号*/
	/*顶点其他信息*/
}VertexType;
typedef struct {
	int edges[MAXV][MAXV];/*邻接矩阵*/
	int n, e;/*顶点数、边数*/
	VertexType vexs[MAXV];/*顶点信息*/
}MatGraph;

邻接矩阵的特点:  

1.一个图的邻接矩阵表示是唯一的

2.特别适合于稠密图的存储--->邻接矩阵的存储空间为O(n²)

无向图的表示
无权图的邻接矩阵

无向无权图G=(V,E)有n(n>=1)个顶点,其邻接矩阵为n阶对称矩阵,其元素定义如下:

带权图的邻接矩阵

无向带权图G=(V,E)的元素定义如下:

无向图邻接矩阵的特点:

1.邻接矩阵是对称矩阵;

2.对于顶点vi,其度数是第i行的非0元素的个数;

3.无向图的边数是上(or下)三角矩阵中的非0元素的个数;

有向图的表示
无权图的邻接矩阵

若有向无权图G=(V,E)有n(n>=1)个顶点,其邻接矩阵不一定为n阶对称矩阵,其元素定义如下:

带权图的邻接矩阵

有向带权图G=(V,E)的元素定义如下:

有向带权图的特点:

1.对于顶点vi,第i行的非0元素的个数是其出度;第i列的非0元素的个数是其出度

2.邻接矩阵中非0元素的个数就是图的弧的数目

邻接表

基本思想:对图的每一个顶点建立一个单链表,存储该顶点的所有邻接顶点及其相关信息,每一个单链表设一个表头结点

图的邻接表存储结构如下:

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;

邻接表的特点:

1.邻接表的表示不唯一;

2.特别适合于稀疏图的存储--->邻接矩阵的存储空间为O(n+e)

用邻接表存储图时,对于无向图,其邻接表是唯一的:

对于有向图,其邻接表有两种形式:

逆邻接表:就是在有向图的邻接表中,对每个顶点,链接的是指向该顶点的边

1.1.3 图的遍历及应用

图的深度优先搜索遍历(DFS)

图的深度优先搜索遍历类似于树的先序遍历,是树的先序遍历的推广

算法思想

设初始状态时图中的所有顶点未被访问:

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

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

算法设计
  • 深度优先遍历的过程体现后进先出的特点:用栈or递归的方式实现
  • 如何确定一个顶点是否访问过?
    • 设置一个visited[] 全局数组,visited[i]=0表示顶点i没有访问;visited[i]=1表示顶点i已经访问过
算法实现
void DFS(AdjGraph* G, int v)
{
	int V;
	ArcNode* p;

	visited[v] = 1;
	cout<<v;
	p = G->adjlist[v].firstarc;
	while (p != NULL)
	{
		V = p->adjvex;
		if (visited[V] == 0) DFS(G, V);
		p = p->nextarc;
	}
}

图的广度优先搜索遍历(BFS)

图的广度优先搜索遍历类似于树的按层次遍历

算法思想

设初始状态图中所有顶点未被访问:

1.访问初始点v,接着访问v的所有未被访问过的邻接点v1,v2,…,vt

2.按照v1,v2,…,vt的次序,访问每一个顶点的所有未被访问过的邻接点  

3.依次类推,直到图中所有和初始点v有路径相通的顶点都被访问过为止

算法设计
  • 广度优先搜索遍历体现先进先出的特点,用队列实现
  • 如何确定一个顶点是否访问过?
    • 设置一个visited[] 数组,visited[i]=0表示顶点i没有访问;visited[i]=1表示顶点i已经访问过
算法实现
void BFS(AdjGraph* G, int v)
{
	queue<int> qu;
	int vertex;
	ArcNode* p;

	flag = 0;
	visited[v] = 1;
	qu.push(v);
	while (!qu.empty())
	{
		vertex = qu.front();
		qu.pop();
	        cout<< vertex;
		p = G->adjlist[vertex].firstarc;
		while (p != NULL)
		{
			vertex = p->adjvex;
			if (visited[vertex] == 0)
			{
				visited[vertex] = 1;
				qu.push(vertex);
			}
			p = p->nextarc;
		}
	}
}

非连通图的遍历

  • 无向连通图:调用一次DFS或BFS,能够访问到图中的所有顶点
  • 无向非连通图:调用一次DFS或BFS,只能访问到初始点所在连通分量中的所有顶点,不能访问到其他连通分量中的顶点;可以通过分别遍历每个连通分量,访问到图中的所有顶点
算法实现

采用DFS算法

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

采用BFS算法

void  BFS1(AdjGraph *G)
{      int i;
       for(i=0;i<G->n;i++)//遍历所有未访问过的顶点
           if(visited[i]==0) 
              BFS(G,i);
}
调用BFS()orDFS()的次数恰好等于连通分量的个数

图遍历应用之判断是否连通

可以采用深度优先遍历方式来判断无向图G是否连通,先给visited[]数组(全局变量)设置初值为0,然后从0顶点开始遍历该图

在一次遍历之后,若所有顶点i的visited[i]均为1,则该图是连通的;否则不连通

算法实现

bool check(AdjGraph* G)
{
	int i;
	bool flag = true;
	for (i = 0; i < G->n; i++)/*初始化visited数组*/
	{
		visited[i] = 0;
	}
	DFS(G, 起始点);
	for (i = 0; i < G->n; i++)
	{
		if (visited[i] == 0)/*出现为0的点说明不连通*/
		{
			flag = false;
			break;
		}
	}
	return flag;
}

图遍历应用之查找路径

采用深度优先遍历的方法

增加path和d形参,其中path存放顶点u到v的路径,d表示path中的路径长度,初始值为-1

从顶点u开始进行深度优先遍历;增加path和d记录存走过的路径

若当前扫描的顶点u = v时,表示找到了一条路径,输出路径path

算法实现

图遍历应用之最短路径

这里的最短路径相当于之前学过的树的迷宫问题

我们用广度搜索的方法一定能找到最短路径,而深度搜索不一定

深度搜索能找到所有路径,而广度搜索很难

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

概念

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

  • 最小生成树:对于带权连通图G (每条边上的权均为大于零的实数),可能有多棵不同生成树,每棵生成树的所有边的权值之和可能不同,其中权值之和最小的生成树称为图的最小生成树

  • 遍历产生的生成树:深度优先遍历得到的称为深度优先生成树,由广度优先遍历得到的生成树称作广度优先生成树;生成树是不唯一的!


Prim算法

算法思路:

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

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

(1) 从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;

(2) 考察当前V-U中的所有顶点j,修改候选边:若(j,k)的权值小于原来和顶点k关联的候选边,则用(k,j)取代后者作为候选边

代码实现

Prim算法有两层for循环 时间复杂度为O(n²)

#define INF 32767//INF表示∞
void  Prim(Graph G,int v)
{     
      int lowcost[MAXV];
      int min;
      int 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记录最近顶点编号
	     }
	     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算法

算法思路

1.给定一个图,可以从任意一个顶点出发

2.将图G中的边按权值从小到大的顺序依次选取:

(1)若选取的边未使生成树T形成回路,则加入TE;

(2)否则舍弃,直到TE中包含(n-1)条边为止。

代码实现
void Kruskal(Graph 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
	for (j=0;j<G.n;j++)
	      if (G.edges[i][j]!=0 && G.edges[i][j]!=INF)
	      {     
                     E[k].u=i;  E[k].v=j;  E[k].w=G.edges[i][j];
	             k++;
	      }
        InsertSort(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)  	//两顶点属于不同的集合
	      {
                k++;		   	//生成边数增1
		for (i=0;i<g.n;i++)  	//两个集合统一编号
		    if (vset[i]==sn2) 	//集合编号为sn2的改为sn1
		       vset[i]=sn1;
	      }
	     j++;			   //扫描下一条边
        }
}

1.1.5 最短路径

概念

  • 在带权有向图中,把一条简单路径上所经边的权值之和定义为该路径的路径长度或称带权路径长度从源点到终点可能不止一条路径,把路径长度最短的那条路径称为最短路径(不一定包含n个顶点)

Dijkstra算法

算法思路

设G=(V,E)是一个带权有向图, 把图中顶点集合V分成两组:

第1组为已求出最短路径的顶点集合,用S表示;第2组为其余未求出最短路径的顶点集合,用U表示

S只包含起点即S={v},v的最短路径为0;U包含除v外的其他顶点,U中顶点i距离为边上的权值(若有边)或∞

从U中选取一个距离v最小的顶点u,把u加入S中

以u为新考虑的点,修改U中各顶点j的最短路径长度:若从起点v到顶点j(j∈U)的最短路径长度(经过顶点u)比原来最短路径长度(不经过顶点u)短,则修改顶点j的最短路径长度

重复以上步骤直到所有顶点都在S中

算法设计
  • 如何存放最短路径长度:

用一维数组dist[j]存储:dist[j]表示起点v到顶点j的最短路径长度

  • 如何存放最短路径:

从起点到其他顶点的最短路径有n-1条;一条最短路径用一个一维数组表示;所有n-1条最短路径可以用二维数组path[][]存储


代码实现

算法时间复杂度为O(n²)

void Dijkstra(Graph 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;		
      }
      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;
	        }
            }
         }
      }
    //输出最短路径
}

Floyd算法

算法思路

初始时令S={},设置一个二维数组A用于存放当前顶点之间的最短路径长度,分量A[i][j]表示当前顶点i到j的最短路径长度

当i=j时,A[i][j]赋初值为0;当i≠j且<vi,vj>∈E时,A[i][j]赋初值为弧上的权值;当i≠j且<vi,vj>∉E时,A[i][j]赋初值为∞

将图中一个顶点Vk加入S中,修改A[i][j]的值

重复上面步骤直到图中所有顶点都加入到S中

算法设计
  • 用二维数组A存储最短路径长度:

Ak[i][j]表示考虑顶点0~k后得出的i到j的最短路径长度。

An-1[i][j]表示最终的i到j的最短路径长度

  • 用二维数组path存放最短路径:

pathk[i][j]表示考虑顶点0~k后得出的i到j的最短路径

pathn-1[i][j]表示最终i到j的最短路径

代码实现

算法时间复杂度为O(n³)

void Floyd(Graph 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			 
	      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]=path[k][j]; 	//修改最短路径为经过顶点k
          }
  }
}	
算法应用之旅游规划

1.1.6 拓扑排序

概念

  • 设G=(V,E)是一个具有n个顶点的有向无环图,V中顶点序列v1,v2,…,vn称为一个拓扑序列,在图中找一个拓扑序列的过程称为拓扑排序,每个顶点只能出现一次,且若从顶点i到顶点存在一条路径,则在拓扑序列中i必须在j前面

步骤

1.从有向图中选择一个没有前驱(即入度为0)的顶点并且输出它
2.从图中删去该顶点,并且删去从该顶点发出的全部有向边
3.重复1、2两步,直到剩余的图中不再存在没有前驱的顶点为止

代码实现

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

1.1.7 关键路径

概念

  • AOE网
    • 用一个带权有向图描述工程的预计进度
    • 顶点表示事件,有向边表示活动,边e的权表示完成活动e所需的时间
    • 图中入度为0的顶点表示工程的开始事件,出度为0的顶点表示工程结束事件
  • 关键路径
    • 从AOE网中源点到汇点的最长路径,具有最大长度的路径叫关键路径
    • 关键路径是由关键活动构成的,关键路径可能不唯一

步骤

1.将AOE网利用拓扑排序求出一个拓扑序列
2.从拓扑序列的第一个顶点开始,按拓扑序列依次计算每个事件的最早发生时间ve(i)
3.从拓扑序列的最后一个顶点开始,按拓扑序列依次计算每个事件的最晚发生过程vl(i)

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

首先就是图的学习真的好复杂呀,好几个算法尤其是Dijkstra算法和Kruskal算法,名字长长的好容易搞不清楚哪个算法是对应哪个;

然后是图的知识点理解起来就没有之前树呀线性表呀这些知识点好理解,预习课件的时间显著增长,有时候预习一个晚上也不见得能预习完(当然也有觉得知识点好乱看不进去的锅),不过课前预习真的真的很必要,要不然上课更听不懂了,课前预习完听课效率也会更高

接着是课上老师叫我们做图着色的代码编程,无从下手,只能磕磕绊绊地建邻接表,课上也没有写出来,课后是结合了老师叫同学讲的思路以及参考了好几份代码才搞懂了要怎么写

还有写上机报告的时候,就是做题要花掉很多时间,然后错误率又很高,还就只能无能狂怒:(

写题的时候就是会遇到如果是在函数里面定义数组的话很容易溢出,然后我就问同学都是直接定义全局变量,然后我也跟着这样写了,但是上课老师说全局变量其实不是很友好,最好还是申请动态空间来存放

图的实际应用很广泛,像课表呀百度地图之类的都有用到,是很实用的东西,还是要好好学习掌握

2.阅读代码

2.1 题目及解题代码

  • 题目

  • 代码
class Solution {
public:
    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
        vector<int> rp(1001);
        int sz = edges.size();
       
        for(int i=0;i<sz;i++)
            rp[i] = i;
        for(int j=0;j<sz;j++){
            
            int set1 = find(edges[j][0], rp);
            int set2 = find(edges[j][1], rp);
            if(set1 == set2)  // 两个集合代表节点相同,说明出现环,返回答案
                return edges[j]; 
            else    // 两个集合独立,合并集合。将前一个集合代表节点戳到后一个集合代表节点上
                rp[set1] = set2;
        }
        return {0, 0};
    }


    int find(int n, vector<int> &rp){
        int num = n;
        while(rp[num] != num)
            num = rp[num];
        return num;
    }
};

2.1.1 该题的设计思路

以[[1,2], [2,3], [3,4], [1,4], [1,5]]为例子

  • 将每一个元素看成是两个节点集合:比如[2, 3],就看成是节点集合2和3

  • 先初始化一个容器vector,使得vector[i]=i(这里两个i意思不同,作为索引的i是指当前节点,作为值的i是指当前节点所在集合的代表节点)
    初始化如图:

  • 遍历边集合,将边转化为集合的关系

    • 读取[1,2]后,原集合变化如下:

    • 读取[2, 3]后,集合变化如下:

    • 读取[3, 4]后,集合变化如下:

    • 读取[1, 4]时,因为1和4的代表节点相同,说明这两个节点本在同一个集合中;又因为是无向图,路径是双通的,1能够到达2,而且2能够到达4,再加上1能够到达4,说明1能通过两条路径到达4,,这也意味着这条边出现的时候,原图中一定出现了环,那么就把这条边输出就可以了

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

2.1.2 该题的伪代码

   vector<int> rp(1001);
   int sz = edges.size();
   for(i=0 to sz-1)
      初始化各元素为单独的集合,代表节点就是其本身;
   for(j=0 to sz-1)
   {
       定义集合set1和set2;
       调用函数find()找到边上两个节点所在集合的代表节点;
       if(两个集合代表节点相同,即出现环)  
           return edges[j]; 
       else //两个集合独立,合并集合:将前一个集合代表节点戳到后一个集合代表节点上
           rp[set1] = set2;
   }
   return {0, 0};

  int find(int n, vector<int> &rp)
  {
       int num = n;
       查找当前节点所在的代表节点;
       返回代表节点;
  }

2.1.3 运行结果

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

优势:
1.这题应用了并查集的知识点,虽然之前学过了,但是用起来不是很熟练,这题用并查集知识点来做就很轻轻松松
2.题解里还有用DFS的解法,不过与其相比,并查集的做法时间复杂度就小了,DFS需要O(n²),而并查集只需要O(n)

难点:
1.由于对并查集的解法还不是太熟练,看到题目想不到还能用并查集来做,然后看题解思路什么的也花了好一会时间
2.对于为什么并查集找到的第一条重复边就是最后出现的,我一开始还没搞懂,然后看了解释,代码思路还是有一点点不太好理解

首先题目中假设仅存在一条冗余边. 假设边E(i),E(i+1),...,E(k)构成一个环,理论上,我们任意删除当前环中的任一条边均可消除冗余连接, 题目要求我们应当返回最后一条边. 由于我们是顺序判断每一条边,因此在判断E(i), E(i+1), ...E(k-1)时,我们尚未发现任何一条边,其两个端点属于同一连通分量,只有到E(k)边时,由于构成环,此时E(k)边的两个端点属于同一连通分量,因此我们发现的第一条两端点属于同一连通分量的边就是题目中要求的最后一条边.

2.2 题目及解题代码

  • 题目:

  • 代码:
int trap(vector<int>& height)
{
    int left = 0, right = height.size() - 1;
    int ans = 0;
    int left_max = 0, right_max = 0;
    while (left < right) {
        if (height[left] < height[right]) {
            height[left] >= left_max ? (left_max = height[left]) : ans += (left_max - height[left]);
            ++left;
        }
        else {
            height[right] >= right_max ? (right_max = height[right]) : ans += (right_max - height[right]);
            --right;
        }
    }
    return ans;
}

2.2.1 该题的设计思路

解释一下出现的变量名:

left_max:表示左边的最大值,它是从左往右遍历找到的
right_max:表示右边的最大值,它是从右往左遍历找到的
left:表示从左往右处理的当前下标
right:表示从右往左处理的当前下标

1.在某个位置i处,它能存的水,取决于它左右两边的最大值中较小的一个

2.只要 right_max[i]>left_max[i] ,那么积水高度将由 left_max 决定,同理可得left_max[i]>right_max[i]

3.对于位置left而言,它左边最大值一定是left_max,这时候,如果left_max<right_max成立,那么能存多少水就可以知道了,右边会不会有更大的right_max,都不影响这个结果;所以当left_max<right_max时,就去处理left下标,反之就去处理right下标

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

2.2.2 该题的伪代码

初始化left指针为0 right指针为size-1;
while(left<right)
{
   if(height[left]<height[right])
      if(height[left]>=left_max)   更新left_max;
      else   累加left_max-height[left]到ans;
      left=left+1;
   else
      if(height[right]>=right_max)   更新right_max;
      else   累加right_max-height[right]到ans;
      right=right-1;
}

2.2.3 运行结果

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

优势:

1.运用双指针的做法,只需要一次遍历,而动态规划需要分左和右两次,减少了空间复杂度

难点:

1.对于在一个位置能容下的雨水量来说,能接的雨水量是等于它左右两边柱子高度最小值减去它的高度,所以要思考如何找所有位置的左右两边的柱子的最大值

2.当初看题解的时候我以为是要通过比较left_max和right_max来判断哪个指针动,但是思考了一会才发现其实不用,因为每次移动的都是小的一边,除非出现更大的,因此hight[left]和height[right]中的较大值始终是所扫描的所有数的最大值

2.3 题目及解题代码

  • 题目:

  • 代码:
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        cur = [1] * n
        for i in range(1, m):
            for j in range(1, n):
                cur[j] += cur[j-1]
        return cur[n-1]

2.3.1 该题的设计思路

1.假设d[i][j]是到达i,j的最多路径,使用动态方程:dp[i][j] = dp[i-1][j] + dp[i][j-1](对于第一行 dp[0][j],或者第一列 dp[i][0],因为都在边界,所以都为1)

2.这题计算出来的路径就是上面一格数字加上左边一格数字的和,这样计算第二行的时候就可以直接在第一行计算,可以用一维数组重复计算,因为设定当前点原来值表示上一行的上面点的值,那么当前点点加左边点就可以了

时间复杂度:O(m∗n)

空间复杂度:O(n)

2.3.2 该题的伪代码

 将一维数组cur首行首列初始化为1;
 for (i=1 to m) 
    for (j=1 to n)
       运用动态方程计算当前点的值;
 返回路径值;

2.3.3 运行结果

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

优势:

1.在使用动态规划的基础上,将二维数组压缩成一维数组,进一步优化了算法,将一个表优化成了表中我们需要的当前行(因为cur未更新前保存的结果是上一行的结果)

2.一维时巧妙的用到了滚动数组,当计算第 i 行 j 列时,cur[0] ~ cur[j - 1] 保存之前计算的第 i 行的值, cur[j] ~ cur[n - 1] 保存第 i - 1 行对应列的值,然后每次更新

难点:

1.一维数组的优化做法相对二维来说比较难理解

2.题目中思路所给的动态方程不太好懂,可以理解为杨辉三角形,每个位置的路径 = 该位置左边的路径 + 该位置上边的路径

posted @ 2020-05-06 00:10  陈璧君  阅读(431)  评论(0编辑  收藏  举报