首页 > 其他分享 >图论——最短路

图论——最短路

时间:2023-12-23 19:56:43浏览次数:26  
标签:图论 log int 短路 long times dis

https://csacademy.com/app/graph_editor/

https://riverhamster.gitee.io/app/graph_editor/

注:时间复杂度分析中,假设 \(n \le m \le n ^ 2\)


最短路本质上是一种 DP

阶段:点

状态:拆点

决策:边

最优子结构:最短路的任何子路径都一定是最短路

无后效性:正权图中一定可以找到一种无后效性的枚举顺序(Dijkstra)

重复子问题:\(dis_i\) 表示所有以 \(i\) 为结尾的所有路径的长度的最小值


存图

本来不打算写的,但是发现 vector + O2 跑得比链前快之后真的绷不住了

%%%chx

主要原因是 vector 比链前慢的地方是在建图是需要动态分配内存,但是存完图后每个点连出的边就储存在一段连续的内存中,利用 cache 机制大量访问会比较快

但是链前真的很酷。


Dijkstra

  1. 朴素版

本质上是通过贪心的方式找到一种使得 DP 没有后效性的转移顺序

将所有点分为 \(S\) 和 \(T\) 两个集合,\(S\) 表示最短路确定且不会再更改,\(T\) 表示最短路未确定,最开始所有点都在 \(S\) 中。每次从 \(T\) 找出最短路最小的点,用它更新其他点的最短路,并放进 \(S\) 集合

因为所有边的边权都是正数,所以每次找出的最小的点肯定不会被其他 \(T\) 集合中的点再更新最短路

也就是说,每个点一定是以最短路长度从小到大的顺序被放入 \(S\) 集合的,前面一定不会被后面影响。这也是一个 DAG 的拓扑序

  1. 堆优化 / 线段树优化

每次找出 \(T\) 集合中最短路最小的点可以用堆优化,STL 优先队列 \(O(m \times \log m)\),手写二叉堆 \(O(m \times \log n)\),斐波那契堆 \(O(n \times \log n + m)\)

不会手写堆可以线段树,也是 \(O(m \times \log n)\)。但线段树实际上一般比 STL 更慢(NKOJ 不加快读甚至会 TLE),因为一定会把 \(O(m \times \log n)\) 跑满,但是一般不会有毒瘤出题人把优先队列的 \(\log m\) 卡满 而且 log m 和 log n 本来就差不了多少,线段树常数还大。这里给出线段树的参考代码,但是在 NKOJ(和其他任何 OJ)上不建议使用。

#include <cstdio>
#include <algorithm>

#include <cctype>
namespace fastio
{
	const int MAXBUF = 1 << 20;
	char buf[MAXBUF], *p1 = buf, *p2 = buf;
	inline char getc() { return (p1 == p2) && (p2 = (p1 = buf) + fread(buf, 1, MAXBUF, stdin)), p1 == p2 ? EOF : *p1++; }
} // namespace fastio
using fastio::getc;
template <class _Tp>
inline _Tp& read(_Tp& x)
{
	bool sign = 0;
	char ch = getc();
	for (; !isdigit(ch); ch = getc()) sign |= (ch == '-');
	for (x = 0; isdigit(ch); ch = getc()) x = x * 10 + (ch ^ 48);
	return sign ? (x = -x) : x;
}

const int maxn = 400000 + 10, maxm = 2000000 + 10;
struct graph
{
	int cnt;
	int st[maxm], to[maxm], last[maxn], next[maxm];
	long long w[maxm];
	graph() { cnt = 0; }
	void add(int x, int y, long long z)
	{
		cnt++;
		st[cnt] = x, to[cnt] = y, w[cnt] = z;
		next[cnt] = last[x], last[x] = cnt;
	}
}
g;

struct segmentTree
{
	long long a[maxn];
	struct node { int l, r, pos; } T[maxn << 2];
	void build(int p, int l, int r)
	{
		T[p].l = l, T[p].r = r, T[p].pos = l;
		if (l == r) a[l] = 1LL << 60;
		else build(p << 1, l, (l + r) >> 1), build((p << 1) | 1, ((l + r) >> 1) + 1, r);
	}
	int modify(int p, int k, long long d)
	{
		if (T[p].r < k || T[p].l > k) return T[p].pos;
		else if (T[p].l == k && T[p].r == k) return a[k] = d, T[p].pos = k;
		else return T[p].pos = a[modify(p << 1, k, d)] <= a[modify((p << 1) | 1, k, d)] ? T[p << 1].pos : T[(p << 1) | 1].pos;
	}
}
T;

