集训第二章,最短路。
o(TヘTo)
目录
最短路算法用于求出从给定一个点到任一个点的最短路径长度。
Dijkstra
给定路径从到,路径的边权为。
Dijkstra算法是用于求边权非负时()从起点到任意点的最短路。
Dijkstra算法的基本思想是从出发,逐步地向外探寻最短路径。执行过程中,每个结点记录从起点到这个点的最短路的值,算法的每一步是去修改结点的数值。
void dijkstra()
{
memset(dis , INF , sizeof(dis)) ;//将dis初始化为正无穷
dis[1] = 0 ; //初始1号点的dis为0,
for(int i = 0 ; i < n-1 ; i ++)
{
int t = -1 ; //t记录下一个要走的点
for(int j = 1 ; j <= n ; j ++)
if(!visited[j] && (dis[t] > dis[j] || t == -1))
t = j ; //每次找出最短的路径
for(int j = 1 ; j <= n ; j ++)
dis[j] = min(dis[j] , dis[t]+g[t][j]) ; //dis[j]记录的是从1号点到j的最短路
visited[t] = true ; //标记已经走过的点
}
}
画个图来举栗子~
如图五个点六条边,边权如图所示,求点1到点5的最短路。
首先,将点1初始化:。
起始从点1开始,可以走向点2或点4,则点2和点4的值将被更新,即为点1的值加上从1-4的边权。第二次选取点2,可以走向点3和点4,点3的值为点2的值加上2-3的权,即;而此时的小于第一次更新的点4的值,则此时的点4应为:。
第三次选取点4,从点4到点5,点5的值被更新为 。
最后选取点3,从点4到点5,点5的值被更新为 。
最终从点1到点5的最短路的值为5。
下图为数组的更新过程,也就是每个结点数值的更新,每次更新为最小值,最终得到最小的。
接下来一个模板题,给一个有向图,给出各个边,求从1号点到n号点的最短路,
#include <iostream>
#include <algorithm>
#include <cstring>
#define INF 0x3f3f3f3f
using namespace std ;
const int N = 1e5+10 ;
bool visited[N] ;
int dis[N] ;
int n , m ;
int g[1010][1010] ;
int dijkstra()
{
memset(dis , INF , sizeof(dis)) ;
dis[1] = 0 ;
for(int i = 0 ; i < n-1 ; i ++)
{
int t = -1 ;
for(int j = 1 ; j <= n ; j ++)
if(!visited[j] && (dis[t] > dis[j] || t == -1))
t = j ;
for(int j = 1 ; j <= n ; j ++)
dis[j] = min(dis[j] , dis[t]+g[t][j]) ;
visited[t] = true ;
}
return dis[n] ;
}
int main()
{
cin >> n >> m ;
memset(g , INF , sizeof(g)) ;
for(int i = 0 ; i < m ; i ++)
{
int x , y , z ;
cin >> x >> y >> z ;
g[x][y] = min(z , g[x][y]) ;
}
cout << dijkstra() << endl ;
return 0 ;
}
bellman-ford
bellman_ford算法能找到从某个结点出发到所有结点的最短路(或者某些最短路不存在),支持负权。
用结构体来维护每条路径:
struct node{
int u , v , w ;
} ed[N] ;
bellman_ford算法代码实现如下~
void bellman_ford()
{
memset(dis , 0x3f3f3f3f , sizeof(dis)) ;
dis[1] = 0 ;//从点1开始走
for(int i = 0 ; i < k ; i ++)
{
memcpy(last , dis , sizeof(dis)) ;//记录上一次的路径值
for(int j = 0 ; j < m ; j ++)
{
node e = ed[j] ;
dis[e.v] = min(dis[e.v] ,last[e.u]+e.w) ;//存在u到v的路,v的值为最小的u的值加u到v的权。
}
}
}
还是用这个图来研究bellman_ford算法~
每次循环都是走的当前可走的下一条路径,也就是:第一次走可以走到点2和点4,更新点2和4的值;第二次走可以走到点3、4、5,更新点3、4、5的值;第三次走可以走到5,更新点5的值。
画个每个点值的更新表~
求从点1到点n的最短路径,以下板子:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstring>
using namespace std ;
const int N = 10010 ;
int n , m ;
struct node{
int u , v , w ;
} ed[N] ;
int visited[N] ;
int dis[N] , last[N] ;
int bellman_ford()
{
memset(dis , 0x3f3f3f3f , sizeof(dis)) ;
dis[1] = 0 ;
for(int i = 0 ; i < m ; i ++)
{
memcpy(last , dis , sizeof(dis)) ;
for(int j = 0 ; j < m ; j ++)
{
node e = ed[j] ;
dis[e.v] = min(dis[e.v] ,last[e.u]+e.w) ;
}
}
return dis[n] ;
}
int main()
{
cin >> n >> m ;
for(int i = 0 ; i < m ; i ++)
{
int x , y , z ;
cin >> x >> y >> z ;
ed[i].u = x , ed[i]. v = y ;
ed[i].w = z ;
}
bellman_ford() ;
if(dis[n] > 0x3f3f3f3f/2) cout << "impossible\n" ;
else cout << dis[n] << endl ;
return 0 ;
}
若为有边数限制的最短路,例如从点1到点n最多经过k条边的最短路,也可以用bellman_ford算法来求。
因为bellman_ford算法第k次循环记录的是从起点走k步后每个结点的值,所以循环k次后,所求点的dis值即为最多走k条边的最短路。(算法内部都是相同的。)
//有边数限制的最短路,
int bellman_ford()
{
memset(dis , 0x3f3f3f3f , sizeof(dis)) ;
dis[1] = 0 ;
for(int i = 0 ; i < k ; i ++)
{
memcpy(last , dis , sizeof(dis)) ;
for(int j = 0 ; j < m ; j ++)
{
node e = ed[j] ;
dis[e.v] = min(dis[e.v] ,last[e.u]+e.w) ;
}
//for(int j = 1 ; j <= n ; j ++) cout << dis[j] << " " ;
//cout << endl ;
}
return dis[n] ;
}
此外判断环或是判断负权环也能用bellman_ford算法。
这里有一个栗子,
1860 -- Currency Exchange (poj.org)
题意:
n、m、s、v分别为货币数量,兑换方式的数量,拥有的货币,拥有的货币面额。求是否可以通过兑换货币来增加财富。
思路:
图为有向图。每个货币就是一个点,将现在拥有的货币看成起始点,每一个兑换方式是一条连接两种货币的路,兑换汇率为边权。
判断是否有环,且环的值为正。当有正环时,则说明可以通过兑换货币,最终换回起初的货币时,使货币的数量增加。
复杂度 ;
#include <iostream>
#include <algorithm>
#include <cstring>
#define INF 0x3f3f3f3f
using namespace std;
int n, m, s;
double v;
const int N = 1100;
double dis[N];
struct node
{
int a, b;
double r, c;
} p[N];
bool bellman_ford(int M)
{
for (int i = 1; i <= n; i++)
dis[i] = 0.0; //初始化dis数组
//dis用来记录兑换的货币面额
dis[s] = v;//初始化,起始拥有货币s的数量为v;
for (int i = 0; i < n; i++)
{
bool flag = 0;
for (int j = 0; j < M; j++)
{
node u = p[j];
if (dis[u.b] < (dis[u.a] - u.c) * u.r) //如果a兑换成b后的面额大于b之前一次的面额;
{
flag = 1;
dis[u.b] = (dis[u.a] - u.c) * u.r;
}
}
if (!flag)//如果到某个货币它不能兑换出面额增加的货币。
return false;
}
for (int i = 0; i < M; i++)
{
node u = p[i];
if (dis[u.b] < (dis[u.a] - u.c) * u.r)//如果有兑换后面额增加的货币
return true;
}
return false;
}
int main()
{
cin >> n >> m >> s >> v;
int j = 0; //兑换情况的数量
for (int i = 0; i < m; i++)
{
int a, b;
cin >> a >> b;
double rab, cab, rba, cba;
cin >> rab >> cab >> rba >> cba;
p[j].a = a, p[j].b = b;
p[j].r = rab, p[j].c = cab;
j++;
p[j].a = b, p[j].b = a;
p[j].r = rba, p[j].c = cba;
j++;
}
if (bellman_ford(j))
cout << "YES\n";
else
cout << "NO\n";
return 0;
}
spfa
spfa算法是bellman_ford的升级版本,用到优先队列来优化。
直接写模板题代码:
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool visited[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
visited[1] = true;
while (q.size())
{
int t = q.front();
q.pop();
visited[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!visited[j])
{
q.push(j);
visited[j] = true;
}
}
}
}
return dist[n];
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while (m -- )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
int t = spfa();
if (t == 0x3f3f3f3f) cout << "No Shortest Path" << endl;
else cout << t << endl ;
return 0;
}
用spfa算法判断负权环:
bool spfa() //spfa算法判断是否存在负环
{
queue<int> q;
for (int i = 1; i <= n; i ++ )
{
visited[i] = true;
q.push(i);
}
while (!q.empty())
{
int t = q.front();
q.pop();
visited[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!visited[j])
{
q.push(j);
visited[j] = true;
}
}
}
}
return false;
}
Floyd
Floyd算法用来求任意两点间的最短路。是一种暴力求解法,复杂度 。
只要最短路存在,Floyd算法适用于任何图(只要能跑动三个for循环.jpg)。
其核心思想:,也就是从i到j的最短路为从i到k的最短路加上从k到j的最短路 ,两者取最小。其实就是用了三重循环,,
void floyd()
{
for(int k = 1 ; k <= n ; k ++)
for(int i = 1 ; i <= n ; i ++)
for(int j = 1 ; j <= n ; j ++)
dis[i][j] = min(dis[i][j] , dis[i][k]+dis[k][j]) ;
} //floyd求最短路
(贼喜欢这个算法,贼好理解,贼好写,虽然它贼暴力。)
求任意两点间的最短路,板子:
#include <iostream>
#include <algorithm>
#include <cstring>
#define INF 0x3f3f3f3f
using namespace std ;
int m , n , k ;
const int N = 2e4+10 ;
int dis[N][N] ;
void floyd()
{
for(int k = 1 ; k <= n ; k ++)
for(int i = 1 ; i <= n ; i ++)
for(int j = 1 ; j <= n ; j ++)
dis[i][j] = min(dis[i][j] , dis[i][k]+dis[k][j]) ;
}
int main()
{
cin >> n >> m >> k ;
for(int i = 1 ; i <= n ; i ++)
for(int j = 1 ; j <= n ; j ++)
{
if(i == j) dis[i][j] = 0 ;
else dis[i][j] = INF ;
}
for(int i = 0 ; i < m ; i ++)
{
int x , y , z ;
cin >> x >> y >> z ;
dis[x][y] = min(z , dis[x][y]) ;
}
floyd() ;
while(k--)
{
int l , r ;
cin >> l >> r ;
if(dis[l][r] > INF/2) cout << "No Shortest Path\n" ;
else cout << dis[l][r] << endl ;
}
return 0 ;
}
在郑州经历了一场百年一遇的大雨之后,一周没有学习算法的我,来电后的第一天就拿出键盘把我的最短路火速总结了一下。求求灾情快过去,一切恢复正常运转 ~~~
依然要好好学习啊( ̄︶ ̄)~~~