题目
题目来源
P4779 【模板】单源最短路径(标准版)
题目描述
给定一个 nn 个点,mm 条有向边的带非负权图,请你计算从 ss 出发,到每个点的距离。
数据保证你能从 ss 出发到任意点。
输入格式
第一行包含四个正整数 n,m,s,tn,m,s,t,分别表示点的个数、有向边的个数、源点序号、汇点序号。
接下来M行每行包含三个正整数ui, vi, wi, 表示第 i 条有向边从 ui 除法,到达 vi, 边权为 wi(即该边最大流量为wi)。
输出格式
一行,包含一个正整数,即为该网络的最大流。
输入样例
4 5 4 3
4 2 30
4 3 20
2 3 20
2 1 30
1 3 40
输出样例
50
数据范围
1 <= n <= 1e5;
1 <= m <= 2e5;
s = 1;
1 <= ui, vi <= n;
1 <= wi <= 1e9;
做法详解
PS:这道题目是个经典的最短路,我们在这里使用dijkstra算法来解决。因为在于对该算法的优化,所以不讲解 dijkstra 算法的原理和实现。
最普通的dijkstra:(链式前向星存图)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
using namespace std;
const int inf = 0x3f3f3f3f;
int m, n, s, cnt;
int head[1005], dist[1005], vis[1005];
struct {
int to;
int w;
int next;
}edge[1005];
//存图
void add(int u, int v, int w)
{
edge[cnt].to = v;
edge[cnt].w = w;
edge[cnt].next = head[u];
head[u] = cnt++;
}
//dijkstra算法
void dijkstra()
{
for (int i = 1; i <= n; i++)
dist[i] = inf;
vis[s] = 1;
dist[s] = 0;
for (int i = head[s]; i != -1; i = edge[i].next)
dist[edge[i].to] = min(dist[edge[i].to], edge[i].w);
for (int i = 1; i <= n; i++)
{
int mid = inf;
int idx;
for (int j = 1; j <= n; j++)
{
if (!vis[j] && dist[j] < mid)
{
mid = dist[j];
idx = j;
}
}
for (int j = head[idx]; j != -1; j = edge[j].next)
{
dist[edge[j].to] = min(dist[edge[j].to], edge[j].w + dist[idx]);
}
}
}
int main()
{
cin >> n >> m >> s;
memset(head, -1, sizeof(head));
for (int i = 0; i < m; i++)
{
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
}
dijkstra();
for (int i = 1; i <= n; i++)
cout << dist[i] << ' ';
return 0;
}
写完了后我们满心欢喜的交一发,会发现所有的测试数据都是 tle,搞人心态。dijkstra 算法的时间复杂度是O(n2),所以对于 n 最大是1e5的数据肯定是会超时的,所以我们需要优化。
优化目标
我们知道 dijkstra 算法的实现中,每一次会找到起点到所有边的最小距离的点,然后用该点来松弛其他的点,从而达到起点到任意一点都是最短的距离。那么我们能否把找最小距离点这一个步骤优化一下呢?当然是可以的。对于最初的遍历的 n 的复杂度,找最小值我们可以使用 logn 复杂度的线段树或者优先队列来优化代码。
进行优化后,整体的复杂度会从O(n2)变为O( (m+n)logn ),大大降低了复杂度,即可AC本题。
线段树优化
因为我们找到最小值后,需要这个最小值的节点来进行松弛,所以线段树需要维护最小值 mi, 对应的节点 idx 两个数据
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
#define ln (node << 1)//左儿子
#define rn (node << 1 | 1)//右儿子
#define mid ((l + r) >> 1)//中间值
//位运算加速
const int INF = 0x3f3f3f3f;
const int N = 1e5 + 5;
const int M = 2e5 + 5;
int n, m, s, cnt;
int head[N], dist[N], vis[N];
struct {
int to;
int w;
int next;
}edge[M];//链式前向星
struct {
int idx, mi;
}tree[N << 2];//线段树
//更新节点
void update(int node)
{
if (tree[ln].mi < tree[rn].mi)
{
tree[node].mi = tree[ln].mi;
tree[node].idx = tree[ln].idx;
}//左儿子小,父节点储存左儿子的距离和节点
else
{
tree[node].mi = tree[rn].mi;
tree[node].idx = tree[rn].idx;
}//右儿子小
}
//常规建树操作
void build(int l, int r, int node)
{
if (l == r)
{
tree[node].mi = dist[l];
tree[node].idx = l;
return;
}
else
{
build(l, mid, ln);
build(mid + 1, r, rn);
update(node);
}
}
//链式前向星加边
void add(int u, int v, int w)
{
edge[cnt].w = w;
edge[cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt++;
}
//用完一个节点后,把这个节点维护的最小值赋值为无穷大
void change(int node, int l, int r, int x, int y, int z)
{
if (l >= x && r <= y)
{
tree[node].mi = z;
return;
}
if (r<x || l>y)
return;
change(ln, l, mid, x, y, z);
change(rn, mid + 1, r, x, y, z);
update(node);
}
void dijkstra()
{
for(int i = 0; i < n - 1; i++)
{
int minn = tree[1].mi;//线段树下标为1就是根节点,维护整个区间的信息
int idx = tree[1].idx;
change(1, 1, n, idx, idx, 2147483647);//取出最小值后,把这个节点赋值无穷大
vis[idx] = 1;
for (int j = head[idx]; j != -1; j = edge[j].next)
{
if (dist[edge[j].to] > dist[idx] + edge[j].w)
{
dist[edge[j].to] = dist[idx] + edge[j].w;//dijkstra常规操作
if(!vis[edge[j].to])
change(1, 1, n, edge[j].to, edge[j].to, dist[edge[j].to]);//如果进行了松弛操作,那么也需要更新这个节点的距离信息
}
}
}
}
int main()
{
cin >> n >> m >> s;
memset(head, -1, sizeof(head));
memset(dist, INF, sizeof(dist));
dist[s] = 0;
build(1, n, 1);
while (m--)
{
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
}
dijkstra();
for (int i = 1; i <= n; i++)
cout << dist[i] << ' ';
return 0;
}
优先队列优化
相比于线段树,代码量更小,通俗易懂。
如果对于优先队列有不懂的,自行学习。
#include <algorithm>
#include <queue>
#include <iostream>
#include <cstring>
using namespace std;
typedef long long ll;
typedef pair<int, int> P;
const int N = 2e5 + 5;
const int INF = 1e9;
int n, m, s, head[N], cnt, dis[N], vis[N];
//链式前向星存边
struct Edge {
int to, w, next;
}e[N];
//加边
void add(int u, int v, int w)
{
e[cnt].w = w;
e[cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt++;
}
void dij()
{
priority_queue<P, vector<P>, greater<P> > que;//建立一个维护最小值的优先队列
//这里不能弄反的就是pair的first值,因为优先队列会先对p.first进行排序,如果一样才根据p.second排序
for (int i = 1; i <= n; i++)
dis[i] = INF;
dis[s] = 0;
que.push(P{ 0, s });//把起点放入队列,距离是第一个值,对应节点是第二个值
while (!que.empty())
{
P p = que.top();//堆顶就是当前的距离起点最小值节点
que.pop();
int mi = p.first;
int idx = p.second;
if (vis[idx])
continue;//如果该点已经被当作中间点,则直接进行下一轮
vis[idx] = 1;
for (int i = head[idx]; i != -1; i = e[i].next)
{
int v = e[i].to;
int w = e[i].w;
if (dis[v] > dis[idx] + w)
{
dis[v] = dis[idx] + w;
if (!vis[v])
que.push(P{dis[v], v});//被更新的点就加入队列
}
}
}
}
int main()
{
cin >> n >> m >> s;
memset(head, -1, sizeof(head));
while (m--)
{
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
}
dij();
for (int i = 1; i <= n; i++)
cout << dis[i] << ' ';
return 0;
}