long long dis[maxn];

long long dijk(int st, int ed, int n)
{
	T.build(1, 1, n);
	T.modify(1, st, 0);
	for (int i = 1; i <= n; i++) dis[i] = 1LL << 60;
	dis[st] = 0;
	while (n--)
	{
		int u = T.T[1].pos;
		if (T.a[u] >= 1LL << 60) break;
		if (u == ed) return dis[u];
		for (int j = g.last[u]; j != 0; j = g.next[j])
		{
			int v = g.to[j]; long long w = g.w[j];
			if (dis[v] > dis[u] + w) T.modify(1, v, T.a[u] + w), dis[v] = dis[u] + w;
		}
		T.modify(1, u, 1LL << 60);
	}
	return -1;
}


int main()
{
	int n, m;
	read(n), read(m);
	for (int i = 1; i <= m; i++)
	{
		int x, y;
		long long z;
		read(x), read(y), read(z);
		g.add(x, y, z);
	}
	int st, ed;
	read(st), read(ed);
	printf("%lld", dijk(st, ed, n));
	return 0;
}

Bellman-Ford

  1. 朴素版

进行 \(n - 1\) 次遍历每个边的松弛操作

因为在无负权回路图中最短路不会经过重复点,长度最多为 \(n - 1\),所以在第 \(i\) 次松弛操作中一定能松弛到最终答案的第 \(i\) 条边

时间复杂度 \(O(n \times m)\)(也可以是 \(O(\text{边数最长的最短路长度} \times m)\))

  1. 队列优化(SPFA)

显然只有最短路变化的点才可能更新其他点,所以每次可以把变化的点存下来,再用这些点去更新其他点。时间复杂度为边数乘每个点的平均入队次数 \(O(k \times m)\),随机图中 \(k < 5\),构造图可以卡到 \(O(n \times m)\)。有一些神奇的优化,但是肯定可以被卡。在一些特殊图中还是有一定的作用,但不多

  1. 判负权回路

如果进行 \(n - 1\) 次松弛操作后仍然可以松弛,那么图中存在负环。SPFA 中可以记录每个点的入队次数,超过 \(n\) 次说明存在负环。但是只能判可以从起点到达的负环

Floyd-Warshall

  1. 朴素版

设 \(f_{k,i,j}\) 表示 \(i\) 到 \(j\) 只经过前 \(k\) 个点的最短路

则 \(f_{k,i,j} = \min(f_{k-1,i,j}, f_{k-1,i,k} + f_{k-1,k,j} )\)

  1. 滚动数组优化

滚动第一维,得到 \(f_{i,j} = \min(f_{i,k} + f_{k,j} )\)

因为 \(k\) 并不在 \(i\) 到 \(k\) 的最短路中,所以 \(f_{i,k}\) 在此时表示 \(f_{k-1,i,k}\) 还是 \(f_{k,i,j}\) 都不会有影响,可以直接压缩。\(f_{k,j}\) 同理

循环顺序保持不变,还是 \(k,i,j\)

其实三个循环顺序可以随意交换,在外面再套一个 \(3\) 次的循环即可 没用的知识又增加了

  1. 判负权回路

如果 \(f_{i,i} < 0\),说明点 \(i\) 在一个负权回路中

  1. 传递闭包

设 \(f_{i,j} = 1\) 表示 \(i\) 与 \(j\) 连通,反之不连通,然后使用 Floyd 的三层循环进行求解

可以用 bitset 优化,时间复杂度 \(O(\frac{n ^ 3}{w})\)

BFS

你就说能不能求最短路吧

边权为 \(1\) 用队列,边权为 \(0\) 或 \(1\) 用双端队列,如果经过一条边权为 \(0\) 的边更新一个点放到队头,边权为 \(1\) 放到队尾,第一次取出一个点时它的最短路就一定是已经确定的。时间复杂度 \(O(m)\)

对于边权无限制,有两种解决办法。一是允许节点被多次更新 然后就变成 SPFA 了呢,二是改成优先队列 然后就变成 Dijkstra 了呢


最小环

https://oi-wiki.org/graph/min-cycle/

https://www.luogu.com.cn/problem/P6175

