【图论】Dijkstra算法解决有向图最短路问题

在图论的学习中,我们有时候会涉及到有关点与点之间的最短距离的问题,而今天我在这里介绍一下一个解决最短路问题的比较基础的解决最短路问题的算法----Dijkstra算法及其优化

  • 算法简单介绍: 首先存下每条边的长度与连接的端点与初始化每两个点之间的距离为正无限大。然后通过循环,每一次循环确定一个点i,i点是目前所有未确定点中距离起点最近的点,然后枚举所有未确定的点j,看看是从之前的已经确定的点到j点的距离近还是从i点到j点的距离近。

  • 注意:Dijkstra算法只适用于边权为正的图

  • 伪代码
    d[N]代表从0点到i点的最短距离
    ch[N]代表点i有没有被确定

枚举所有点:设d[0]=0,d[i]=INF

循环n次{
	所有未被确定的点中找出d值最小的点x
	给x点确定标记
	遍历所有从x点出发的边,更新d[y]=min(d[y],d[x]+w(x--->y))
}
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围
1≤n≤500,
1≤m≤1e5,
图中涉及边长均不超过10000。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3

分析题目我们可以得知,该题目所涉及的图边的数量和点的数量相差了几个数量级,为密集图,因此我们可以用邻接矩阵来存图从而实现朴素Dijkstra算法

#include<iostream>
#include<iomanip>
#include<utility>
#include<cstdio>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#include<vector>
#include<map>
#include<stack>
#include<set>
#include<bitset>
#include<ctime>
#include<cstring>
#include<list>
#define ll long long
#define fi first
#define se second
using namespace std;
typedef  pair<int, int> PII;

int n, m;
int dis[505];		//dis[i]代表了i点到起点1的距离,初始化为无限大(0x3f3f3f3f)
int tu[505][505];		//邻接矩阵存图
bool ch[505];		//判断该点是否距离确定

void dijkstra()
{
	memset(dis, 0x3f, sizeof dis);		//距离初始化为正无穷大
	dis[1] = 0;/		/起点到起点的距离为0,第一次t一定是起点
	/*算法实现过程,n次循环每次找到未确定的离起点最短点贪心求解*/
	for (int i = 1; i <= n; i++)
	{
		int t = -1;		//t为该次循环所确定的离起点距离最短的点的编号
		for (int j = 1; j <= n; j++)
		{
			if (!ch[j] && (t == -1 || dis[t] > dis[j]))		//每次找到未确定最短点
				t = j;
		}
		ch[t] = 1;	//每次循环确定一个点离起点的最小距离
		for (int j = 1; j <= n; j++)	//判断j点是直接从起点来的距离短还是路过该次循环所确定的点到达这个点的距离短
		{
			dis[j] = min(dis[j], dis[t] + tu[t][j]);
		}
	}
	if (dis[n] < 0x3f3f3f)
		cout << dis[n] << endl;
	else  //如果目标点在算法中未被更新,则无法到达
		cout << "-1" << endl;
}

void solve()
{
	memset(tu, 0x3f,sizeof tu);  //邻接矩阵初始化
	cin >> n >> m;
	while (m--)
	{
		int x, y, z;
		cin >> x >> y >> z;
		if (x != y)//避免无意义的自环
		{
			tu[x][y] = min(z, tu[x][y]);  //重边则选取最小的边
		}
	}
	dijkstra();
}

int main()
{
	solve();
	return 0;
}

时间复杂度为O(n2),不是很好,但是在一般情况下够用了。

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围
1≤n,m≤1.5×1e5,
图中涉及边长均不小于 0,且不超过 10000。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3

该题目与上面的题目的区别是点与边数量关系的不同,为稀疏图,因此我们采用邻接表来存图,并且用到STL中的数据结构-----小根堆(优先队列实现)。

不难发现这么一回事:在朴素版的算法中,我们需要通过一层循环来确定每一次距离起点最短的点,但是我们如果用小根堆来维护每个点到起点的距离,我们只需要每次把小根堆的第一个数取出就可以得到最短的点。时间复杂度优化到O(mlogn)。

这里还有一个问题:m在最坏的情况下可以达到n2,但是在很多情况下这么优化是没错的。

#include<iostream>
#include<iomanip>
#include<utility>
#include<cstdio>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#include<vector>
#include<map>
#include<stack>
#include<set>
#include<bitset>
#include<ctime>
#include<cstring>
#include<list>
#define ll long long
#define fi first
#define se second
using namespace std;
typedef  pair<int, int> PII;

const int N = 150050;
int h[N],e[N],ne[N], v[N], idx;//邻接表(链式前向星)
int dis[N];
bool ch[N];
int n, m;

void add(int a, int b, int c)//存边的add函数
{
	e[idx] = b;
	ne[idx] = h[a];
	v[idx] = c;
	h[a] = idx++;
}

