DS博客作业04--图
0.PTA得分截图
1.本周学习总结
1.1 总结图内容
🍒 图
- 了解图
- 图是一种多对多的概念。即元素(各顶点)之间都可以有任意的关系
🎀无向图
- 顶尖之间连接的线段叫做边,没有方向
- 记两个顶点为
vi
和vj
则这条边表示为(vi,vj)
- 如果一个图所有的连接线都是边,那这个图就是无向图
- 记两个顶点为
- 无向完全图
- 如果任意两个顶点之间都存在边,则这个图就是完全无向图
🎀有向图
- 边如果出现了方向,那这时候这个边就不叫做边叫做弧,图称为有向图
- 一条弧的两个顶点,从弧尾指向弧头
设弧头为vi,弧尾为vj
则表示一条弧就是<vj,vi>
顺序不能反过来
- 有向完全图
- 任意两个顶点之间都存在两条弧
- 要求这两条弧的方向相反
注意:无向图用小括号,有向图用尖括号
🎀简单图
- 如果是有向图则不存在指向自己本身的弧,而且同一条弧不会重复出现,同理如果是无向图,同一条边不重复出现。这样的图为简单图
🎀稠密图是指边或弧很多的图,适用于邻接矩阵的存储结构 稀疏图是指边或弧很少的图,适用于邻接表的存储结构
🎀权:与弧或者边有关的数,一般可以比作路径或者活动的时间等等
🎀子图:一个完整的图的一部分也能形成的一个图。
🎀连通图:图中任意两个顶点都是连通的。
- 连通分量:无向图中的极大连通子图,子图必须是连通的且含有极大顶点数。
- 强连通分量:有向图中的极大强连通子图。
🍒 图的存储结构
🎀邻接矩阵
- 我们要知道,图并不是线性结构,所以不存在顺序存储结构,我们可以借助一个二维数组来表示一个图
- 这个二维数组有一个专业的名称叫做邻接矩阵
- 图的定义
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,弧数
} MGraph; //图的邻接矩阵表示类型
那么定义一个图MGraph G
-
例如:给定一个无向图,观察一下它的邻接矩阵
-
可以发现以下一些特点
- 初始化全部为0,如果两个顶点x,y之间有边,则更改
g.edges[x][y]=1
和g.edges[y][x]=1
- 对角线都为0
- 初始化全部为0,如果两个顶点x,y之间有边,则更改
-
例如:给定一个有权值有向图,观察它的邻接矩阵
-
可以发现以下特点
- 对角线都为0
- 弧尾是行标,弧头是列标
- 弧头指向弧尾是∞
- 有路径,则其值存它的权值
补充,有权值的图的创建,不管是有向还是无向,除了对角线为0外,其它初始化都是∞,即没有边或弧相连
-
代码实现
int a,b;//分别为两个顶点
for i=0 to g.n-1
for j=0 to g.n-1
初始化邻接矩阵
end for
end for
//根据边的个数来控制是1还是0
for i=0 to g.e-1
cin >> a >> b;//输入两个顶点 //输入权值也同理~
g.edges[a][b]=1;
g.edges[b][a]=1;
end for
//这里是无向图的邻接矩阵创建,不讲究方向,所以两个都要置为1
- 好处:
- 直观且容易理解
- 方便查找一个顶点所有的邻接点
- 方便查找两个顶点是否有边
🎀邻接表
- 邻接矩阵好处很多,但是也有不好的,比如它用来存稀疏图的时候会浪费空间,而且不便于增加删除顶点
- 这时就有了邻接表
- 顶点:按照编号的顺序把顶点数据存入一维数组中
- 要有关联同一顶点边或者以顶点为尾的弧
实际上就是数组和链表的结合用法
- 邻接表结点定义
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); //创建图邻接表
{
n,e分别表示顶点数和边数
初始化每个顶点,使每个结点指向NULL
定义指针 ArcNode *p;//一会插入的时候使用头插法
for i=1 to e
分别输出a和b和info
a表示弧尾 b表示弧头 info是a->b的权值
p = new ArcNode;
p->info=info;
p->adjvex = b
p->nextarc = G->adjlist[a].firstarc
G->adjlist[a].firstarc = p;
}
🍒 图的遍历
🎀深度优先搜索遍历 DFS
- 从某一个顶点出发,一个顶点一个顶点的往下走,走到其邻接点全部访问位置
- 根据出发顶点,dfs不唯一
- 有点类似于树的先序遍历
- 如图所示
注意:这里涉及到返回,所以该遍历是使用递归的解法
- 如图所示
- 邻接表做法
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
- 广度优先搜索遍历有点像树的层次遍历
- 从一个顶点出发,访问它所有的邻接点,然后依次访问下一个顶点,再访问下一个顶点没有被访问过的邻接点以此类推
- 如图示:
- 邻接表做法
void BFS(AdjGraph *G, int v)
{
flag = 0;
int a;
queue<int> q;
ArcNode *p;
visited[v] = 1;
q.push(v);
while (!q.empty())
{
a = q.front();
q.pop();
if (!flag)
{
cout << a;
flag = 1;
}
else cout << " " << a;
p= G->adjlist[a].firstarc;
while (p)
{
a = p->adjvex;
if (!visited[a])
{
visited[a] = 1;
q.push(a);
}
p = p->nextarc;
}
}
🍒 最小生成树
- 这里涉及两个算法,分别是适用于邻接矩阵的prime算法和适用于邻接表的kluskal算法
💕prime算法
- 我对这个算法的理解就是 先入一个顶点,初始化其所有顶点,记录已经访问,然后选择其最近的邻接点记录已访问,再比较它的邻接点有没有比前一个顶点更短的路径,然后修改初始化数组
- 可能字面不好理解,这里就不放长图了,我做了一个动图演示
- 具体实现过程如下
#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记录最近顶点的编号}
}
}
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;
}
}
}
适用于稠密图
💕kluskal算法
- 我对这个算法的理解就是把所有边都抽出来 按照从小到大放回原图中,但是如果出现回路就要删掉这条边
- 哈哈 拿同一张图来讲
实际上生成的都是同样的最小生成树,只是方法不同
- 采用并查集改进kluskal算法:
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\n",u1,v1,E[j].w);
k++; //生成边数增1
UNION(t,u1,v1);//将u1和v1两个顶点合并
}
j++; //扫描下一条边
}
}
适用于稀疏图
这里提出一个问题:如何判断图是否有回路?
- 由于我不想打乱顺序。讲完拓扑排序再做一个总结,先往下看
🍒 最短路径
- 最小生成树是有劣势的,主要是因为它利用的是贪心算法,所以这时候有一个最短路径,它的实质实际上就是找某一点到某一点的最短路径,不会像最小生成树那样分开找
- 这里主要是有 适用于稠密图邻接矩阵的Dijkstra算法和适用于稀疏图的Floyd算法**
💕Dijkstra算法
- 这个算法主要是求单源最短路径,其算法的过程跟prim算法及其相似,不一样的是,在修正数组的时候是通过路径之和来比较的
- 如图所示:
观察图,我们可以知道v1到v6有多条路径,可是最短的是红标的部分,通过dijkstra算法使用一个lowcost数组来算
初始化起点的邻接点的路径存储到lowcost数组中,每开启一个顶点记录已访问,从起点开始加,如果起点加后面路径的距离 比原来的lowcost数组里面的值更小 则修正 否则不修正
- 如图所示:
- 其源代码如下
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); //输出最短路径
}
💕Floyd算法
- 这个主要是求多源最短路径,实际上是考虑每个顶点然后进行对其邻接矩阵的修改,比如顶点A到顶点C本来是没有直接的路径到达的,但是当加入一个顶点B的时候,A到C就有路径了就修改
- 第二个修改的点就是,当A到B本来就已经有路径的情况下,假设引入一个D点,考虑D点的时候发现 A->D->B的路径比A->B的路径要短,则进行修改
- 通过以上两个修改的方式,考虑完所有的顶点,对数组进行修改即可
- 这里就不作图了,直接看源代码
- 源代码
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
}
}
}
对比这两种算法,Floyd算法比Dijkstra算法的代码更简洁,如果想要使用Dijkstra算法来计算多源最短路径,得多次调用Dijkstra算法的函数,所以在使用这两种算法之前,先考虑求的是单源还是多源,再优先使用哪一种算法
🍒 拓扑排序
- 拓扑排序就是控制事件的前提条件,比如一个顶点表示一个事件,那么事件V是事件W的前提,也就是说先发生事件V才能发生事件W,那么顶点V一定是在顶点W前面的
- 这里注意,能进行拓扑排序一定是一个有向无环图
- 那么如何进行拓扑排序呢?
- 通过一个动画演示来展示拓扑排序的过程:实际上就是不断“删除”入度为0的点
- 这里拿排课的顺序来举例
拓扑排序不是唯一的
- 代码实现
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; //找下一个邻接点
}
}
}
通过拓扑排序,我们可以判断一个图是否有回路,这就回归到了上上上面的问题,我们在学习kluskal算法的时候,知道,当插入的边和已插入的边顶点形成回路的时候,这条边就不能要
- 先说一下拓扑排序如何判断有没有回路
- 首先思考的是一个拓扑排序,怎么样才象征结束,是不是加入栈的顶点个数等于所有顶点的时候,邻接表遍历到末尾了呢,假设一个图有回路那么拓扑排序就不能进行了 因为明明就还有顶点,但是这些顶点始终都是有入度的,所以入不了栈,那么我们只需要计算入栈的个数是否等于所有顶点的个数即可,如果等于则说明没有回路,不等于则有回路
- 我们还可以通过深度搜索遍历来判断这个图是否连通,即是否有回路
- 其代码实现如下
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;
}
🍒 关键路径
- 我觉得这个知识点是所有图的知识点里最复杂的了,因为东西实在是太多啦,这个关键路径其实就是找找最长路径,那么影响最长路径的一个东西叫做关键活动
- 首先理解什么是AOE网
- 用顶点表示事件,用有向边表示活动,边的权表示活动持续时间。
- 是一个有向无环图
- 关键活动实际上就是指关键路径的边啦,它影响则关键路径
🏍关键路径
-
先了解以下有关于关键路径的四个数组
-
ve(顶点vj):表示事件vj的最早发生时间
-
vl(顶点vj):表示事件vj的最迟发生时间
-
e(i):表示活动ai的最早开始时间
-
l(i):表示活动ai的最迟开始时间
要先算出ve vl 才能算出e和l
简单地说最早发生时间是从前往后算,最迟发生事件则是从后往前算
关键活动指的是l-e为0的活动~
👨🌾举个栗子
-
求解关键路径的过程
- 对有向图拓扑排序
- 根据拓扑序列计算事件(顶点)的ve,vl数组
- 计算关键活动的e[],l[]。
- 找e=l边即为关键活动
- 关键活动连接起来就是关键路径
1.2 谈谈对图的知识点学习体会
我觉得对于图来讲,树就是个弟弟,图的概念要比树多很多,而且这是一个多对多的关系,要把图学好,不仅空间思维要灵活还需要借助画图,而且相对来讲,几乎每一个知识点都有至少一个算法,一般来说是两个算法,我发现图的应用也经常在生活中出现,同时在学习图的时,还会和树进行综合,例如最小生成树之类的,我认为如果要熟练图,还是需要丰富的刷题经验和代码量,不然再写一遍同样的题还是不会写的,一定先把思路理好再写。
3.阅读代码
3.1 题目及其解题代码
🍹题目:冗余连接
题目来源于力扣
难度:中等
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。
返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。
示例 1:
输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
示例 2:输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
注意:
输入的二维数组大小在 3 到 1000。
二维数组中的整数在1到N之间,其中N是输入数组的大小。
🍹解题代码
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;
}
};
3.1.1 该题的设计思路及其伪代码
🍹该题的设计思路
- 这道题目实际上就是让一个图变成树,这个树指的是连通且无环的无向图
- 在没有添加边的时候,各个节点相互独立,在添加边让它们联系在一起的时候,选择一个节点作为两节点的代表结点,表示两节点有边联系在一起,将两点边的关系转化成集合的关系,两点边的连接关系可以转换成两点所在集合的合并关系,于是将其中一点所在集合的代表结点也赋予为另一点所在集合的代表结点,而当两点的代表结点相同时,说明两点本身处于一个集合中,可以形成环,而在此边出现之前图中无边,所以这条边视为形成环的最后一条边
- 时间复杂度:O(n)
- 空间复杂度:O(n)
🍹伪代码
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
vector<int> rp(1001);
定义sz的大小为edges的长度
for(i=0 to sz-1)
rp[i] = i;
end for
for( j=0 to sz-1){
定义ste1和set2
set1 = find(edges[j][0], rp);
set2 = find(edges[j][1], rp);
if(当set1和set2的代表结点相同时)
return edges[j];
else
rp[set1] = set2;
end if
end for
}
return {0, 0};
}
int find(int n, vector<int> &rp){
int num = n;
while(没找到num所在集合的代表结点时)//代表结点的代表结点为本身
num = rp[num];
end while
return num;
}
3.1.2 该题运行结果
3.1.3 分析题目的解题优势以及难点
🍹优势
- 该题将各点各边形成环的判断问题简单转换成判断集合代表定点的问题
- 运用了并查集的知识,相对于DFS遍历的解法来说,时间复杂度从O(n^2)减少到了O(n)
🍹难点
关于vector容器的理解还有一些困难
关于对于并查集遍历的结束的理解,应该为找到代表定点,而代表定点的特征则是所在集合的代表顶点就是本身
3.2 题目及其解题代码
🍹题目: 判断二分图
题目来源于力扣
难度:中等
给定一个无向图graph,当这个图为二分图时返回true。
如果我们能将一个图的节点集合分割成两个独立的子集A和B,并使图中的每一条边的两个节点一个来自A集合,一个来自B集合,我们就将这个图称为二分图。
graph将会以邻接表方式给出,graph[i]表示图中与节点i相连的所有节点。每个节点都是一个在0到graph.length-1之间的整数。这图中没有自环和平行边: graph[i] 中不存在i,并且graph[i]中没有重复的值。
示例 1: 输入: [[1,3], [0,2], [1,3], [0,2]] 输出: true
示例 2: 输入: [[1,2,3], [0,2], [0,1,3], [0,2]] 输出: false
注意:
- graph 的长度范围为 [1, 100]。
- graph[i] 中的元素的范围为 [0, graph.length - 1]。
- graph[i] 不会包含 i 或者有重复的值。
- 图是无向的: 如果j 在 graph[i]里边, 那么 i 也会在 graph[j]里边。
🍹解题代码
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;
}
};
3.2.1 该题的设计思路及其伪代码
🍹该题的设计思路
- 在没有添加边的时候,各个节点相互独立,在添加边让它们联系在一起的时候,选择一个节点作为两节点的代表结点,表示两节点有边联系在一起
- 将两点边的关系转化成集合的关系,两点边的连接关系可以转换成两点所在集合的合并关系,于是将其中一点所在集合的代表结点也赋予为另一点所在集合的代表结点
- 当两点的代表结点相同时,说明两点本身处于一个集合中,可以形成环,而在此边出现之前图中无边,所以这条边视为形成环的最后一条边
- 时间复杂度:O(n)
- 空间复杂度:O(n)
🍹伪代码
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
vector<int> rp(1001);
定义sz的大小为edges的长度
for(i=0 to sz-1)
rp[i] = i;
for( j=0 to sz-1){
定义ste1和set2
set1 = find(edges[j][0], rp);
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(没找到num所在集合的代表结点时)//代表结点的代表结点为本身
num = rp[num];
return num;
}
3.2.2 该题运行结果
3.2.3 分析题目的解题优势以及难点
🍹优势
- 灵活运用了深度遍历的递归减少了代码的大小
- 将二分图转化为图填色的问题,简化了思路,将问题变为简单的遍历所有顶点的相邻顶点
🍹难点
- 关于for循环中auto的理解,auto函数没怎么接触,一开始理解起来有些困难
- 一开始看代码的时候关于颜色什么时候需要填充,什么时候进行深度遍历不是很了解
3.3 题目及其解题代码
🍹题目: 克隆图
题目来源于力扣
难度:中等
给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。
图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])。
class Node {
public int val;
public Listneighbors;
}测试用例格式:
简单起见,每个节点的值都和它的索引相同。例如,第一个节点值为 1(val = 1),第二个节点值为 2(val = 2),以此类推。该图在测试用例中使用邻接列表表示。
邻接列表 是用于表示有限图的无序列表的集合。每个列表都描述了图中节点的邻居集。
给定节点将始终是图中的第一个节点(值为 1)。你必须将 给定节点的拷贝 作为对克隆图的引用返回。
输入:adjList = [[2,4],[1,3],[2,4],[1,3]]
输出:[[2,4],[1,3],[2,4],[1,3]]
解释:
图中有 4 个节点。
节点 1 的值是 1,它有两个邻居:节点 2 和 4 。
节点 2 的值是 2,它有两个邻居:节点 1 和 3 。
节点 3 的值是 3,它有两个邻居:节点 2 和 4 。
节点 4 的值是 4,它有两个邻居:节点 1 和 3 。
示例2:
输入:adjList = [[]] 输出:[[]]解释:输入包含一个空列表。该图仅仅只有一个值为 1 的节点,它没有任何邻居
详情点击链接
🍹解题代码
class Solution {
public:
Node* cloneGraph(Node* node) {
if ( !node ) return NULL;
queue<Node *> m_queue; //利用队列实现图的广度优先遍历
map<Node *,Node *> m_map;
Node *temp;
Node *p;
m_queue.push(node); //第一个结点入队
//首先BFS所有节点,创建新节点,并保存新节点与原节点的映射关系
while(!m_queue.empty()){
//出队元素
temp = m_queue.front();
m_queue.pop();
//新节点创建
p = new Node(temp->val,{});
m_map.insert({temp,p});
//入队元素
for( Node *neighborsNode : temp->neighbors ) {
if( m_map.find(neighborsNode) == m_map.end() ) { //如果该节点已经有了映射关系,则不入队
m_queue.push(neighborsNode);
}
}
}
//遍历所有节点 完成边的链接
map<Node *,Node *>::iterator iter;
for( iter = m_map.begin(); iter != m_map.end(); ++iter ){
for( Node *neighborsNode : iter->first->neighbors ) {
iter->second->neighbors.push_back(m_map.find(neighborsNode)->second);
}
}
return m_map.find(node)->second;
}
};
3.3.1 该题的设计思路及其伪代码
🍹该题的设计思路
- 首先我们要明确这道题要么我们做什么,实际上就是遍历每个节点,根据每个节点的邻接点记录位置,这个我们可以使用map映射关系来做,第二就是更换克隆点位置,注意要考虑特殊情况,例如节点是空的情况或者两个节点的情况,所以,我们通过对题目的理解归纳出两个必须解决的问题
- 通过广度优先遍历所有的节点,因为克隆图的节点数必须与旧图一致,所以每遍历到一个节点创建一个对应的新节点,并利用map保存这个新的节点与原图节点的映射关系。
- 按照原图节点的对应关系,将新图节点进行连接,因为在第一步的时候我们已经把所有节点对应关系保存到map中,只需要遍历map中的全部原节点,找到其邻接节点数组,遍历该数组。然后按照该规则将新图的对应节点相连即可。
🍹伪代码
if 节点为空 return 节点
end if
queue<Node *> m_queue; //利用队列实现图的广度优先遍历
map<Node *,Node *> m_map;//映射新节点和原节点对应关系
Node *temp;
Node *p;
原第一节点入队
//遍历所有结点的时候顺便创建新节点和保留新节点与原结点关系
while(队列不空)
出队,存储temp
创建新节点p
map映射temp,p关系
入队temp的邻接点中没有映射关系的结点
end while
//遍历所有新节点连接
map<Node *,Node *>::iterator iter;//用来遍历map中原来的结点
for m_map映射中原结点的起始 to m_map映射中原结点的结束
再遍历邻接节点数组
连接新图对应节点
end for
3.3.2 该题运行结果
3.3.3 分析题目的解题优势以及难点
🍹优势
- 这道题使用两个map一个映射关系一个遍历,我觉得这样做法大量减少了代码量同时使得代码效率更高,就不需要搞很多个if了
- 这题还有一个骚操作就是,先通过map建立新节点,到后面再根据map来连接边,我觉得这操作好骚啊,表示低端玩家做不到
- 通过map可以通过考虑有没有映射关系来判断是不是访问过,很方便,不需要另外建一个数组啦
🍹难点
- 我觉得这题的难点在于两个map的使用,虽然通过题目会想到深度搜索和广度搜索但是,大家的写法应该都很复杂不会这么简便,因为map的使用会提高很多效率,但是也是很难用的,map是一种容器也是一种映射,阅读很多代码,发现优秀代码都会使用map,如果在写代码的过程中使用map,必须对map的知识点掌握牢固才不会出错
- 我觉得在遍历原结点映射关系数组的时候,连接新节点的边是一个难点,他这里是用map重新定义边,我觉得需要还是map_find和map里面结点的push_back的问题,表示这里看得不是很明白哈啊哈