写图论的时候其实还是很怕写最短路,自己会的算法也停留在最早时候教的原始dij算法,一般比赛里dij算法过于简单见不到原题,也很久没有复习过,现在基本忘完了。趁讲课之前赶紧补救一下。
最短路定义:
最短路问题(short-path problem)是网络理论解决的典型问题之一,可用来解决管路铺设、线路安装、厂区布局和设备更新等实际问题。基本内容是:若网络中的每条边都有一个数值(长度、成本、时间等),则找出两节点(通常是源节点和阱节点)之间总权和最小的路径就是最短路问题。
每次只能求出从某一个点出发,求出到其他所有点的最短路的算法。
点的路程代表当前点到起点的路程。
核心思想:起点到每一个点的最短路程都可以由与它相邻且确定最短路程的点连接而来,因此,只要一个点周围的点路程都确定了,那么当前点的路程就是周围(点的路程+加上边长)的最小值。反过来说,只要确定了一个点的最短路程,就可以更新与它相邻的点的路程。此后依次取路程最短的点当作新一个确定最短路程的点进行下一轮更新即可。
所以dij的算法如下:
1. 初始化起点的最短路长度为0;
2. 找当前距离起点最近的点塞进集合;
3. 更新与之相邻的点到起点的长度(放缩);//算法核心
4. 回到步骤2,直到确定所有点的最短路;
为了实现这个算法,我们需要:
1.int d[maxn] 记录所有点的最短路长度
2.bool vis[maxn] 记录当前点是否已经直到最短路长度
3.int cost[maxn][maxn] 记录给出的所有边的长度//cost[1][2]表示从点1到点2的路多长
有问题吗? 不说话就当没有啊,算法搞起。
void dij(int begin){
int d[maxn];
int vis[maxn];//也可以用int取代bool
for(int i = 0 ; i <= n ; i++){
d[i] = inf;//所有点初始都是不知道最短路径的,标成无穷大即可。
vis[i] = 0;
}
d[begin] = 0;//1.初始化起点的最短路长度为0;
while(1){
int v = -1;
for(int i = 1 ; i <= n ; i++){
if(!vis[i] && (v == -1 || d[i] < d[v]))v = i;// 2. 找当前距离起点最近的点塞进集合;
}
if(v == -1)break;// 4. 直到确定所有点的最短路;
vis[v] = 1;
for(int i = 1 ; i <= n ; i++){
d[i] = min(d[i] , d[v] + cost[v][i]);// 3. 更新与之相邻的点到起点的长度(放缩);//算法核心
}
}
}
运行完后d[i]就是起点到i点的最短路径了。
程序最多运行n个循环确定n个点,每次循环需要枚举n个点,
所以时间复杂度O(n^2)
因为用邻接矩阵来存图所以空间复杂度也是O(n^2)
只要 n > 1e4 就根本装不下。装下了也会T
这个时候就需要用到
用vector可以轻松解决存图的问题。
用了vector之后,放缩每一个点不再需要用
for(int i = 1 ; i <= n ; i++)d[i] = min(d[i] , d[v] + cost[v][i]);
枚举n个点,而是
for(int i = 0 ; i < vec[now].size() ; i++)放缩过程;
复杂度由O(n)降为与当前点连接的边数O(E)。
每次选出最新的最短路径可以用优先队列优化
复杂度由O(n*n)降到所有点数O(nlogn)。
最坏情况把每条边都更新两次。
所以堆优化后的时间复杂度最差为O(2m + nlogn)。
正常情况O(m logn)
例题
最短路
给你n个点m条双向边,问从点1到点n的最短距离。
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 1e5 + 5;
const int inf = 0x3f3f3f3f;
int d[maxn];
struct NODE{
int id, dis;
friend bool operator < (NODE a,NODE b){
return a.dis > b.dis;
}
};
vector<NODE>vec[maxn];
priority_queue<NODE>que;
int n, m;
int main(){
while(~scanf("%d%d",&n,&m)){
if(n == 0 && m == 0)break;
for(int i = 1 ; i <= n ; i++)vec[i].clear();
memset(d, 0x3f, sizeof d);
while(!que.empty())que.pop();
int u,v,c;
for(int i = 1 ; i <= m ; i++){
scanf("%d%d%d",&u,&v,&c);
vec[u].push_back((NODE){v,c});
vec[v].push_back((NODE){u,c});
}//建图
d[1] = 0;
que.push((NODE){1,0});
NODE cnt;int now;
while(!que.empty()){
cnt = que.top(); que.pop();
now = cnt.id;
if(d[now] < cnt.dis)continue;
int len = vec[now].size();
for(int i = 0 ; i < len ; i++){
int to = vec[now][i].id;
if(d[now] + vec[now][i].dis < d[to]){
d[to] = d[now] + vec[now][i].dis;
que.push((NODE){to,d[to]});
}
}
}
printf("%d\n",d[n]);
}
}
只要记住这个就可以忘记前面没有优化的dij了!
这个算法一定可以秒杀所有单源最短路了吧!
你信了,于是
你问学长,“上课是不是又少加括号了?”
学长笑着跟你说,这道题里面,有负环。
啥是负环?
“ 当图里存在负边权的时候,能从中找出一个首尾相连的路径,使得总路程小于零” —— X_X_X
“说的就是要你用ford算法” —— 普通OI爷
“就是说spfa” —— 高中OI爷
-------------↑一个可爱的负环↑-----------------
so TM what?
睿智的人会不停的循环那个总路程小于零的环,使得花费变得无限小。
而dij算法此时像个不会走圈圈的憨憨。
怎么解决?
dij不会走环的关键在于默认每个点只需要更新一次。
但是因为负环的存在,这个假设是错误的。
只需要把vis数组删掉,然后一直放缩就行了。
由于在有负环的情况下可以无限放缩(一直转圈),而没有负环时一个图最多放缩n - 1次
所以当放缩到第n轮的时候就可以判定这个图包含负环,没有必要继续跑最短路了。
所以又有新板子了?
这些算法都是放缩嘛,自己改吧。
每个点最多更新N - 1次 O(N)
每次更新一轮枚举所有边O(M)
预期复杂度应该是
总共O(N*M)
好在它是没有优化的,直接记它大哥吧
去年学长pass掉了spfa说他不行
今年学完网络流,点名spfa说要讲
被迫学习bellman的加强版
bellman的复杂度主要浪费在枚举无效的边上。
于是记录哪些边成了首要任务。
如果一个点的最短路更新了,可以想到与它相连的边都可能会更新周围点的最短路,所以需要取出判断。
于是spfa又变回了堆优化dij一般。
区别在于
spfa可以更新一个点多次,优先队列里装的是需要被再次更新的点。
具体实现参照
其他大佬的图解
咱只学过一个闭着眼睛都会敲,但是基本上不会有裸题。
因为**复杂度O(n^3)**数据n>500就瑟瑟发抖等着T掉的
核心思想:任意两个点之间的最短距离,可以用枚举所有的点来确定。
于是我们只需要按一定的顺序枚举,就写出完成了弗洛伊德算法
int G[maxn][maxn];//存图
int n;//顶点数
...//省略建图
for(int k = 1 ; i <= n ; k++){
for(int i = 1 ; i <= n ; i++){
for(int j = 1 ; j <= n ; j++){
G[i][j] = min(G[i][j] , G[i][k] + G[k][j]);
}
}
}
为什么是对的?
证明如下:
结束。