DS博客作业04--图
0.PTA得分截图
1.本周学习总结
1.1 总结图内容
图的基本定义:图(Graph)G由顶点集合V(G)和边集合E(G)构成
图的基本术语:
(1)线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(Vertex)。
(2)线性表可以没有元素,称为空表;树中可以没有节点,称为空树;但是,在图中不允许没有顶点(有穷非空性)。
(3)线性表中的各元素是线性关系,树中的各元素是层次关系,而图中各顶点的关系是用边来表示(边集可以为空)。
图的分类:
(1)无向图:
如果图中任意两个顶点之间的边都是无向边,即在图中是没有方向的边,则称该图为无向图。
(2)有向图:
如果图中任意两个顶点之间的边都是有向边,即在图中是有方向的边,则称该图为有向图。
(3)完全图:
1.无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。
注:在含有n个顶点的无向完全图有 n×(n-1))/2 条边
2.又向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。
注:在含有n个顶点的有向完全图有n×(n-1)条边
注:当一个图接近完全图时,则称它为稠密图,而当一个图含有较少的边时,则称它为稀疏图
图的基本术语:
顶点的度:
顶点Vi的度是指在图中与Vi相关联的边的条数。对于有向图来说,有入度和出度之分有向图顶点的度等
路径:
在无向图中,若从顶点Vi出发有一组边可到达顶点Vj,则称顶点Vi到顶点Vj的顶点序列为从顶点Vi到顶点Vj的路径为该顶点的入度和出度之和。
连通:
在无向图中,从顶点Vi出发有一组边可以达到顶点vj;
邻接:
1.若无向图中的两个顶点V1和V2存在一条边(V1,V2),则称顶点V1和V2邻接
2.若有向图中存在一条边<V3,V2>,则称顶点V3与顶点V2邻接,且是V3邻接到V2或V2邻接直
V3;
权:
有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权
有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权
子图:
若有两个图G=(V,E),G1=(V1,E2),若V1是V的子集且E2是E的子集,称G1是G的子图。
图存储结构
1.邻接表
2.邻接矩阵
邻接表
邻接表由表头节点和表节点两部分组成,图中每个顶点均对应一个存储在数组中的表头节点。如果这个表头节
点所对应的顶点存在邻接节点,则把邻接节点依次存放于表头节点所指向的单向链表中。
对于下面的无向图:
建邻接表:
由上图中我们知道,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点
的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由
adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则
存储指向边表中下一个结点的指针。例如:v1顶点与v0、v2互为邻接点,则在v1的边表中adjvex
分别为v0的0和v2的2。
对于下面的有向图:
如果是有向图,邻接表结构是类似的,但要注意的是有向图由于有方向的。因此,有向图的邻接表分
为出边表和入边表(又称逆邻接表),出边表的表节点存放的是从表头节点出发的有向边所指的尾
节点;入边表的表节点存放的则是指向表头节点的某个顶点
建立邻接表:
对于下面的带权图:
对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可
建立邻接表:
建邻接矩阵
图的邻接矩存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为
邻接矩阵)存储图中的边或弧的信息。
对于下面的无向图
建邻接矩阵
在邻接矩阵中我们可以设置两个数组,顶点数组为vertex[4]={v0,v1,v2,v3},边数组arc[4]
[4]为上图右边这样的一个矩阵。对于矩阵的主对角线的值,即arc[0][0]、arc[1][1]、arc[2]
[2]、arc[3][3],全为0是因为不存在顶点的边。
对于有向图
建立邻接矩阵
缺点:
对于存在n个顶点的图需要n*n个数组元素进行存储,当图为稀疏图时,使用邻接矩阵存储方法
将会出现大量0元素,这会造成极大的空间浪费。这时,可以考虑使用邻接表表示法来存储图
中的数据
图遍历及应用
DFS算法(深度优先遍历)
深度优先搜索遍历的过程是:
(1)从图中某个初始顶点v出发,首先访问初始顶点v。
(2)选择一个与顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,
直到图中与当前顶点v邻接的所有顶点都被访问过为止。
具体的操作过程
令初始条件下所有节点为白色,选择一个作为起始顶点,按照如下步骤遍历:
1. 选择起始顶点涂成灰色,表示还未访问
2. 从该顶点的邻接顶点中选择一个,继续这个过程(即再寻找邻接结点的邻接结点),一直深入下
去,直到一个顶点没有邻接结点了,涂黑它,表示访问过了
3. 回溯到这个涂黑顶点的上一层顶点,再找这个上一层顶点的其余邻接结点,继续如上操作,如果
所有邻接结点往下都访问过了,就把自己涂黑,再回溯到更上一层。
4. 上一层继续做如上操作,知道所有顶点都访问过。
从顶点1开始做深度搜索:
1.初始状态,从顶点1开始
2.依次访问过顶点1,2,3后,终止于顶点3
3.从顶点3回溯到顶点2,继续访问顶点5,并且终止于顶点5
4.从顶点5回溯到顶点2,并且终止于顶点2
5.从顶点2回溯到顶点1,并终止于顶点1
6.从顶点4开始访问,并终止于顶点4
邻接矩阵表示
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,弧数
} MGraph; //图的邻接矩阵表示类型
void DFS(MGraph g, int v)//深度遍历
{
static int n = 0;
int j;
if (visited[v] == 0)
{
if (n == 0)
{
cout << v;
n++;
}
else
{
cout << " " << v;
n++;
}
visited[v] = 1;
}
for (j = 1; j <= g.n; j++)
{
if (g.edges[v][j] && visited[j] == 0)
DFS(g, j);
}
}
邻接表表示
typedef struct ANode
{ int adjvex; //该边的终点编号
struct ANode *nextarc; //指向下一条边的指针
int info; //该边的相关信息,如权重
} ArcNode; //边表节点类型
void DFS(AdjGraph* G, int v)//v节点开始深度遍历
{
static int n = 0;
ArcNode* p;
visited[v] = 1;
if (!n)
{
cout << v;
n++;
}
else
{
cout << " " << v;
n++;
}
p = G->adjlist[v].firstarc;
while (p != NULL && n < G->n)
{
if (visited[p->adjvex] == 0)DFS(G, p->adjvex);
p = p->nextarc;
}
}
BFS算法(广度优先遍历)
广度优先搜索遍历的过程是:
(1)访问初始点v,接着访问v的所有未被访问过的邻接点。
(2)按照次序访问每一个顶点的所有未被访问过的邻接点。
(3)依次类推,直到图中所有顶点都被访问过为止。
具体操作步骤
1 .首先选择一个顶点作为起始结点,并将其染成灰色,其余结点为白色。
2. 将起始结点放入队列中。
3. 从队列首部选出一个顶点,并找出所有与之邻接的结点,将找到的邻接结点放入队列尾部,将已访问过结点涂成黑色,没访问过的结点是白色。如果顶点的颜色是灰色,表示已经发现并且放入了队列,如果顶点的颜色是白色,表示还没有发现
4. 按照同样的方法处理队列中的下一个结点。
从顶点1开始进行广度优先搜索:
1.初始状态,从顶点1开始,队列={1}
2.访问1的邻接顶点,1出队变黑,2,3入队,队列={2,3,}
3.访问2的邻接结点,2出队,4入队,队列={3,4}
4.访问3的邻接结点,3出队,队列={4}
5.访问4的邻接结点,4出队,队列={ 空}
建邻接矩阵
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,弧数
} MGraph; //图的邻接矩阵表示类型
void BFS(MGraph g, int v)
{
queue<int>q; //定义队列q
int i, j;
cout << v; //输出起始顶点
visited[v] = 1; //已访问顶点
q.push(v); //顶点加入队列
while (!q.empty()) //队列不空时循环
{
i = q.front(); //出队顶点i
q.pop();
for (j = 1; j <= g.n; j++)
{
if (g.edges[i][j] && !visited[j])//顶点i的邻接点入队并输出
{
cout << " " << j;
visited[j] = 1;
q.push(j);
}
}
}
}
建邻接表
typedef struct ANode
{ int adjvex; //该边的终点编号
struct ANode *nextarc; //指向下一条边的指针
int info; //该边的相关信息,如权重
} ArcNode; //边表节点类型
void BFS(AdjGraph* G, int v)
{
queue<int>q; //定义队列q
ArcNode* p;
int d;
cout << v; //输出起始顶点
visited[v] = 1; //已访问顶点
q.push(v); //顶点加入队列
while (!q.empty()) //队列不空时循环
{
d = q.front(); //出队顶点d
q.pop();
p = G->adjlist[d].firstarc; //顶点d的边结点
while (p)
{
if (visited[p->adjvex] == 0) //每个边结点入队并输出
{
cout << " " << p->adjvex;
visited[p->adjvex] = 1;
q.push(p->adjvex);
}
p = p->nextarc;
}
}
}
最短路径
Dijkstra(迪杰斯特拉)算法
基本思想
通过Dijkstra计算图G中的最短路径时,需要指定起点s(即从顶点s开始计算)。
此外,引进两个集合S和U。S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。
初始时,S中只有起点s;U中是除s之外的顶点,并且U中顶点的路径是”起点s到该顶点的路径”。然后,从U中找出路径最短的顶点,并将其加入到S中;
接着,更新U中的顶点和顶点对应的路径。 然后,再从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 … 重复该操作,
直到遍历完所有顶点。
操作步骤
1.初始时,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为”起点s到该顶点的距离”
2.从U中选出”距离最短的顶点k”,并将顶点k加入到S中;同时,从U中移除顶点k。
3.更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;
4.重复步骤(2)和(3),直到遍历完所有顶点。
对于下面的有向图
用Dijkstra(迪杰斯特拉)算法的遍历过程
从顶点1开始遍历
遍历的S U Dish[] 和 path[]数组
代码
void Dijkstra(MatGraph g,int v)
{
int dist[MAXV],path[MAXV];
int visited[MAXV];
int mindis,j,ul
for(i=0;i<g.n;i++)
{
dist[i]=g.edges[v][i]; //距离初始化
visited[i]=0;
if(g.edges[v][i]<INF)
{
path[i]=v; //顶点v到i有边
}
else
{
path[i]=-1; //顶点v到i边
}
}
visited[v]=1;
for(i=0;i<g.n;i++)
{
mindis=INF;
for(j=0;j<g.n;j++)
{
if(s[j]==0&&dist[j]<mindis) //找最小路径长度顶点u
{
u=j;
mindis=dist[j];
}
}
visited[u]=1; //顶点u加入s中
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])
{
dist[j]=dist[u]+g.edges[u][j];
path[j]=u;
}
}
}
Dispath(dist,path,s,g.n,v);
}
Floyd(弗洛伊德)算法
算法步骤
1,从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
2,对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比已知的路径更短。如果是更新它。
把图用邻接矩阵G表示出来,如果从Vi到Vj有路可达,则G[i,j]=d,d表示该路的长度;否则G[i,j]=无穷大。定
义一个矩阵D用来记录所插入点的信息,D[i,j]表示从Vi到Vj需要经过的点,初始化D[i,j]=j。把各个顶点插入
图中,比较插点后的距离与原来的距离,
3.G[i,j] = min( G[i,j], G[i,k]+G[k,j] ),如果G[i,j]的值变小,则D[i,j]=k。在G中包含有两点之间最短道路
的信息,而在D中则包含了最短通路径的信息。
具体操作过程
代码操作:
void Floyd(MatGraph g) //求每对顶点之间的最短路径
{
int A[MAXVEX][MAXVEX];
int path[MAXVEX][MAXVEX];
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++)
{
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;
}
}
}
}
}
拓扑排序
基本定义
对一个有向无环图G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),
则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序的序列,简称拓扑序列。简单的说,由某个集合
上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序
对于下面的图
拓扑排序具体操作过程
1:删除1或2输出
2:删除2或3以及对应边
3:删除3或者4以及对应边
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]; //出栈一个顶点i
top--;
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.阅读代码
2.1 题目及解题代码
2.1.1 该题代码
2.1.1 该题的设计思路
该题的思路其实就是根据并查集,不断去合并“子树”,从而完成图的连接,一开始对各个节点单独赋
值后根据题目给出的边我们一步一步去合并各个集合,同时边合并边判断是否存在环,因为题目要
就形成的是无环图,所以判断造成环的边再去返回
该题的伪代码
{
定义动态数组rp存储结点
取边长数,以便对节点数组进行初始化
for(int i to sz)
初始化所有节点数组值为节点本身
for(int j=0 to sz){
找边上节点所在集合的父节点
if(父节点相同)
出现环,返回边
else
两个集合独立,合并集合。将前一个集合代表节点戳到后一个集合代表节点上
}
return {0, 0};
}
};
运行截图
分析该题目解题优势及难点
优势:其实一开始我自己看到题目时第一反应就是先建完图再依次去遍历,没有考虑过并查集的使
用,该解题思路并查集的使用降低了解题繁琐度,同时对于如何引入并查集,将节点初始化为
自己这个做法也降低了题目难度,同时对于环的判断也简单易懂
难点:这题的难点主要就是如何对节点进行并查集合并,因为这是本题的核心,也是实现起来需要细
细思考的地方,如果没有理清楚合并思路,则题目就会出现问题。
2.2题目及解题代码
该题的代码
该题的设计思路
该题其实就是判断是否存在环或者说,前驱不存在的点而这个解题最经典的就是拓扑排序,该解题代码
其实就是在使用拓扑排序进行环判断,以此完成题目要求
该题的伪代码
定义一个记录入度得动态数组indegree
定义数组graph以构建临接表
vector<int> v;
for (int i = 0 to numCourses)
{
将所有入度赋值为0以方便构建邻接表
}
for (int i to prerequisites.size())
{
构建邻接表,修改入度值
}
将入度为0的顶点入队
for (int i = 0 to numCourses)
{
if (入度值== 0)
入队
}
定义cnt = 0;
while (队不空)
{
取队首
cnt++;
for (int i = 0 to graph[temp].size())
{
修改入度判断
if (修改后入度为0)
将此时的边入队
}
}
return cnt == numCourses;
}
运行截图
分析该题目解题优势及难点
课程表是拓扑排序很经典的题目,该解题方法在运行时间上也就是时间复杂度上其实不高,但是占用的内存在数据过大
时会很多,它的思路其实很明显就是在拓扑排序修改前驱,以此判断是不是存在环
题目
该题的代码
该题的伪代码
bool isBipartite()函数
{
构造动态数组f,数组大小为函数参数graph的大小
for (int i = 0; i < graph.size(); i++)
{
if (元素i还未访问)
then if (顶点i与第一个顶点都着了色且颜色相同),return false
}
return true;
}
bool Recur()函数
{
for (int idx = 0; idx < graph[i].size(); idx++)
{
if (顶点idx还未访问)
{
if (顶点idx与邻接点都着了色且颜色相同)
then return false
}
else if (顶点idx的颜色值与函数参数target不同)
then return false
}
return true;
}
该题的设计思路
没有访问的节点的颜色初始化为-1,然后访问的时候,将它染成0或者1,遍历它相连的节点,将它的相连的节点染成不同的颜色。如
果深度优先遍历图的时候碰到了和该节点染的颜色不一样的且已经染了色的节点,说明就不是二分图了。
该算法的时间复杂度为O(e),e为边数;空间复杂度为O(n),n为顶点数
该题的运行截图
分析该题目解题优势及难点
该算法通过给每个节点赋上颜色值,巧妙地通过比较节点和邻接点的颜色值是否相同,最终在递归遍历节点的过程中进行二分图的判断:如
果某一节点和其相邻节点都已着色且两节点的颜色值相同,则不是二分图。