关于最短路,就学了几种基础算法,分别是:最简单的bfs求路径、dijk、bellman、floyed、以及基于bellman的SPFA。
其实单纯最短路算法在比赛中几乎很少用到(比如像acm这样的正式比赛,它们在我看来只是提供算法基本思路的形象例子之一)
bfs最为简单不说了。
dijk基于贪心思想,解决的是单源最短路问题。其实现步骤大致如下:找出一个距离起点最近的未用过的点(这个距离本身就可以认为是起点到这个点最短的距离了),然后通过这个点不断更新这个点能够达到的点。所有点都用过之后,d[s][t]就是我们要的答案了。
凭什么这样做能得出最优解?
证明过程如下:(其实不是很严谨凑合看吧)
假设S为已经确定最短距离的点集,k为点集之外的一点,我们假设用点集S之外的a点更新k点,那么a点的距离一定小于k点,那么当a点加入点集S时,还是会更新k点。也就是说,对于k点的入度都会先一步加入点集,然后可以更新k点,这样我们就取得了k点的最短距离。
正常人都会给dijk加个堆优化一下,n2的代码不贴了,就贴个nlogn的
#include<bits/stdc++.h>
#define inf 1e9
using namespace std;
struct node
{
int to,val;
};
struct nod
{
int d,dian;
friend bool operator < (const nod x,const nod y)
{
return x.d>y.d;
}
};
int n,m,s;
int d[200005];
vector<node> a[200005];//这是存的图
void dijk()
{
fill(d,d+200005,inf);
d[s]=0;
int use[200005]={0};
priority_queue<nod> p;
nod t={0,s};
p.push(t);
while(p.size())
{
nod t=p.top();
p.pop();
if(use[t.dian] || d[t.dian]<t.d) continue;
use[t.dian]=1;
int x=t.dian;
int k=a[x].size();
for(int i=0;i<k;i++)
{
if(d[a[x][i].to]>d[x]+a[x][i].val)
{
d[a[x][i].to]=d[x]+a[x][i].val;
nod tt={d[a[x][i].to],a[x][i].to};
p.push(tt);
}
}
}
}
int main()
{
cin>>n>>m;
cin>>s;
for(int i=1;i<=m;i++)
{
int x,y,z;
cin>>x>>y>>z;
//假设是有向图
node t={y,z};
a[x].push_back(t);
}
dijk();
for(int i=1;i<=n;i++)
if(i!=n) cout<<d[i]<<" ";
else cout<<d[i];
}
差分约束系统
三角形不等式角形还有一个应用,就是求解差分约束系统(差分约束系统就是一堆一次不等式的组合),利用三角不等式的d[v]<=d[u]+w[u][v],刚好满足约束条件,
最终转化为最短路问题,由于一般都带有负边,还要讨论负环有无解的问题,一般都会用SPFA来做。
那怎么确保每一个约束条件都有贡献?(也就是走过每一条边)
reason:当所有边都为正时,即使d数组全为0也一样满足条件;
当存在负边时,根据最短路的原则一定会利用负边进行松弛,这样这条边一定会被用上,所以约束条件会得到满足,不单单只是为了连通性(这一点很多博客都没有提到)
bellman 其实就是暴力求解,考虑了每一个点的前导边是否能优化最短路,还有就是由于最短路上的点有V个,那么路径边一定是V-1条所以我们执行
n-1次的“大松弛”就一定能得到最优解。我们可以在再也无法松弛的情况下提前退出循环。代码过于简单就不贴了,下面会贴一下优化后的bellman(即SPFA)
SPFA的思想是更新某个点他的前导点必定要松弛过,我们只把松弛成功的节点加入到队列中(在队列中不能重复存在),注意如果一个点入队超过V-1次的
话就会存在负圈,按照所说的均摊复杂度是O(kn)其中k是一个相对较小的常数。另外,实践证明,当跑的图是一个菊花图(稠密图)时k会变得很大
这时候复杂度会大幅上升,总而言之起不到优化效果了,这是血淋淋的教训!!!
贴一波SPFA代码。
#include<bits/stdc++.h>
#define inf 1e9
using namespace std;
struct node
{
int to,val;
};
int n,m,s;
vector<node> a[200005];
long long d[200005];
void spfa()
{
fill(d,d+200005,inf);
queue<int> p;
d[s]=0;
p.push(s);
int use[200005]={0};
use[s]=1;
while(p.size())
{
int dian=p.front();
p.pop();
use[dian]=0;
int t=a[dian].size();
for(int i=0;i<t;i++)
{
if(d[dian]+a[dian][i].val<d[a[dian][i].to])
{
d[a[dian][i].to]=d[dian]+a[dian][i].val;
if(use[a[dian][i].to]==0)
{
use[a[dian][i].to]=1;
p.push(a[dian][i].to);
}
}
}
}
}
int main()
{
freopen("in1.txt","r",stdin);
std::ios::sync_with_stdio(false);
cin>>n>>m>>s;
for(int i=1;i<=m;i++)
{
int x,y,z;
cin>>x>>y>>z;
node t={y,z};
a[x].push_back(t);
}
spfa();
for(int i=1;i<=n;i++)
if(i!=n) cout<<d[i]<<" ";
else cout<<d[i];
}
我这个可以说是朴素版的spfa,太辣鸡了,洛谷p4779最短路标准版数据都能卡到32分,天哪!!!spfa成废物了吗???
关于负边和负环的讨论:
如果存在负边的话是可以得出最优解的,但是dijkstra不行(结合证明过程可知),bellman可以用这时候但是很慢,求带负边单源的最好用spfa,
那负边全源呢?全源可以用Floyd,但是太慢了,Johnson可以用。
这个算法的复杂度可以达到o(nmlogm),比起floyd的n^3好上不少。
而对于负环,理论上就不存在最优解别说设计算法了。
Johnson的思路就是弄一张新图把负边在不影响求解的情况下换成正边。
在讨论这个问题之前,我们先讨论一个物理概念——势能。
诸如重力势能,电势能这样的势能都有一个特点,势能的变化量只和起点和终点的相对位置有关,而与起点到终点所走的路径无关。
势能还有一个特点,势能的绝对值往往取决于设置的零势能点,但无论将零势能点设置在哪里,两点间势能的差值是一定的。
(这里有一个错误思路,就是对于所有边先加上x是所有边的权值都为正,然而这显然是错误的)
证明过程:
这里的h[k]就是建立的超级源点到各点的值,加入各边的权值均为正,那么h数组的贡献为零
操作步骤是:建立一个新点0号点,然后跑一边spfa求好h[i],然后改边的权值,用新图跑n遍dijkstra。(记得吧最后的结果要减去hs-ht!!!)
水平一般,本蒟蒻欢迎各位大佬神仙来dis
持续更新中。。。