dijkstra算法的优化

题目

题目来源

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;
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值