最短路径问题
1:单源最短路径
单源路径的定义:
带权值的有向图 G=(V , E) 加权函数 w: E->R 路径 p = {v0 ,v1,v2,vk) 的权值:
定义从 u 到 v 的最短路径:
Bellman-ford算法
BellMan_Ford算法能表面图中是否存在一个从源点出发可达到的权值为负数的回路。
松弛技术:
在松弛一条边 (u, v),进行测试是否通过 u ,对迄今找到的 v 的最短路径 进行改进, 如果可以更新则更新 d[v] 和 pre [v] 。 此为松弛技术
伪代码如下:
RELAX ( u, v, w)
IF d[v] > d[u] + w(u, v)
THEN d[v] <- w( u, v)
Pre[v] <- u;
Bellman-Ford算法描述:
1,.初始化:将除源点外的所有顶点的最短距离估计值 d[v] ←+∞, d[s] ←0;
2.迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)
3.检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中。
Bellman-Ford 的伪代码:
BELLMAN_FORD (G, w ,s)
1: INITIALIZE-SINGLE-SOURCE (G,s) // 进行初始化
2:for i = 1 to |V[G]| - 1 // 进行 V[G] - 1 次循环
Do for each edge(u,e ) // 对每一条边 (u,e) 进行松弛技术
Do RELAX( u,v,w)
For each edge(u,v) // 判断是否存在 带负权值的回路
Do if d[v] > d[u] + w(u, v)
Then return FALSE
Return
模板如下:
#include <iostream> #define Max 1<<30; #define N 101 using namespace std; typedef struct Edge{ // 边的结构体 int u;// 起始点 int v;// 结束点 int w;// w(u,v) 的值 }Edge; typedef struct Graph{ int dis[N] ;// 定义 d[i] 从 s 到 i 最短距离 int n;// 顶点的个数; int e;// 边的条数 int pre[N]; // 记录前驱 Edge edge[N] ; }Graph; bool bellMan_Ford( Graph graph, int s){ // 进行初始化 for(int i = 0; i < graph.n; i++ ) { graph.dis[i] = Max; graph.pre[i] = -1; } graph.dis[s] = 0; graph.pre[s] = -1; // 第一次 将 dis pre 进行初始化 // 进行 RELAX 操作 for(i = 0; i< graph.n; i++) { // 进行 n-1 次循环 for( int j = 0; j< graph.e; j++) { if(graph.dis[graph.edge[j].v] > graph.dis[graph.edge[j].u] + graph.edge[j].w){ graph.dis[graph.edge[j].v] = graph.dis[graph.edge[j].u] + graph.edge[j].w; graph.pre[graph.edge[j].v] = graph.edge[j].u; } } } for(i = 0; i< graph.e; i++) { if(graph.dis[graph.edge[i].v] > graph.dis[graph.edge[i].u] + graph.edge[i].w) return false; } }
模板解析:
1:传入的参数 Graph 结构 和 开始的 源点 s
2:结果:如果 返回 true 说明 能没有负权值回路(结构的 最短子结构树存在 pre[] 数组中,false 说明 存在。
测试如下:
int main (void){ Graph graph; cin>>graph.n>>graph.e; for(int i = 0 ; i < graph.e ; i++) { cin>>graph.edge[i].u>>graph.edge[i].v>>graph.edge[i].w; } cout<<bellMan_Ford(graph,0)<<endl; }
Dijstra算法:
此算法只适合 权值全部为 非负数的单元路径问题,一个实现得较好的 dijkstra 算法,比BellMan-Ford 算法的运行时间低。
算法描述:
注意: S 是顶点结合, Q 是 用到的最小顶点的优先 队列
1: 初始化 G, s
2: 提取 Q队列中 最短的路劲 的 顶点 u 并将 u 在 Q 中移除
3: 将 u 加入到 S 中
4:对 加入的 u 点 进行 松弛操作 更新 距离 dis[]
5: 判断 Q 是否为空 ,为空算法结束 不为空 转到 2
注意: 在上面 的 while Q!=Φ中,可以 wile |G[v]--| 代替,也就是说 对于 图 找到最短路径 一定 是 n-1 条比 (n 为顶点数)
伪代码如下:
DIJKSTRA(G, w, s)
INITIALIZE-SINGLE-SOURCE( G, s)// 初始化 图 G 和 源点
S <- Φ
Q <- v[G] ;//
While Q != Φ
Do u <- EXTRACT-MIN(Q)
S<- SU{u}
For (eatch vertex v ∈ Adj[u]
Do RELAX (u, v, w)
模板代码如下:
#include<iostream> #define Max 1000 using namespace std; /*最短路径的dijkstra算法 * n 表示结点个数 * v 表示的是 源 * dis[i] 表示从 v 到 i 的距离 (最后记录 最短路径) * prev[]如何 prev[i]表示前驱 */ void dijkstra(int n,int v,int dis[],int prev[],int **c){// n 表示结点个数,v 表示的是 源 dis[i] 表示从 v 到 i 的距离,prev[] bool s[Max];// s[i] 记录 节点 i 是否应经访问 false 为已经访问 for(int i=1;i<=n;i++){ dis[i]=c[v][i]; s[i]=false; // 表示 if(dis[i]==Max) // 如果 从 v 点 到 i 无通路 prev[i]=0; // prev[] 表示 i 节点的直接前驱 else prev[i]=v; //记录 i节点的 前驱 为 v } dis[v] = 0;// 从 v点 到 v 点的距离为 0 s[v] = true;// 将 v 加入到 s[] 中 for( i = 1;i<n;i++){ int temp=Max; int u=v; for(int j=1;j<=n;j++){ if(!s[j] && dis[j]<temp){// 如果s[j]=fale 表示 j 没有使用 并且 dis[j] u=j;temp=dis[j];// 找到距离最短的 点j 并把 j-> u; } } s[u]=true; for(j=1;j<=n;j++){ if(!s[j] && c[u][j]<Max){ int newdis=dis[u]+c[u][j]; if(newdis<dis[j]){ dis[j]=newdis; prev[j]=u; } } } } }
2:每对顶点之间的最短路径
算法的基本描述:
诺 d[i][j] 为 顶点 i 到 j 的最短距离 那么对于 d[i][j] = min {d[i][j] , d[i][k] + w[k][j]} 其中 k ∈ 1,2->n
算法注意:对于 k 应该放在最外层 的循环中,如果放在里面 会导致过早的确定路径,而不能找到真正最短的。
其算法代码 如下:
/* floyd 算法 */ void floyd(int n, int **map, int **dis ){// n 为节点个数, **map 为 路径矩阵, dis[i][j] 表示为从 节点 i 到 节点 j 的最短距离 // 初始化 dis for(int i = 1; i <=n ; i++){ for(int j=1; j <= n; j++){ if( map[i][j] == Max){ // path[i][j] = 0;//表示 i -> j 不通 }else{ // path[i][j] = i;// 表示 i -> j 前驱为 i } } } for(int k = 1; k <=n; k++){ for(int i = 1; i <=n; i++){ for(int j = 1; j <=n; j++){ if(dis[i][j] > dis[i][k] + dis[k][j]){ dis[i][j] = dis[i][k] + dis[k][j]; // path[i][k] = i; // path[i][j] = path[k][j]; } } } } /* for(i = 1; i <=n ; i++){ for(int j=1; j <= n; j++){ cout<<dis[i][j]<<" "; } cout<<endl; }*/ }
第 K短路(Dijkstra)
dij变形, 可以证明每个点经过的次数为小于等于K, 所有把dij的数组dist
由一维变成2维,记录经过该点1次,2次。。。k次的最小值。
输出dist[n-1][k]即可
第 k 短路径
所谓K短路,就是从s到t的第K短的路,第1短就是最短路。
如何求第K短呢?有一种简单的方法是广度优先搜索,记录t出队列的次数,当t第k次出队列时,就是第k短路了。但点数过大时,入队列的节点过多,时间和空间复杂度都较高。
下面是用 A* 算法搜素求解,基本思路与广度搜索相同,只是搜索效率提高.
A*是在搜索中常用的优化,一种启发式搜索。简单的说,它可以用公式表示为f(n) = g(n) + f(n),其中,f(n)是从s经由节点n到t的估价函数,g(n)是在状态空间中从s到n的实际代价,h(n)是从n到t的最佳路径估计代价。在设计 中,要保证h(n)<= n到t的实际代价,这一点很重要,h(n)越接近真实值,速度越快。
由于启发函数的作用,使得计算机在进行状态转移时尽量避开不可能产生最优解的分支,而选择相对较接近最优解的路径进行搜索,降低了时间和空间复杂度。
算法过程:
1.将图反向,用dijstra+heap求出t到所有点的最短距离,目的是求所有点到点t的最短路,用dis[i]表示i到t的最短路,其实这就是A*的启发函数,显然:h(n)<= n到t的实际代价。
2. 定义估价函数。我们定义g(n)为从s到n所花费的代价,h(n)为dis[n],显然这符合A*算法的要求。
3. 初始化状态。状态中存放当前到达的点i,fi,gi。显然,fi=gi+dis[i]。初始状态为(S,dis[S],0),存入优先级队列中。
4. 状态转移。假设当前状态所在的点v相邻的点u,我们可以得到转换:(V,fv,gv)-->(U,fu+w[v][u],gv+w[v][u])。
5. 终止条件。每个节点最多入队列K次,当t出队列K次时,即找到解。
代码没写……