第一种方法是枚举图中每一条边,再用 Dijkstra 计算这条边的两个端点在不经过这条边本身的最短路,最终答案即为 \(dis_{v,u} + w_{u,v}\)。时间复杂度为 \(O(m ^ 2 \times \log n)\)

第二种方法需要用到 Floyd 中的性质。因为 \(f_{k,i,j}\) 表示 \(i\) 到 \(j\) 只经过前 \(k\) 个点的最短路,

连通性

并查集。

最短路相关计数

[HAOI2012] 道路

如果对每一条边都枚举最短路可能经过它的起点和终点,时间复杂度太高。考虑对一个点求出单源最短路后枚举对每一条边答案的贡献(控制变量法——前物理科代表 \(a^6q^6\))

因为最短路中不会出现环,且最短路的前缀一定是最短路,所以所有可以被作为最短路上的边(\(dis_u + w = dis_v\))一定构成了一个有向无环图,这个 DAG 上的任何一条路径都是最短路,可以在 Dijkstra 过程中求出拓扑序,然后按拓扑序进行计数

#include <cstdio>
#include <algorithm>

const int maxn = 1500 + 10, maxm = 5000 + 10;
const long long inf = 1LL << 60;
struct graph
{
	int cnt;
	int st[maxm], to[maxm], last[maxn], next[maxm];
	long long w[maxm];
	graph() { cnt = 0; }
	void add(int x, int y, long long z)
	{
		cnt++;
		st[cnt] = x, to[cnt] = y, w[cnt] = z;
		next[cnt] = last[x], last[x] = cnt;
	}
}
g, gt;

struct segmentTree
{
	long long a[maxn];
	struct node { int l, r, pos; } T[maxn << 2];
	void build(int p, int l, int r)
	{
		T[p].l = l, T[p].r = r, T[p].pos = l;
		if (l == r) a[l] = inf;
		else build(p << 1, l, (l + r) >> 1), build((p << 1) | 1, ((l + r) >> 1) + 1, r);
	}
	int modify(int p, int k, long long d)
	{
		if (T[p].r < k || T[p].l > k) return T[p].pos;
		else if (T[p].l == k && T[p].r == k) return a[k] = d, T[p].pos = k;
		else return T[p].pos = a[modify(p << 1, k, d)] <= a[modify((p << 1) | 1, k, d)] ? T[p << 1].pos : T[(p << 1) | 1].pos;
	}
}
T;


long long dis[maxn];
int cnt = 0;
int ord[maxn];

void dijk(int st, int n)
{
	cnt = 0;
	T.build(1, 1, n);
	T.modify(1, st, 0);
	for (int i = 1; i <= n; i++) dis[i] = inf;
	dis[st] = 0;
	while (n--)
	{
		int u = T.T[1].pos;
		if (T.a[u] >= inf) break;
		ord[++cnt] = u;
		for (int j = g.last[u]; j != 0; j = g.next[j])
		{
			int v = g.to[j]; long long w = g.w[j];
			if (dis[v] > dis[u] + w) T.modify(1, v, T.a[u] + w), dis[v] = dis[u] + w;
		}
		T.modify(1, u, inf);
	}
	return;
}

const long long mod = 1e9 + 7;
long long ans[maxm];
long long in[maxn], out[maxn];

int main()
{
	int n, m;
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= m; i++)
	{
		int x, y;
		long long z;
		scanf("%d %d %lld", &x, &y, &z);
		g.add(x, y, z);
		gt.add(y, x, z);
	}
	for (int i = 1; i <= n; i++)
	{
		dijk(i, n);
		for (int j = 1; j <= n; j++) in[j] = out[j] = 0;
		in[i] = 1;
		for (int k = 1; k <= cnt; k++)
		{
			int u = ord[k];
			for (int j = gt.last[u]; j != 0; j = gt.next[j])
			{
				int v = gt.to[j];
				long long w = gt.w[j];
				if (dis[v] + w == dis[u]) in[u] = (in[v] + in[u]) % mod;
			}
		}
		for (int k = cnt; k >= 1; k--)
		{
			int u = ord[k];
			out[u] = 1;
			for (int j = g.last[u]; j != 0; j = g.next[j])
			{
				int v = g.to[j];
				long long w = g.w[j];
				if (dis[u] + w == dis[v]) out[u] = (out[u] + out[v]) % mod;
			}
		}
		for (int j = 1; j <= m; j++)
		{
			int u = g.st[j], v = g.to[j];
			if (dis[u] + g.w[j] == dis[v]) ans[j] = (ans[j] + in[u] * out[v]) % mod;
		}
	}
	
	for (int i = 1; i <= m; i++) printf("%lld\n", ans[i]);
	return 0;
}