void dijkstra()
{
	memset(dis, 0x3f, sizeof dis);
	dis[1] = 0;
	/*PII中,first代表与起点的距离,second代表点的编号*/
	priority_queue<PII, vector<PII>, greater<PII>> heap;//(STL优先队列实现)小根堆的定义,小根堆会自动根据pair中first的值(到起点的距离)对内部的点进行排序,因此省下落遍历点寻找未定最短点的时间
	heap.push({ 0,1 });//放入起点
	while (heap.size())
	{
		auto t = heap.top();
		heap.pop();
		int ver = t.second, distance = t.first;
		if (ch[ver])continue;
		ch[ver] = true;
		for (int i = h[ver]; i != -1; i = ne[i])//邻接表的遍历
		{
			int j = e[i];
			if (dis[j] >( distance + v[i]))
			{
				dis[j] = distance + v[i];
				heap.push({ dis[j], j });
			}
		}
	}
	if (dis[n] >=0x3f3f3f3f)
		cout << "-1" << endl;
	else
		cout << dis[n] << endl;
}


void solve()
{
	cin >> n >> m;
	memset(h, -1, sizeof h);//邻接表初始化为-1
	while (m--)
	{
		int x, y, z;
		cin >> x >> y >> z;
		add(x, y, z);
	}
	dijkstra();

}

int main()
{
	solve();
	return 0;
}

在上面两道题目中,我们都只介绍了从起点到终点的最短路,但是其实如果是要求出任意选择两点的最短距离,只需要简单的改变一下代码自定义起点和终点就可以了。
练习题:HDU1548 A strange lift 奇怪的电梯
题目描述

Problem Description
There is a strange lift.The lift can stop can at every floor as you want, and there is a number Ki(0 <= Ki <= N) on every floor.The lift have just two buttons: up and down.When you at floor i,if you press the button "UP" , you will go up Ki floor,i.e,you will go to the i+Ki th floor,as the same, if you press the button "DOWN" , you will go down Ki floor,i.e,you will go to the i-Ki th floor. Of course, the lift can't go up high than N,and can't go down lower than 1. For example, there is a buliding with 5 floors, and k1 = 3, k2 = 3,k3 = 1,k4 = 2, k5 = 5.Begining from the 1 st floor,you can press the button "UP", and you'll go up to the 4 th floor,and if you press the button "DOWN", the lift can't do it, because it can't go down to the -2 th floor,as you know ,the -2 th floor isn't exist.
Here comes the problem: when you are on floor A,and you want to go to floor B,how many times at least he has to press the button "UP" or "DOWN"?
Input
The input consists of several test cases.,Each test case contains two lines.
The first line contains three integers N ,A,B( 1 <= N,A,B <= 200) which describe above,The second line consist N integers k1,k2,....kn.
A single 0 indicate the end of the input.
Output
For each case of the input output a interger, the least times you have to press the button when you on floor A,and you want to go to floor B.If you can't reach floor B,printf "-1".
Sample Input
5 1 5
3 3 1 2 5
0
Sample Output
3


题意:电梯在每一层都有一个数k,只能往上k层或者往下k层。让你求从m层到n层至少需要嗯多少次电梯按钮。

ac代码

#include<iostream>
#include<iomanip>
#include<utility>
#include<cstdio>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#include<vector>
#include<map>
#include<stack>
#include<set>
#include<bitset>
#include<ctime>
#include<cstring>
#include<list>
#define ll long long
#define fi first
#define se second
using namespace std;
typedef  pair<int, int> PII;

int dis[205];
int tu[205][205];
bool ch[205];


void dijkstra(int f, int to,int n)//f是起点,to是终点,n是总长度
{
	memset(dis, 0x3f, sizeof dis);
	memset(ch, 0, sizeof ch);
	dis[f] = 0;
	for (int i = 1; i <= n; i++)
	{
		int t = -1;
		for (int j = 1; j <= n; j++)
		{
			if (!ch[j] && (t == -1 || dis[t] > dis[j]))
			{
				t = j;
			}
		}
		ch[t] = 1;
		for (int j = 1; j <= n; j++)
		{
			dis[j] = min(dis[j], dis[t] + tu[t][j]);
		}
	}
	if (dis[to] < 0x3f3f3f)
		cout << dis[to] << endl;
	else
		cout << "-1" << endl;
}


void solve()
{
	int n, a, b;
	cin >> n;
	while (n)
	{
		memset(tu, 0x3f, sizeof tu);
		cin >> a >> b;
		for (int i = 1; i <= n; i++)
		{
			int k;
			cin >> k;
			if (i - k > 0)
			{
				tu[i][i - k] = 1;
			}
			if (i + k <= n)
			{
				tu[i][i + k] = 1;
			}
		}
		dijkstra(a, b, n);
		cin >> n;
	}
}

int main()
{
	solve();
	return 0;
}

作者:Avalon Demerzel,喜欢我的博客就点个赞吧,更多图论与数据结构知识点请见作者专栏《图论与数据结构》

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值