存在最短路的前提
不存在负环。
链接
还是 OIWiki 好啊~~
Floyd 算法
两两间最短路,可判负环。时间复杂度 \(O(n^3)\)。
像 DP 的思路一样。
设 \(f_{k,x,y}\) 表示允许经过 \(1\sim k\) 的点,\(x\to y\) 的最短距离。
\(O(n^3)\) 的 DP 即可。
按照 \(k,x,y\) 的顺序循环,每次与 \((x,k),(k,y)\) 取 \(\min\) 即可。
即
f[k][x][y] = min(f[k - 1][x][y], f[k - 1][x][k] + f[k - 1][k][y]);
因为第一维对结果无影响,我们可以发现数组的第一维是可以省略的,于是可以直接改成 f[x][y] = min(f[x][y], f[x][k]+f[k][y])
。
搬 OIWiki 的代码:
for (k = 1; k <= n; k++) {
for (x = 1; x <= n; x++) {
for (y = 1; y <= n; y++) {
f[k][x][y] = min(f[k - 1][x][y], f[k - 1][x][k] + f[k - 1][k][y]);
}
}
}
综上时间复杂度就是 \(O(n^3)\),空间复杂度就是 \(O(n^2)\)。
Floyd 求图的传递闭包
用 bitset 优化可以实现 \(O(\frac{n^3}{w})\) 的复杂度。
Floyd 判负环
\(\exists x,f_{x,x}<0 \Rightarrow \text{存在负环}\)
Bellman-Ford 算法
单源最短路,可判负环。时间复杂度 \(O(nm)\)。
共循环 \(O(n)\) 次,每次枚举每一条边进行松弛操作(即对边 \((u,v)\) 对 \(dis_v\) 进行取 \(\min\) 操作)。若在某一次没有对任意一条边进行松弛操作,可以直接结束循环。
证明只用循环 \(n-1\) 次即可求出最短路。
因为第 \(i\) 次操作可以确定最短路经过 \(i\) 条边的结点的 \(dis\),因此最坏情况下只需要重复 \(n-1\) 次即可。
证毕。
然鹅我们一般循环 \(n\) 次,为的是顺便判一下有没有负环(最短路是否存在)。
判负环
若第 \(n\) 次仍然可以进行松弛操作,则存在负环。
Code (显然是从 OIWiki 搬的)
struct Edge {
int u, v, w;
};
vector<Edge> edge;
int dis[MAXN], u, v, w;
const int INF = 0x3f3f3f3f;
bool bellmanford(int n, int s) {
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
bool flag = false; // 判断一轮循环过程中是否发生松弛操作
for (int i = 1; i <= n; i++) {
flag = false;
for (int j = 0; j < edge.size(); j++) {
u = edge[j].u, v = edge[j].v, w = edge[j].w;
if (dis[u] == INF) continue;
// 无穷大与常数加减仍然为无穷大
// 因此最短路长度为 INF 的点引出的边不可能发生松弛操作
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
flag = true;
}
}
// 没有可以松弛的边时就停止算法
if (!flag) {
break;
}
}
// 第 n 轮循环仍然可以松弛时说明 s 点可以抵达一个负环
return flag;
}
SPFA 算法
最坏时间复杂度仍为 \(O(nm)\)。
很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。
那么我们用队列来维护「哪些结点可能会引起松弛操作」,就能只访问必要的边了。
SPFA 也可以用于判断 \(s\) 点是否能抵达一个负环,只需记录最短路经过了多少条边,当经过了至少(\(\geq\)) \(n\) 条边时,说明 \(s\) 点可以抵达一个负环。
struct edge {
int v, w;
};
vector<edge> e[maxn];
int dis[maxn], cnt[maxn], vis[maxn];
queue<int> q;
bool spfa(int n, int s) {
memset(dis, 63, sizeof(dis));
dis[s] = 0, vis[s] = 1;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop(), vis[u] = 0;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
cnt[v] = cnt[u] + 1; // 记录最短路经过的边数
if (cnt[v] >= n) return false;
// 在不经过负环的情况下,最短路至多经过 n - 1 条边
// 因此如果经过了多于 n 条边,一定说明经过了负环
if (!vis[v]) q.push(v), vis[v] = 1;
}
}
}
return true;
}
Dijkstra 算法
单源最短路。时间复杂度 \(O(n^2)\) 或 \(O(m \log m)\)。必须为非负权图。
搬 OIWiki 甚至懒得打字
简单来说,就是每次选一条集合内目前最短的路径,然后扩展一圈,放回集合。
时间复杂度 \(O(n^2)\) 的。
Code(搬的)
struct edge {
int v, w;
};
vector<edge> e[maxn];
int dis[maxn], vis[maxn];
void dijkstra(int n, int s) {
memset(dis, 63, sizeof(dis));
dis[s] = 0;
for (int i = 1; i <= n; i++) {
int u = 0, mind = 0x3f3f3f3f;
for (int j = 1; j <= n; j++)
if (!vis[j] && dis[j] < mind) u = j, mind = dis[j];
vis[u] = true;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if (dis[v] > dis[u] + w) dis[v] = dis[u] + w;
}
}
}
优先队列优化
时间复杂度 \(O(m\log m)\)。
复杂度易证,略。
终于有我自己写的代码了
#include<bits/stdc++.h>
#define ll long long
#define pf printf
#define sf scanf
using namespace std;
const int N=1e5+7;
int cnt,head[N];
struct edge{
int to,ne,val;
} e[N<<1];
void add(int u,int v,int w) { e[++cnt]={v,head[u],w},head[u]=cnt; }
int n,m,s,u,v;
int w;
struct node{
int u,dis;
bool operator>(const node &x)const{
return dis>x.dis;
}
};
priority_queue<node,vector<node>,greater<node> > q;
int dis[N];
bool vi[N];
void getans(){
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
q.push({s,0});
while(!q.empty()){
int u=q.top().u,d=q.top().dis;
q.pop();
if(vi[u]) continue;
vi[u]=1;
for(int i=head[u];i;i=e[i].ne){
int v=e[i].to,w=e[i].val;
if(d+w<dis[v]){
dis[v]=d+w;
q.push({v,dis[v]});
}
}
}
}
int main(){
sf("%d%d%d",&n,&m,&s);
for(int i=1;i<=m;i++){
sf("%d%d%d",&u,&v,&w);
add(u,v,w);
}
getans();
for(int i=1;i<=n;i++){
pf("%d ",dis[i]);
}
}
Johnson 全源最短路径算法
……到时候补充。
差分约束系统
其实这才是我今天的正题额。
若对这样一组不等式,我们要求 \(max(x_i-x_j)\),那么:
对于 \(x_v-x_u\leq b_k\) 我们建一条有向边 \((u,v,b_k)\),在图上跑单源最短路 SPFA 算法(当然这个例子没有负权,也可以跑 Dijkstra 算法),\(ans=dis_i-dis_j\)。
为什么呢?
可以发现,在求最短路的过程中,对于边 \((u,v,w)\) 总有 \(dis_v\leq dis_u+w\),我们把它叫做三角不等式。即:
\(dis_v-dis_y\leq w\)
这个式子和
\(x_j-x_i\leq b_k\)
是不是很像呢?
因此差分约束系统可以转换成有向图求解。
因为我们的有向图不一定连通,为了方便,我们一般设一个超级源点 \(s\),令 \(dis_s=D\),连边 \(s\to x,1\leq x \leq n\),边权为 \(0\),即增加关系 \(x-s\leq 0,1\le x \le n\)。这样,我们就可以求出一组解 \(\{ x_1,x_2,\dots,x_n\} \le D\)(毕竟 \(s\) 与所有点都有权为 \(0\) 的边,所以 \(dis_x\) 最大也是 \(D\))。
当然,如果差分约束系统里的所有 \(\le\) 换成 \(\geq\) 也可以做,只是把求最短路变成求最长路罢了。
SPFA 最短路 Code
#include<bits/stdc++.h>
#define ll long long
#define pf printf
#define sf scanf
using namespace std;
const int N=5e3+7;
int n,m,u,v,w;
int cnt,head[N];
struct edge{
int to,ne,val;
}e[N<<1];
int s;
void add(int u,int v,int w){
e[++cnt]={v,head[u],w};
head[u]=cnt;
}
int dis[N];
bool vi[N];
int tot[N];
bool SPFA(){
queue<int> q;
q.push(s);vi[s]=1;
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
while(!q.empty()){
int u=q.front();
q.pop();vi[u]=0;
for(int i=head[u];i;i=e[i].ne){
int v=e[i].to,w=e[i].val;
if(dis[u]+w<dis[v]){
dis[v]=dis[u]+w;
tot[v]=tot[u]+1;
if(tot[v]>n){//注意因为加入了超级源点,因此一共有 n+1 个点,最短路包含恰好 n 条边是可以的。
return 0;
}
if(!vi[v]) q.push(v);
vi[v]=1;
}
}
}
return 1;
}
int main(){
sf("%d%d",&n,&m);
s=n+1;
for(int i=1;i<=n;i++) add(s,i,0);
for(int i=1;i<=m;i++){
sf("%d%d%d",&v,&u,&w);
add(u,v,w);
}
if(SPFA())
for(int i=1;i<=n;i++){
pf("%d ",dis[i]);
}
else pf("NO\n");
}