虚点

https://www.cnblogs.com/SillyTieT/p/11545966.html

分层图最短路 / 多维最短路 / 拆点

其实就是 DP

线段树优化建图

咕咕咕


P8817 [CSP-S 2022] 假期计划

看到范围 \(n \le 2500\) 不难想到可以枚举其中两个点。既然都直接枚举了,那也没必要枚举 A 和 D 这种快确定的点

标签:图论,log,int,短路,long,times,dis
From: https://www.cnblogs.com/wxr-blog/p/17923530.html

相关文章

  • 图论
    1.图的基本概念例2.握手定理例3.完全图4.子图与补图5.图的同构例6.路与回路7.割集1.点割集2.边割集3.点连通度4.边连通度8.有向图的连通性例9.图的矩阵表示例10.欧拉图11.汉密尔顿图例12.树1......
  • 使用OCCT构建两个面之间的最短路径
    查找两个面之间的最短面路径查找面的邻面。std::vector<TopoDS_Face>OCCTUtility::adjacentFace(TopoDS_Faceconst&face,std::optional<TopoDS_Shape>shape,std::optional<TopTools_IndexedDataMapOfShapeListOfShape>edgeMapFace){std::vector<TopoD......
  • dijkstra最短路
    给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。输入格式第一行包含整数 n 和 m。接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y的......
  • 浅谈一类边权带指数的图论问题
    偶然看到了这道题,求的是边权为\(n^w\)次方时树上的第\(k\)小路径,觉得这类题目很有意思,就研究了一下。1.CF464ETheClassicProblem题意:给一个无向图,每条边的边权是\(2^{w_i}\),求\(s\)到\(t\)的最短路。思路:首先,我们可以把距离看成一个二进制数,那么我们需要能支持快......
  • 12.11 迪杰斯特拉方法实现最短路径(c++)
     今天通过自主学习,,对数据结构中的迪杰斯特拉方法实现最短路径进行了深造,让我学会了很多新的东西。首先是问题描述:用迪杰斯特拉算法实现有向网的最短路径输入格式:第一行输入有向网的顶点和边数,第二行输入各顶点值,用空格间隔,第三行开始输入各条边的两个点的及边上的权值,用空......
  • #P1009. 单源最短路
    模板代码#include<bits/stdc++.h>#defineintlonglongusingnamespacestd;constintN=1e5+10;vector<pair<int,int>>a[N];intdis[N],vis[N];intn,m;voidspfa(){ for(inti=1;i<=n;i++)dis[i]=0x3f; queue<int>q; dis[1]=0; q.push(......
  • 12.2迪杰斯特拉方法实现最短路径
    掌握迪杰斯特拉方法设计文档 代码#include<iostream>usingnamespacestd;//迪杰特斯拉:邻接矩阵:一维数组+二维数组+点边数typedefintVexType;#defineMVNum100#defineMaxInt32767intS[MVNum],Path[MVNum],D[MVNum];//迪杰特斯拉的三个数组typedefstruct{......
  • [学习笔记]分层图最短路
    分层图的概念分层图最短路,听名字就知道他和其他最短路不一样,实际也确实如此,可以解决一些普通最短路无法解决的问题。比如有\(n\)个点\(m\)条带权无向边,可以将\(k\)条边进行某些操作,然后求出从\(1\)到\(n\)的最短路,此时即可使用分层图。例题例题1P4568[JLOI2011]......
  • 最短路径
    #include<iostream>#include<cstring>#include<cstdio>usingnamespacestd;constintN=1010;structedge{intv,w;edge*next;};structnode{intk;edge*next;}a[1010];intn;intfind(intu){for(inti=0;i<n;i++){if(a......
  • 路径规划算法 - 求解最短路径 - A*(A-Star)算法
    Dijkstra(迪杰斯特拉)算法A*(A-Star)算法是一种静态路网中求解最短路径最有效的直接搜索方法,也是解决许多搜索问题的有效算法。算法中的距离估算值与实际值越接近,最终搜索速度越快。A*算法是一个“搜索算法”,实质上是广度优先搜索算法(BFS)的优化A*算法的作用是“求解最短路径......