赞
踩
- 最小生成树可能有多个,但边的权值之和总是唯一且最小的。
- 最小生成树的边数 =顶点数 -1。砍掉一条则不连通,增加一条边则会出现回路。
- 如果一个连通图本身就是一棵树,则其最小生成树就是它本身。
- 只有连通图才有生成树,非连通图只有生成森林。
求最小生成树的两种方法
从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。时间复杂度: O(V2)适合用于边稠密图。核心思想:贪心算法
同一顶点开始生成的最小生成树可能也不一样,但是最小代价是一样的
Prim 算法的实现思想:
1.初始:从V0开始,标记各节点是 否已加⼊树isJoin,各节点加⼊树 的最低代价,lowCost
2. 第1轮:循环遍历所有个结点,找 到lowCost最低的,且还没加⼊树 的顶点将该顶点加入树,再次循环遍历,更新还没加⼊的 各个顶点的lowCost值
3. 重复1,2,从V0开始,总共需要 n-1 轮处理,每⼀轮处理:循环遍历所有个结 点,找到lowCost最低的,且还没 加⼊树的顶点。 再次循环遍历,更新还没加⼊的 各个顶点的lowCost值,
每⼀轮时间复 杂度O(2n),总时间复杂度 O(n2),即O(|V|2)
- void Prim(G, T)
- {
- // T为空;
- // U = {w};
- while((V-U)! = NULL)
- {
- 设(u,v)为让u属于U,v属于(V-U)对最短边
- T = T U {(u,v)}; //边入树
- U = U U {v}; //顶点入树
- }
- }
-
- //辅助数组:
- isJoin[vexNum]; //标记各节点是否已加入树
- lowCost[vexNum]; //各节点加入树的最小代价 != 权值,每次并入新节点后都需要更新
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选)直到所有结点都连通。时间复杂度: O(|E|log|E|)适合用于边稀疏图。
1. 初始:将各条边按权值排序
2.第1轮:检查第1条边的两个顶点是否 连通(是否属于同⼀个集合) 不连通,则连起来
2.第i轮:检查第i条边的两个顶点是否 连通(是否属于同⼀个集合)不连通,则连起来,已连通,则跳过
共执⾏ e 轮,每轮判断两个顶点是 否属于同⼀集合,需要 O(log2e) 总时间复杂度 O(elog2e)
- void Kruskal(v, T)
- {
- T = v;
- numS = n; //连通分量数
- while(numS>1)
- {
- 从E中选取权值最小的边(u,v);
- if(v和u属于不同连通分量)
- {
- T = T U {(v,u)}; //边入树
- numS--;
- }
- }
- }
⽆权图可以视为⼀种特殊的带权图,只是每条边的权值都为1
从2出发寻找无权图的单源最短路径
使用 BFS算法求无权图的最短路径问题,需要使用三个数组
d[]
数组用于记录顶点 u 到其他顶点的最短路径。path[]
数组用于记录最短路径从那个顶点过来。visited[]
数组用于记录是否被访问过。在visit⼀个顶点时,修改其最短路径⻓度 d[ ] 并在 path[ ] 记录前驱结点
代码实现:
- #define MAX_LENGTH 2147483647 //地图中最大距离,表示正无穷
-
- // 求顶点u到其他顶点的最短路径
- void BFS_MIN_Disrance(Graph G,int u){
- for(i=0; i<G.vexnum; i++){
- visited[i]=FALSE; //初始化访问标记数组
- d[i]=MAX_LENGTH; //初始化路径长度
- path[i]=-1; //初始化最短路径记录
- }
- InitQueue(Q); //初始化辅助队列
- d[u]=0;
- visites[u]=TRUE;
- EnQueue(Q,u);
- while(!isEmpty[Q]){ //BFS算法主过程
- DeQueue(Q,u); //队头元素出队并赋给u
- for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w)){
- if(!visited[w]){
- d[w]=d[u]+1;
- path[w]=u;
- visited[w]=TRUE;
- EnQueue(Q,w); //顶点w入队
- }
- }
- }
- }
带权路径⻓度——当图是带权图时,⼀条路径上所有边的权值之和,称为该路径的带权路径⻓度
使用 Dijkstra算法求最短路径问题,需要使用三个数组:
final[]
数组用于标记各顶点是否已找到最短路径。dist[]
数组用于记录各顶点到源顶点的最短路径长度。path[]
数组用于记录各顶点现在最短路径上的前驱。1. 初始:从V0开始,初始化三个数组信息
2. 第1轮:循环遍历所有结点,找到还没确定最短 路径,且dist 最⼩的顶点Vi,令final[i]=ture,检查所有邻接⾃ Vi 的顶点,若其 final 值为false, 则更新 dist 和 path 信息
3.重复过程2,n-1轮处理,直到所有顶点的final 值为true.并更新完成
4. 使⽤数组信息
代码实现:
- #define MAX_LENGTH = 2147483647;
-
- // 求顶点u到其他顶点的最短路径
- void BFS_MIN_Disrance(Graph G,int u){
- for(int i=0; i<G.vexnum; i++){ //初始化数组
- final[i]=FALSE;
- dist[i]=G.edge[u][i];
- if(G.edge[u][i]==MAX_LENGTH || G.edge[u][i] == 0)
- path[i]=-1;
- else
- path[i]=u;
- final[u]=TREE;
- }
-
- for(int i=0; i<G.vexnum; i++){
- int MIN=MAX_LENGTH;
- int v;
- // 循环遍历所有结点,找到还没确定最短路径,且dist最⼩的顶点v
- for(int j=0; j<G.vexnum; j++){
- if(final[j]!=TREE && dist[j]<MIN){
- MIN = dist[j];
- v = j;
- }
- }
- final[v]=TREE;
- // 检查所有邻接⾃v的顶点路径长度是否最短
- for(int j=0; j<G.vexnum; j++){
- if(final[j]!=TREE && dist[j]>dist[v]+G.edge[v][j]){
- dist[j] = dist[v]+G.edge[v][j];
- path[j] = v;
- }
- }
- }
- }
时间复杂度: O(n2)即O(|V|2)
逐遍的对图中每一个边去迭代计算起始点到其余各点的最短路径,执行N-1遍,最终得到起始点到其余各点的最短路径。(N为连通图结点数)
1. 迪杰斯特拉算法是借助贪心思想,每次选取一个未处理的最近的结点,去对与他相连接的边进行松弛操作;贝尔曼福特算法是直接对所有边进行N-1遍松弛操作。
2. 迪杰斯特拉算法要求边的权值不能是负数;贝尔曼福特算法边的权值可以为负数,并可检测负权回路。
优于Dijkstra的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高。时间复杂度是O(nm)
名词解释:
1. 松弛操作:不断更新最短路径和前驱结点的操作。
2. 负权回路:绕一圈绕回来发现到自己的距离从0变成了负数,到各结点的距离无限制的降低,停不下来
注意:如果图中有负权回路的话,最短路就不一定存在了,转一圈长度就会减少,因此我们可以转无穷多圈,转无穷多圈总长度就会变成负无穷,出圈的话还是负无穷。所以说图中存在负权回路的话,从1号点到n号点的距离就会变成负无穷,就不存在了。所以能求出来最短路的话,图中是没有负权回路的。
而Bellman-ford算法是可以判断图中存不存在负权回路。首先上面的迭代次数是有实际意义的,比如我们迭代了k次,那么我们求的最短距离就是从1号点经过不超过k条边走到n号点的最短距离。所以在第n次迭代的时候又更新了某些边的话,就说明路径中一定存在环,并且是负权回路。因为第n次迭代在不存在负权回路的情况下是遍历到第n号点了,后面是没有点了,如果还能更新,说明路径中存在回路,而且是负权回路。
一般找负环是不用Bellman-ford算法,一般是用SPFA算法——Bellman—Ford算法的队列优化
首先,Bellman算法的核心是松驰,和Dijsktra算法不一样,Djikstra算法是松驰+贪心(,其实质就是在问相应边对面的顶点————“你能够被改进(更短)吗?”)
最短路算法的本质,都是在研究 松驰的顺序!通过不断的松驰,最终求得每个顶点的最短路
经过上面的分析,可以得出,bellman的松驰顺序是的策略是,暴力遍历,无脑松驰。
串联问题一般发生在求解有边数限制的最短路问题中
在遍历的过程中,虽然说第二层的节点的dist可能任然为初始化的正无穷,但是由于第一层的更新和第二层的更新是同时的,很有可能更新完某个第一层节点,恰好后面去更新与它相连的第二层节点,那么该第二层节点的dist由于第一层节点的更新也更新了(如果该第二层节点同时也是处于第一层位置,
可以发现,如果我们没有备份上一次的dist数组的话,限制从1出发不超过1条边到3最短距离本应该是3,但变成了2。内层循环只迭代了一次,但是在更新的过程中会发生”串联”
防止串联,其实就是防止在第k次循环,更新k+1层节点时,由于k+1层节点的更新和确定,以k+1更新后的结果为基础松驰了与之相连的下一层的某个节点。!
假设每次迭代,遍历所有边,遍历边的顺序如下:
1→2, 1→3, 2→3
遍历完第一条边dist[2] = 1,遍历完第二条边dist[3] = 3,遍历第三条边,由于1→2的dist已经确定,在掌握这个信息的前提下,发生串联,dist[3]可以直接松驰,更新为dist[3] = 2,但这不是我们想要的答案。我们想要的是:迭代k次,得到从源点出发,不超过k条边的最短路。
怎么保证不发生串联呢?我们保证更新的时候只用上一次循环的结果就行。所以我们先备份一下。备份之后backup数组存的就是上一次循环的结果,我们用上一次循环的结果来更新距离。所以我们这样写dist[b]=min(dist[b],backup[a]+w)来更新距离,而不是dist[b]=min(dist[b],dist[a]+w),这样写就会发生上面说的”串联”现象。
假如我们现在是第k次迭代,那么backup保留的是第k-1次迭代后获得的信息。
在这个例子中,backup保留的是没有迭代之前(比如站在3的视角,它不会知道1→2的距离,即使1→2的距离在2→3之前更新,这样就不会因为1→2dist的确定,而串联确定2→3)
1. 初始化源点s到各个点v的路径dis[v] = ∞,dis[s] = 0。
2. 进行n - 1次遍历,每次遍历对所有边进行松弛操作,满足则将权值更新。
松弛操作:以a为起点,b为终点,ab边长度为w为例。dis[a]代表源点s到a点的路径长度,dis[b]代表源点s到b点的路径长度。如果满足下面的式子则将dis[b]更新为dis[a] + w。
dis[b] > dis[a] + w
3. 遍历都结束后,若再进行一次遍历,还能得到s到某些节点更短的路径的话,则说明存在负环路。
代码实现
- int n, m; // n表示点数,m表示边数
- int dist[N]; // dist[x]存储1到x的最短路距离
-
- struct Edge // 边,a表示出点,b表示入点,w表示边的权重
- {
- int a, b, w;
- }edges[M];
-
- // 求1到n的最短路距离,如果无法从1走到n,则返回-1。
- int bellman_ford()
- {
- memset(dist, 0x3f, sizeof dist);
- dist[1] = 0;
-
- // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
- for (int i = 0; i < n; i ++ )
- {
- for (int j = 0; j < m; j ++ )
- {
- int a = edges[j].a, b = edges[j].b, w = edges[j].w;
- if (dist[b] > dist[a] + w)
- dist[b] = dist[a] + w;
- }
- }
-
- if (dist[n] > 0x3f3f3f3f / 2) return -1;
- return dist[n];
- }
Bellman-ford算法,直接就是两层循环,内层循环所有边,外层循环就是循环所有边的次数,这个外层循环次数k一般是题目控制的,也即经过不超过k条边走到n号点。时间复杂度是O(n*m)
Bellman—Ford算法的队列优化还有一个名字叫做SPFA。
优化原理
简洁起见,我们约定加权有向图G不存在负权回路,即最短路径一定存在。用数组dis记录每个结点的最短路径估计值,而且用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
- #include <iostream>
- #include <algorithm>
- #include <string.h>
- #include <queue>
-
- #define inf 1e5+5
-
- using namespace std;
-
- int n,m,s,t;
- int vis[1005],cost[1005],h[205];
- int tot;
-
- struct Node //结点信息
- {
- int to,w,next;
- }road[3000];
- struct Node2
- {
- int cost1,to;
-
- friend bool operator < (Node2 x,Node2 y)
- {
- return x.cost1 > y.cost1;
- }
- }road1[3000];
-
- void add(int u,int v,int w) //链式前向星存储邻接表
- {
- road[tot].to = v;
- road[tot].w = w;
- road[tot].next = h[u];
- h[u] = tot++;
- }
-
- void SPFA(int s)
- {
- priority_queue <Node2> pq;
-
- int e = 0;
-
- memset(vis,0,sizeof(vis));
-
- cost[s] = 0;
- road1[e].cost1 = 0;
- road1[e].to = s;
- pq.push(road1[e]);
-
- while(!pq.empty())
- {
- int x = pq.top().to;
- pq.pop();
-
- if(vis[x] ++) continue;
-
- for(int i = h[x]; i != -1; i = road[i].next)
- {
- int to = road[i].to;
-
- if(!vis[to] && cost[x] + road[i].w <= cost[to])
- {
- cost[to] = cost[x] + road[i].w;
-
- e++;
- road1[e].cost1 = cost[to];
- road1[e].to = to;
- pq.push(road1[e]);
- }
- }
- }
- }
-
- int main()
- {
- while(~scanf("%d%d",&n,&m))
- {
- int u,v,w;
- tot = 0;
-
- memset(h,-1,sizeof(h));
- for(int x = 0; x < 1005; x++)
- {
- cost[x] = inf;
- }
-
- for(int i = 0; i < m; i++)
- {
- cin >> u >> v >> w;
-
- add(u,v,w);
- add(v,u,w);
- }
-
- cin >> s >> t;
-
- SPFA(s);
-
- if(cost[t] == inf)
- {
- cout << -1 << endl;
- }
- else
- {
- cout << cost[t] << endl;
- }
- }
- }
-
求出每⼀对顶点之间的最短路径,使⽤动态规划思想,将问题的求解分为多个阶段。
可以⽤于负权值带权图,但是不能解决带有“负权回路”的图(有负权值的边组成回路),这种图有可能没有最短路径。
Floyd算法使用到两个矩阵:
dist[][]
:目前各顶点间的最短路径。path[][]
:两个顶点之间的中转点。递推一个n阶方阵序列,,...,,...,,其中[i][j]表示从顶点vi到vj的长度,k表示绕行第k个顶点的运算步骤,利用记录节点的中转情况。
步骤:①初始时若v0到vi之间有边,则记录其最短路径为该边权值,若不存在则记∞
②尝试允许经过v0顶点中转,更新顶点间最短路径
③依此尝试允许经过v1,v2,...,vk顶点中转,并不断更新最短路径,,直到允许v(n-1)顶点都经过中转,方阵 [i][j] = Min{[i][j] , [i][k]+[k][j]}
④经过n次迭代,最终[i][j]就是vi到vj的最短路径长度
代码实现:
- //初始化矩阵A和path
- ...
- for(int k=0; k<n; k++)
- {
- for(int i=0; i<n; i++)
- {
- for(int j=0; j<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; //中转点
- }
- }
- }
- }
算法分析:时间复杂度——O(),空间复杂度——O()
有向⽆环图:若⼀个有向图中不存在环,则称为有向⽆环图,简称DAG图(Directed Acyclic Graph)
DAG描述表达式:((a+b)*(b*(c+d))+(c+d)*e)*((c+d)*e)
求最小顶点数时, 顶点中不可能出现重复的操作数
用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行。
在图论中,由⼀个有向⽆环图的顶点组成的序列,当且仅当满⾜下列条件时,称为该图的⼀个拓扑排序:
或定义为:拓扑排序是对有向⽆环图的顶点的⼀种排序,它使得若存在⼀条从顶点 A 到顶点 B 的路径,则在排序中顶点 B 出现在顶点 A 的后⾯。每个 AOV ⽹都有⼀个或多个拓扑排序序列。
拓扑排序:找到做事的先后顺序
① 从AOV⽹中选择⼀个没有前驱(⼊度为0)的顶点并输出。
② 从⽹中删除该顶点和所有以它为起点的有向边。
③ 重复①和②直到当前的AOV⽹为空或当前⽹中不存在⽆前驱的顶点为⽌(说明有回路)。
- #define MaxVertexNum 100 //图中顶点数目最大值
-
- typedef struct ArcNode{ //边表结点
- int adjvex; //该弧所指向的顶点位置
- struct ArcNode *nextarc; //指向下一条弧的指针
- }ArcNode;
-
- typedef struct VNode{ //顶点表结点
- VertexType data; //顶点信息
- ArcNode *firstarc; //指向第一条依附该顶点的弧的指针
- }VNode,AdjList[MaxVertexNum];
-
- typedef struct{
- AdjList vertices; //邻接表
- int vexnum,arcnum; //图的顶点数和弧数
- }Graph; //Graph是以邻接表存储的图类型
-
- // 对图G进行拓扑排序
- bool TopologicalSort(Graph G){
- InitStack(S); //初始化栈,存储入度为0的顶点
- for(int i=0;i<g.vexnum;i++){
- if(indegree[i]==0)
- Push(S,i); //将所有入度为0的顶点进栈
- }
- int count=0; //计数,记录当前已经输出的顶点数
- while(!IsEmpty(S)){ //栈不空,则存入
- Pop(S,i); //栈顶元素出栈
- print[count++]=i; //输出顶点i
- for(p=G.vertices[i].firstarc;p;p=p=->nextarc){
- //将所有i指向的顶点的入度减1,并将入度为0的顶点压入栈
- v=p->adjvex;
- if(!(--indegree[v]))
- Push(S,v); //入度为0,则入栈
- }
- }
- if(count<G.vexnum)
- return false; //排序失败
- else
- return true; //排序成功
- }
每个顶点都需要 处理⼀次,每条边都需要处 理⼀次
时间复杂度:O(|V|+|E|) 若采⽤邻接矩阵,则需O(|V|2)
对⼀个AOV⽹,如果采⽤下列步骤进⾏排序,则称之为逆拓扑排序:
① 从AOV⽹中选择⼀个没有后继(出度为0)的顶点并输出。
② 从⽹中删除该顶点和所有以它为终点的有向边。
③ 重复①和②直到当前的AOV⽹为空。
- #define MaxVertexNum 100 //图中顶点数目最大值
-
- typedef struct ArcNode{ //边表结点
- int adjvex; //该弧所指向的顶点位置
- struct ArcNode *nextarc; //指向下一条弧的指针
- }ArcNode;
-
- typedef struct VNode{ //顶点表结点
- VertexType data; //顶点信息
- ArcNode *firstarc; //指向第一条依附该顶点的弧的指针
- }VNode,AdjList[MaxVertexNum];
-
- typedef struct{
- AdjList vertices; //邻接表
- int vexnum,arcnum; //图的顶点数和弧数
- }Graph; //Graph是以邻接表存储的图类型
-
- // 对图G进行逆拓扑排序
- bool ReverseTopologicalSort(Graph G){
- InitStack(S); //初始化栈,存储出度为0的顶点
- for(int i=0;i<g.vexnum;i++){
- if(outdegree[i]==0)
- Push(S,i); //将所有出度为0的顶点进栈
- }
- int count=0; //计数,记录当前已经输出的顶点数
- while(!IsEmpty(S)){ //栈不空,则存入
- Pop(S,i); //栈顶元素出栈
- print[count++]=i; //输出顶点i
- for(p=G.vertices[i].firstarc;p;p=p=->nextarc){
- //将所有指向i的顶点的出度减1,并将出度为0的顶点压入栈
- v=p->adjvex;
- if(!(--outdegree[v]))
- Push(S,v); //出度为0,则入栈
- }
- }
- if(count<G.vexnum)
- return false; //排序失败
- else
- return true; //排序成功
- }
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如 完成活动所需的时间),称之为⽤边表示活动的⽹络,简称 AOE ⽹ (Activity On Edge NetWork)。
AOE⽹具有以下两个性质:
源点和汇点:在 AOE ⽹中仅有⼀个⼊度为 0 的顶点,称为开始顶点(源点),它表示整个⼯程的开始; 也仅有⼀个出度为 0 的顶点,称为结束顶点(汇点),它表示整个⼯程的结束。
关键路径和关键活动:
活动和事件的最早和最迟发生时间:
若⼀个活动的时间余量为零,则说明该活动必须要如期完成,d(i)=0即l(i) = e(i)的活动ai是关键活动 由关键活动组成的路径就是关键路径
1.求所有事件的最早发生时间ve(): 按拓扑排序序列,依次求各个顶点的 ve(k)即事件的关键路径。
2.求所有事件的最迟发生时间 vl(): 按逆拓扑排序序列,依次求各个顶点的 vl(k)即从后往前推(最后一个点的最早和最迟相等)。
3.求所有活动的最早发生时间 e(): 若边 < vk,v; > 表示活动 ai,则有 e(i) = ve(k)。
4.求所有活动的最迟发生时间 l(): 若边 < Vk,v; > 表示活动 a,则有 l(i) = vl(j)- Weight(vk,v;).
5.求所有活动的时间余量 d(): d() =l(i) - e(i)=最早-最迟。
d(i)=0的活动就是关键活动, 由 关键活动可得关键路径
5.3 关键活动、关键路径的特性:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。