DS博客作业04--图
这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
0.PTA得分截图
1.本周学习总结
图是顶点和边的集合,存储多个点之间的关系,简单来说就是多对多的关系。
1.1 图的存储结构
1.1.1 邻接矩阵
一个一维数组存放图中所有顶点数据,一个二维数组存放顶点间关系(边或弧)的数据,这个二维数组称为邻接矩阵。用二维数组来表示边。下标表示顶点,比如数组egde [i] [j] 表示的是i到j的边的情况,有边则数据不为0,无边则数据为0。注意,点到本身也是没有边的,即edge [i] [j] =0。
- 邻接矩阵结构体定义
#define MAXV 最大顶点个数
//声明顶点类型
typedef struct {
int number;/*顶点编号*/
Info Type info;/*顶点其他信息*/
}VertexType;
//声明邻接矩阵类型
typedef struct {
int edges[MAXV][MAXV];/*邻接矩阵*/
int n, e;/*顶点数、边数*/
VertexType vexs[MAXV];/*顶点信息*/
}MatGraph;
可简化成:
typedef struct
{ int edges[MAXV][MAXV]; /*邻接矩阵*/
int n,e; /*顶点数、弧数*/
} MGraph;
- 邻接矩阵两种类型
无向图(对称):
有向图(可能不对称):
- 邻接矩阵的特点
一个图的邻接矩阵表示是唯一的
特别适合于稠密图的存储(邻接矩阵的存储空间为O(n^2))
- 邻接矩阵建图
void CreateMGraph(MGraph& g, int n, int e)//邻接矩阵建图
{ //n顶点个数,e边个数
int i, j;
int a, b;
for (i = 1; i <= n; i++) //注意输入的起始顶点值从0还是1开始
for (j = 1; j <= n; j++)
g.edges[i][j] = 0;
for (i = 0; i < e; i++)
{
cin >> a >> b;
g.edges[a][b] = 1;
g.edges[b][a] = 1;
}
g.n = n;
g.e = e;
}
时间复杂度为 O(n^2)
1.1.2 邻接表
结合数组和链表的方法来存储。每个顶点有一个单链表,连接这个顶点的所有邻接点,然后将这些顶点的单链表的头结点存到一个数组中。
- 邻接矩阵的结构体定义
//声明边结点类型
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;
- 邻接表两种类型
无向图:
有向图:
- 邻接表的特点
邻接表表示不唯一
特别适合于稀疏图的存储(邻接表的存储空间为O(n+e))
- 邻接表建图
void CreateAdj(AdjGraph*& G, int n, int e)//创建图邻接表
{
int i;
int a, b;
ArcNode* p;
G = new AdjGraph;
//G->adjlist = new VNode[n];//申请空间
for (i = 0; i < n; i++)
G->adjlist[i].firstarc = NULL;
for (i = 0; i < e; i++)
{
cin >> a >> b;
//用头插法创建邻接表
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;
}
G->e = e;
G->n = n;
}
时间复杂度为 O(n+e)
1.1.3 邻接矩阵和邻接表表示图的区别
邻接矩阵 | 邻接表 | |
---|---|---|
表示方式 | 数组表示 | 链式表示 |
适用图 | 稠密图 | 稀疏图 |
时间复杂度 | O(n^2) | O(n+e) |
1.2 图遍历
1.2.1 深度优先遍历DFS(Depth-First-Search)
图中的一个顶点出发,每次遍历当前访问顶点的临界点,一直到访问的顶点没有未被访问过的临界点为止。然后采用依次回退的方式,查看来的路上每一个顶点是否有其它未被访问的临界点。访问完成后,判断图中的顶点是否已经全部遍历完成,如果没有,以未访问的顶点为起始点,重复上述过程。是一个不断回溯的过程。
- 深度遍历代码
邻接矩阵
void DFS(MGraph g, int v)//邻接矩阵深度遍历
{
if (flag == 0)
{
cout << v;
flag = 1;
}
else
cout << " " << v; //输出顶点
visited[v] = 1;//标记已访问该节点
for (int i = 1; i <= g.n; i++)
{
if(g.edges[v][i] == 1 && visited[i] == 0)
{
DFS(g, i); //当前顶点与 i 顶点邻接且未被访问,递归搜索
}
}
}
邻接表
void DFS(AdjGraph* G, int v)//v节点开始深度遍历
{
ArcNode* p;
p = G->adjlist[v].firstarc;
if (flag == 0)
{
cout << v;
flag = 1;
}
else
cout << " " << v;
visited[v] = 1; //标记已访问
while (p)
{
if (!visited[p->adjvex])//未被访问过
DFS(G, p->adjvex);
p = p->nextarc;
}
}
- 应用
判断是否连通
采用深度遍历的方法,先给visited[]数组置初值为0,然后从0顶点开始遍历
记录路径visited[顶点],如果是连通图,它的深度遍历就可以经过图中所有的点,如果不是连通图,那么它就无法遍历所有的顶点,visited数组会存在数值为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, 0);
for (i = 0; i < G->n; i++)
{
if (visited[i] == 0)/*出现为0的点说明不连通*/
{
flag = false;
break;
}
}
return flag;
}
找简单路径
采用深度遍历的方法,增加path[i]存放路径,递归函数添加形参d
从起始点开始遍历,到遇到终点的时候停止。
方法:用一个数组来存路径,在原函数的基础上加上出口,即遇到终点时结束程序,输出存放路径的数组。
void findPath (AdjGraph* G,int u,int v,int path[],int d)
{
int w,i;
ArcNode* p;
visited[u] = 1;
d++; /*路径长度自增*/
path[d] = u;
if (u == v)/*到达终点*/
{
for(i=0;i<=d;i++)
{
cout << path[i];/*输出*/
}
return;
}
p = G->adjlist[u].firstarc;/*p指向u的第- -个邻接点*/
while (p)
{
w =p->adjvex;/*邻接点编号为w*/
if (!visited[w])
findPath(G,w,v,path,d);
p = p->nextarc ;
}
}
1.2.2 广度优先遍历BFS(Breadth-First-Search)
类似于树的层次遍历。从图中的某一顶点出发,遍历每一个顶点时,依次遍历其所有的邻接点,然后再从这些邻接点出发,同样依次访问它们的邻接点。按照此过程,直到图中所有被访问过的顶点的邻接点都被访问到。最后还需要做的操作就是查看图中是否存在尚未被访问的顶点,若有,则以该顶点为起始点,重复上述遍历的过程。
- 广度遍历代码
邻接矩阵
void BFS(MGraph g, int v)//邻接矩阵广度遍历
{
int t;
queue<int>q;
if (visited[v] == 0)
{
cout << v;
visited[v] = 1;
q.push(v);
}
while (!q.empty())
{
t = q.front();
q.pop();
for (int j = 1; j <= g.n; j++)
{
if (g.edges[t][j] == 1 && visited[j] == 0)
{
cout << " " << j;
visited[j] = 1;
q.push(j);
}
}
}
}
邻接表
void BFS(AdjGraph* G, int v) //v节点开始广度遍历
{
queue<int>q;
ArcNode* node;
int n;//边的序号
int j;
visited[v] = 1;//表示已访问
cout << v ;
q.push(v);//入队
while (!q.empty())//队不空
{
j = q.front();
q.pop();
node = G->adjlist[j].firstarc;
while (node)//按邻接表输出头结点后的所有节点
{
if (!visited[node->adjvex])
{
visited[node->adjvex] = 1;
cout << " " << node->adjvex;
q.push(node->adjvex);
}
node = node->nextarc;
}
}
}
- 应用
找最短路径
定义非循环队列结构体类型:
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.3 最小生成树
生成树是图中的一个最小连通子图,它的边为n-1,且不为回路。对于带权连通图G ,n个顶点,n-1条边
根据深度遍历或广度遍历生成生成树,树不唯一,其中权值之和最小的生成树称为图的最小生成树。
1.3.1 Prim算法求最小生成树
普里姆(Prim)算法是一种构造性算法,用于构造最小生成树。过程如下:
(1)初始化U={v}。v到其他顶点的所有边为候选边;
(2)重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
考察当前V-U中的所有顶点j,修改候选边:若(j,k)的权值小于原来和顶点k关联的候选边,则用(k,j)取代后者作为候选边
-
思路
- 给定一个图,和一个起始点x。初始化U,使得U={x}。
- 在x到其他顶点的边里,选择最小的那条边,并得到x的邻接点y,将y放入U中。
- 判断y的加入是否产生了更短的路径,如果有更短的路径,修改候选边。
- 重复2、3两步直到所有的点都在U中为止。
-
最小生成树求解
- 找最小lowcost
- 加入新顶点,修正lowcost, closest
- Prim算法代码
#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;
}
}
}
- 辅助数据结构
Q:如何选j到U顶点集的最小边?
设置2个辅助数组,closest[i]:最小生成树的边依附在U中顶点编号;lowcost[i]表示顶点i(i ∈ V-U)到U中顶点的边权重,取最小权重的顶点k加入U,并规定lowcost[k]=0表示这个顶点在U中;(closest[k],k)构造最小生成树一条边。
-
Prim算法时间复杂度及适用范围
时间复杂度为 O(n^2) (n为顶点数)
稠密图:与网中的边数无关,从点的方面考虑构建
1.3.2 Kruskal算法求解最小生成树
一种求带权无向图的最小生成树的构造性算法,按权值的递增次序选择合适的边来构造最小生成树的方法
(1)置U的初值等于V(即包含有G中的全部顶点),TE(表示最小生成树的边集)的初值为空集(即图T中每一个顶点都构成一个连通分量)。
(2)将图G中的边按权值从小到大的顺序依次选取:
若选取的边未使生成树T形成回路,则加入TE;
否则舍弃,直到TE中包含(n-1)条边为止。
- 思路:
- 给定一个图,无需知晓起始点
- 在所有边里找最小边,并将其标记,如果这个边没有让这个图产生回路,则可以把它加入生成树。
- 重复第2步,直到生成树包含所有的点为止。
- Kruskal算法代码
typedef struct
{ int u; //边的起始顶点
int v; //边的终止顶点
int w; //边的权值
} Edge;
Edge E[MAXV];
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++; //扫描下一条边
}
}
并查集改良Kruskal算法(时间复杂度是O(elog2e)):
typedef struct
{ int u; //边的起始顶点
int v; //边的终止顶点
int w; //边的权值
} Edge;
Edge E[MAXV];
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\n",u1,v1,E[j].w);
k++; //生成边数增1
for (i=0;i<g.n;i++) //两个集合统一编号
if (vset[i]==sn2) //集合编号为sn2的改为sn1
vset[i]=sn1;
}
j++; //扫描下一条边
}
}
- 辅助数据结构
vset[MAXV]集合辅助数组,2个顶点集合编号不同,加入边不会形成回路
- Kruskal算法时间复杂度及适用范围
**时间复杂度为 O(eloge) **
稀疏图:与网中的边数有关,从边的方面考虑按权值的递增次序选择合适的边来构造最小生成树。
实现Kruskal算法的辅助数据结构是什么?其作用是什么?Kruskal算法代码。
1.3.3 prim算法和kruskal算法的比较
算法名 | prim算法 | kruskal算法 |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | O(n^2) | O(eloge) |
适用范围 | 稠密图 | 稀疏图 |
相同点:加入这条边之后,顶点集合不能构成一个回路。
不同点:Prim算法要求每次添加一条边,都要集合中所有的顶点都是连通状态的;kruskal算法却没有这样的要求,它只需要每条边的权值都是从小往大递增选择的。
1.4 最短路径
最短路径问题是指:
如果从图中某一顶点(源点)到达另一顶点(终点)的路径可能不止一条,如何找到一条路径使得沿此路径上各边的权值总和(称为路径长度)达到最小。两种常见的最短路径问题:
1、 单源最短路径—用Dijkstra(迪杰斯特拉)算法(一顶点到其余各顶点)
2、所有顶点间的最短路径—用Floyd(弗洛伊德)算法(任意两顶点之间)典型用途:
交通问题。城市A到城市B有多条线路,但每条线路的交通费不同,那么选哪一条线路,使总费用最少。
1.4.1 Dijkstra算法求解最短路径
通过 选定的被访问顶点 ,求出从出 发访问顶点到其他顶点的最短路径
- 过程:
初始化顶点合集S和未选顶点合集T;
1.S={入选顶点集合,初值V0},T={未选顶点集合}。
若存在<V0,Vi>,距离值为<V0,Vi>弧上的权值
若不存在<V0,Vi>,距离值为∞
2.从T中选取一个其距离值为最小的顶点W, 加入S
3.S中加入顶点w后,对T中顶点的距离值进行修改:
若加进W作中间顶点,从V0到Vj的距离值比不加W的路径要短,则修改此距离值;
4.重复上述步骤1,直到S中包含所有顶点,即S=V为止。
- 思路:
- 确定起始点x,存入S中;
- 找x连着的边里最小的那条,得到邻接点y。
- 把邻接点加入S中,同时判断它的加入是否会出现更短的路径,如果有更短的路径,则修改路径。剩下未被选择的顶点j,对其最短路径进行修正:将w作为中间顶点,从源点到顶点j的距离比不加入顶点w的路径长度短,那么修改dist[j]= dist[w]+ 边(u,j)的权值,修改j的前驱结点path[j]=w.
- 重复2、3步直到S中包含所有顶点。
S | U | Dist[] | Path[] |
---|---|---|---|
0 1 5 2 ∞ ∞ | 0 0 0 0 -1 -1 | ||
{0,1} | 0 1 4 2 8 ∞ | 0 0 1 0 1 -1 | |
{0,1,3} | 0 1 4 2 8 10 | 0 0 1 0 1 3 | |
{0,1,3,2} | 0 1 4 2 8 10 | 0 0 1 0 1 3 | |
{0,1,3,2,4} | 0 1 4 2 8 10 | 0 0 1 0 1 3 | |
{0,1,3,2,4,5} | 0 1 4 2 8 10 | 0 0 1 0 1 3 |
- Dijkstra算法伪代码
初始化dist数组,path数组,s数组;
遍历图中所有结点
{
遍历dist数组,找为被s收入的距离源点最小的顶点w;
s[w]=1;//将w加入集合s;
for i=0 to g.n //修正未加入s集合的顶点的dist和path
若dist[i]>dist[w]+g.edges[w][i];
dist[i]=dist[w]+g.edges[w][i];
path[i]=w;
end for
}
- Dijkstra算法代码
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++)//找最小路径长度顶点u
{
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;
}
}
}
}
//输出最短路径
}
- 代码注意事项
没有边的前驱结点就初始化为-1;有边就初始化为起点编号;
调整:dist[u]+G.edges[u][j]<dist[j]
- 辅助数据结构
如何存放最短路径长度
用一维数组dist[j]存储(distance):源点V0到每个终点的最短路径长度
源点v默认,dist[j]表示源点→顶点j的最短路径长度
eg. dist[2]=12 表示源点→顶点2的最短路径长度为12
如何存放最短路径
用一维数组path[j]存储(path):最短路径序列的前一个顶点的序号;初值或无路径用-1表示
一条最短路径用一个一维数组表示
eg.从顶点0→5的最短路径为0、2、3、5,表示为path[5]={0,2,3,5}
从源点到其他顶点的最短路径有n-1条,二维数组path[] []存储
- Dijkstra算法如何解决贪心算法无法求最优解问题?
Dijkstra算法本质上是贪心算法,下一条路径都是由当前更短的路径派生出来的更长的路径。不存在回溯的过程。
- Dijkstra算法时间复杂度及适用范围
时间复杂度为O(n^2)
存储结构:邻接矩阵存储
适用范围:不适用带负权值的带权图求单源最短路径,也不适用于求最长路径长度
- Dijkstra算法特点
1.不适用带负权值的带权图求单源最短路径。
2.不适用求最长路径长度。
3.最短路径长度是递增
4.顶点u加入S后,不会再修改源点v到u的最短路径长度
5.按Dijkstra算法,找第一个距离源点S最远的点A,这个距离在以后就不会改变。但A与S的最远距离一般不是直连。
1.4.2 Floyd算法求解最短路径
每一个顶点都是出发访问点 ,所以需要将每一个顶点看做被访问顶点,求出从 每一个顶点到其他顶点的最短路径 。
- 思路:(找任意两点的最小路径)
- 定义两个二维数组,A用于存放当前顶点之间的最短路径长度,分量A[i] [j]表示当前顶点i到顶点j的最短路径长度;二维数组path用于存放结点在最短路径中的前驱结点。
- 考虑顶点x,思路有点像Dijkstra,修改数组A和path,(与这个点相关的路径长度不变,比如下标中有x的),数组path[i] [j]:如果顶点i和顶点j之间存在边,那么就初始化为i,否则初始化为-1;
- 重复考虑点直到所有点都遍历过。
- 最后,根据数组path从终点找前继,直到起点,对应点就是路径的逆序列。数组A[起点编号] [终点编号]就是所求的路径长度。每个顶点作为中间顶点k,遍历二维数组A,判断加入顶点k后,顶点i到顶点j的路径是否比原来要小,如果要小,则修改A[i] [j]为A[i] [k]+A[k] [j],path[i] [j]=k;
- Floyd算法伪代码
初始化二维数组A,二维数组path;
for k=0 to g.n
遍历二维数组A
将顶点k作为中间站,判断加入顶点k后的路径长度是否比原来小;
若 A[i][j]>A[i][k]+A[k][j]
修改A[i][j]=A[i][k]+A[k][j];
修改path[i][j]=k;
end for
- Floyd算法代码
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
}
}
}
-
Floyd算法解决相关问题
解决多源最短路问题
-
Floyd算法优势
Floyd算法适用于APSP(AllPairsShortestPaths),是一种动态规划算法,稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法。可以算出任意两个节点之间的最短距离,代码编写较为简单。
Floyd算法是解决任意两点间的最短路径的一种算法,可以正确处理有向图或无向图或负权(但不可存在负权回路)的最短路径问题,同时也被用于计算有向图的传递闭包。
- Floyd算法时间复杂度及适用范围
时间复杂度:O(n³)
适用范围:弗洛伊德算法可以解决负权值的带权图,也可以解决求最长路径长度问题。
1.5 拓扑排序
将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。
在一个有向图中找一个拓扑序列的过程称为拓扑排序。序列必须满足条件:
每个顶点出现且只出现一次。
若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
有向无环图才有拓扑排序,图中有回路,无法拓扑排序.拓扑排序可以用来检测图中是否有回路.
- 拓扑排序的过程
1.从有向图中选取一个没有前驱的顶点,并输出之;
2.从有向图中删去此顶点以及所有以它为尾的弧;
3.重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。
- 思路:
- 在图中选取一个没有入度的点,输出它,然后删掉这给顶点以及它所有的有向边;
- 重复上述步骤,直到所有点都已经输出为止。
- 伪代码
遍历邻接表
计算每个顶点的入度,存入头结点count成员中;
遍历图顶点
找到一个入度为0的顶点,入栈/队列/数组;
while(栈不为空)
出栈结点v,访问;
遍历v的所有邻接点
{
所有邻接点的入度-1;
若有邻接点入度为0,入栈/队列/数组;
}
- 代码
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; //找下一个邻接点
}
}
}
使用邻接表
当某个顶点的入度为0时 输出顶点信息
设置栈来存放入度为0的顶点
-
如何用拓扑排序代码检查一个有向图是否有环路?
每次找入度为0的点 进入输出队列 然后将与此点相连的节点入度减1 重复做
当做n-1 次后还有点没进输出队列 那么这些点就是环上的 因为环上的各点入度都为1,没有0的 就不能更新。
1.6 关键路径
关键路径是指有向图中从源点到汇点的最长路径。其中,关键路径中的边叫做关键活动
- AOE-网
AOE 网是在 AOV 网的基础上,其中每一个边都具有各自的权值,是一个有向无环网。其中权值表示活动持续的时间,用顶点表示事件,用有向边e表示活动,边的权c(e)表示活动持续时间。是带权的有向无环图。
AOE网——带权的有向无环图
顶点--事件或状态
弧(有向边)--活动及发生的先后关系
权--活动持续的时间
起点--入度为0的顶点(只有一个)
终点--出度为0的顶点(只有一个)
- 关键路径
关键路径是指有向图中从源点到汇点的最长路径。其中,关键路径中的边叫做关键活动。
步骤:
对图进行拓扑排序。
在拓扑排序得到的序列的基础上,计算出边的最早开始时间和最晚开始时间,分别得到ve和vl数组。ve是当前点到起始点的最长路径,vl有点像是找当前点到终点的最长路径,然后用ve[终点]-最长路径;
计算所有边的e和l,其中,对边,e=ve[i],l=vl[l]-边的权值
如果e=l,那么它就是关键活动,而所有的关键活动相连,就是它的关键路径。
- 关键活动
关键活动(key activity)指的是:关键路径中的边.
2.PTA实验作业
2.1 六度空间
“六度空间”理论又称作“六度分隔(Six Degrees of Separation)”理论。这个理论可以通俗地阐述为:“你和任何一个陌生人之间所间隔的人不会超过六个,也就是说,最多通过五个人你就能够认识任何一个陌生人。”如图所示。
“六度空间”理论虽然得到广泛的认同,并且正在得到越来越多的应用。但是数十年来,试图验证这个理论始终是许多社会学家努力追求的目标。然而由于历史的原因,这样的研究具有太大的局限性和困难。随着当代人的联络主要依赖于电话、短信、微信以及因特网上即时通信等工具,能够体现社交网络关系的一手数据已经逐渐使得“六度空间”理论的验证成为可能。
假如给你一个社交网络图,请你对每个节点计算符合“六度空间”理论的结点占结点总数的百分比。
输入格式:
输入第1行给出两个正整数,分别表示社交网络图的结点数N(1<N≤103,表示人数)、边数M(≤33×N,表示社交关系数)。随后的M行对应M条边,每行给出一对正整数,分别是该条边直接连通的两个结点的编号(节点从1到N编号)。
输出格式:
对每个结点输出与该结点距离不超过6的结点数占结点总数的百分比,精确到小数点后2位。每个结节点输出一行,格式为“结点编号:(空格)百分比%”。
2.1.1 伪代码
函数:
/*邻接表建图*/
/*输出百分比*/
主函数main:
建图
for i=1 to n
for j=1 to G->n
初始化各点,将visited置0
输出百分比 sum= (percent * 100.0) / n
end for
邻接表建图函数:
头插法创建邻接表
无向图,两方结点都要遍历
输出百分比函数:
定义队列q,让结点v入队
将结点v标记为已访问
while(队列不空的时候)
temp=队首元素
队首元素出队并标记为已访问
while(p不为空)
if(p->adjvex未被访问)
该结点进队并标记为已访问
记录有联系结点个数的num加一
tail记录此时结点的值
end while
if(队首等于该层最后一个结点)
层数加一,last重置为队尾元素
if(层数为6)
此时可以不再进行结点记录,直接结束
最后返回记录标记的结点个数num
2.1.2 提交列表
2.1.3 本题知识点
-
此题有点的像树的层次遍历,使用queue辅助搜索,visite[i]标记已经认识过的人,然后通过广度优先搜索,把所有人都认识完或者已经达到第六层(最多通过五个人认识)就可以结束搜索。
-
算法思路
1、对每个节点进行广度优先搜索
2、搜索过程中累计访问的节点数
3、需要记录层次,仅计算6层以内的节点数
-
采用邻接表的存储结构进行操作
-
采用BFS搜索,若用DFS会有一个测试点无法通过。该题不是把一整个图都遍历一遍,二十只遍历指定深度的点,用DFS会导致一些在指定深度范围内的点范围不到,遍历就结束了。
例如:
但按照六度空间理论,这个例子中,路线1->3->5->6->7->8->9,顶点9是可以被访问的。所以DFS不适合。
-
在BFS中需要知道遍历到每层最后一个结点并层数+1
引入2个变量 last tail 分别指向 当前层数的最后一个元素 和 一层的最后一个元素 ,tail变量记录每层入队时的结点,last变量记录每层最后一个元素且在该层入队后出来更新last=temp
2.2 公路村村通
现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低成本。
输入格式:
输入数据包括城镇数目正整数N(≤1000)和候选道路数目M(≤3N);随后的M行对应M条道路,每行给出3个正整数,分别是该条道路直接连通的两个城镇的编号以及该道路改建的预算成本。为简单起见,城镇从1到N编号。
输出格式:
输出村村通需要的最低成本。如果输入数据不足以保证畅通,则输出−1,表示需要建设更多公路。
2.2.1 伪代码
函数:
/*邻接矩阵建图*/
/*最小生成树prim算法--连接村庄*/
主函数main:
建图
判断镇的数目是否足以保证通畅
若不正常即-1,正常Prim输出成本
邻接矩阵建图函数:
二维数组输入矩阵,不能初始化为0
初始化邻接矩阵:
输入边依附的两个顶点序号a,b和权值weight
将邻接矩阵的第a行第b列的元素值置为初始权值weight
将邻接矩阵的第b行第a列的元素值置为初始权值weight
最小生成树prim算法--连接村庄函数:
初始化lowcost,closest数组
for v=1 to n
遍历lowcost数组 //选最小边
若lowcost[i]!=0 找最下边邻接点node 若权值不为 0 且小于 min
计算总费用即最小边顶点编号对应的权值
lowcost[node] = 0;
再次遍历lowcost数组 //修正lowcost
修改数组lowcost和closest,检查新加入生成树的顶点和其他顶点是否存在更小的边
若lowcost[i]!=0&& edges[i][node]<=lowcost[node]
修正lowcost[node]=edges[i][node]
end for
判断是否连通
for i=1 to g->n
若还有lowcost[i]
返回-1
2.2.2 提交列表
2.2.3 本题知识点
- 采用邻接矩阵的存储结构进行操作
typedef struct
{
int **edges;//邻接矩阵
int n, e;//顶点数,边数
}MGraph;
-
结构体中用指针edge,后面动态申请空间为
g->edges = new int* [n + 1];
-
本题为最小生成树问题--采用Prim算法,若用floyd算法,不能保证任一两点之间是最短路径
-
Prim算法需要两个辅助数组,closest[i]为最小生成树的边依附在U中顶点编号,lowcost[i]表示顶点i到U中顶点的边权重