算法笔记8.最短路

简介

最短路问题一般分为两类,单源最短路和多源汇最短路。

单源最短路是求一个点到其他点的最短距离,单源最短路又可以分为两大类,所有边权都是正数和存在负权边两类。

所有边权都是正数:朴素Dijkstra算法,时间与边数无关,适合稠密图。和堆优化版的Dijkstra算法,适合稀疏图。

存在负权边:Bellman-Ford算法。SPEA算法(效率高一些,但是如果有限制经过的边数就不能用)

多源汇最短路就是起点和终点不固定求最短路径,只有一种Floyd算法。

朴素Dijkstra算法

基本思路

首先初始化距离,我们用一个dis数组表示每个点到起点的距离,dis[1]就表示1号点到起点的距离(其实1号点就是起点)。令dis[1] = 0,别的为正无 穷(用一个很大的数代替就行),然后声明一个s来存已经确定最短距离的点。

然后循环n次,在循环中,我们找一个不在s中的,距离最近的点t。然后把t加入s中,再用t更新各点的距离(假如更新的距离比原距离小就覆盖,大就略过)

这样我们每次循环就能确定一个点的最短距离,n次就能确定所有点了。

(这个的感觉其实就是,先慢慢从起点往外延申,起点延申出来的好几条中最短的路径,这一条肯定是最短的,延申出来的这个点又可以当一个新的起点,假如说这个起点到一个点的距离有点大了,我们就会用老起点延申出来的第二短的路继续找。有一种更新起点的机制,同时又避免了绕路)

这个适用于稠密图(用邻接矩阵存),不用太过关注是有向图还是无向图,无向图就是一种特殊的有向图(两个方向都通),所以一律当作有向图来做就行

代码

const int N = 510;
int n,m;
int g[N][N]; //邻接矩阵存图
int dis[N]; 
bool st[N]; //dis中的距离是不是最短的

int dijkstra()
{
	//初始化
	memset(dis, 0x3f, sizeof dis);
	dis[1] = 0;

	for (int i = 0; i < n; i++)
	{
		int t = -1;
		//找最短距离
		for (int j = 1; j <= n; j++)
		{
			if (!st[j] && (t == -1 || dis[t] > dis[j]))
				t = j;
		}
		st[t] = true; //加入s
		//利用t更新最短距离
		for (int j = 1; j <=n; j++)
		{
			dis[j] = min(dis[j], dis[t] + g[t][j]);
		}
	}

	//返回距离
	if (dis[n] == 0x3f3f3f) return -1;
	return dis[n]
}

堆优化版的Dijkstra算法

基本思路

在上面的朴素版本里面,最慢的一步是找出最小距离,我们就可以用之前学过的堆结构来优化它。我们直接用c++的优先队列就行。

然后因为是稀疏图,我们要用邻接表的形式

代码

typedef pair<int, int> PII;

const int N = 100010;
int n,m;

//h存链表头指针
//w存边的权重
//e存终点
//ne存链表指针
int h[N], w[N], e[N], ne[N], idx;
int dis[N]; 
bool st[N]; //dis中的距离是不是最短的

void dijkstra()
{
	//初始化
	memset(dis, 0x3f, sizeof dis);
	dis[1] = 0;
	//队列初始化
	//这三个模板的意思是数据类型,容器类型,比较方式(这个是PII升序的意思)
	priority_queue<PII, vector<PII>, greater<PII>> heap;
	heap.push({ 0,1 });

	while (heap.size())
	{
		//这样就相当于是得到最短的了
		auto t = heap.top();
		heap.pop();
		//ver是对应的位置,distance是距离
		int ver = t.second, distance = t.first;

		if (st[ver]) continue;

		//遍历所有路径
		for (int i = h[ver]; i != -1; i = ne[i])
		{
			int j = e[i];
			//更新距离,加入队列
			if (dis[j] > distance + w[i])
			{
				dis[j] = distance + w[i];
				heap.push(dis[j], j);
			}
		}
	}

	//返回距离
	if (dis[n] == 0x3f3f3f) return -1;
	return dis[n]
}

Bellman-Ford算法

基本思路

迭代n次,每次迭代遍历所有边。每次循环用当前边更新距离(第二重遍历的时候用已经找到的最短距离更新最短距离)。这个算法对数据结构无要求,我们可以直接用结构体数组存。

我们第一重迭代的次数其实有实际意义,第一重迭代的次数就是经过几条边找到的最短路径。

代码

const int N = 510, M = 10010;

int n, m, k;
int dis[N], backup[N];

struct Edge
{
	int a, b, w;
}edges[M];//Edge是边,a,b是起点终点,w是权值

int bellman_ford()
{
	//初始化
	memset(dis, 0x3f, sizeof dis);
	dis[1] = 0;

	//这个n可以改成边数限制的次数
	for (int i = 0; i < n; i++)
	{
		//这个相当于是将上一次的结果拷贝一下,确保我们更新只用上一次的结果
		memcpy(backup, dis, sizeof dis);
		//遍历边,更新距离
		for (int j = 0; j < m; j++)
		{
			int a = edges[j].a, b = edges[j].b, w = edges[j].w;
			dis[b] = min(dis[b], backup[a] + w);
		}
	}
	if (dis[n] > 0x3f3f3f3 / 2) return -1; //除2是为了防止在负路径里面套导致更新距离
	return dis[n];
}

SPEA算法

基本思路

这个算法是对Bellman-Ford算法的一个优化,因为Bellman-Ford算法其中有很多不需要做的操作,比如如果一个点没有被更新过,那么它后面的点也不需要更新,就相当于是跳过了一些无用功。

我们用类似宽搜的思想来优化它,把更新过的点放进一个队列,再然后取出更新放入新点。

(长得特别像Dijkstra算法)

代码

typedef pair<int, int> PII;

const int N = 100010;
int n, m;

//h存链表头指针
//w存边的权重
//e存终点
//ne存链表指针
int h[N], w[N], e[N], ne[N], idx;
int dis[N];
bool st[N]; //dis中的距离是不是最短的

int SPFA()
{
	//初始化 
	memset(dis, 0x3f, sizeof dis);
	dis[1] = 0;

	queue<int> q;
	q.push(1);
	st[1] = false;
	while (q.size())
	{
		int t = q.front();
		q.pop();
		st[t] = false;

		for (int i = h[t]; i != -1; i = ne[i])
		{
			int j = e[i];
			if (dis[j] > dis[t] + w[i])
			{
				dis[j] = dis[t] + w[i];
				if (!st[j])
				{
					q.push(j);
					st[j] = true;
				}
			}
		}
	}
	if (dis[n] == 0x3f3f3f3f) return -1;
	return dis[n];
}

Floyd算法

基本思路

基于动态规划的思想,用邻接矩阵存图。

首先需要初始化一个二维数组dis,dis[i][j]表示从i到j的距离。初始化时直接相连的赋值,不直接相连的用正无穷代替。

算法将由i到j换成由i到k的距离再加上由k到j的距离,如果更小就替换。

需要使用三重循环,第一重循环k,使算法经过每一个点,第二重循环i,第三重循环j,然后用一个if判断需不需要改距离。

更适合稠密图。

代码

void floyd()
{
    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值