最短路算法

最短路算法

存在最短路的前提

不存在负环。

链接

还是 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)\)

复杂度易证,略。

终于有我自己写的代码了

P4779 【模板】单源最短路径(标准版)

#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 全源最短路径算法

……到时候补充。

差分约束系统

其实这才是我今天的正题额。

OIWiki参考博客另一篇博客

若对这样一组不等式,我们要求 \(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

P5960 【模板】差分约束

#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");
}

同余最短路

简介

来自 OI Wiki,讲得好,就搬上来了。

给定 \(n\) 个整数,每个数字可以使用多次,判定能拼出哪些数字。

其中给定数字的值域 \(V\) 使得时间复杂度主要是 \(O(nV)\) 的。

如果我们只有一个数字 \(a\),那么我们可以拼出 \(0a,1a,2a,\cdots, na\)

如果再来一个 \(b\),我们就可以拼出 \(pa+qb\)

我们将 \(a\) 视为同余系的模数,认为只要有 \(qb \equiv x(\bmod a)\) 我们就可以拼出 \(x+pa\) 了。因此我们需要求出只使用 \(b\),在模 \(a\) 意义下可以到达那些 \(x\)

对于每个 \(x(\bmod a)\),我们连一条 \(x \to (x+b) \bmod a\) 的边。边权为 \(b\)。对于除了 \(a\)\(n-1\) 个数皆是如此。

这样我们从 \(x=0 (\bmod a)\) 开始跑最短路。\(dis_x\) 就表示我可以走到的最小的 \(\bmod a = x\) 的数字是什么,此后的 \(dis_x + pa\) 都是可以走到的。

经验

模板题:P3403 跳楼机

\(h\) 减一,当做从第零层开始会更加方便一些。

\(x\) 为模数,求该同余系意义下的同余最短路。时间复杂度 \(O(x \log x)\)

P2371 [国家集训队] 墨墨的等式

同样很板子。有 \(n \le 12\) 个数字,时间复杂度 \(O(nV \log (nV))\)

posted @ 2024-08-13 21:22  wing_heart  阅读(19)  评论(0)    收藏  举报