最短路算法

邻接矩阵

设一个图中有n个点,那么这个图的邻接矩阵就是一个nn的矩阵。

所以用一个二维数组map[][]来储存这个邻接矩阵。

举个例子:若已知无向图中,xy的路径权值是z,那可以给map[x][y]赋值为z,由于这是一个无向图,所以还需要反向连一次边,就是把map[y][x]也赋值为z

邻接矩阵的空间复杂度是O(n2)

邻接表

邻接矩阵的空间复杂度对于稀疏图来说太浪费,所以邻接表就派上用场。

邻接表通常用四个数组head[]ver[]edge[]next[]来储存。

head[]数组记录了每个节点的第一条边在ver数组和edge数组中的储存位置;

ver[]数组和edge[]数组分别储存每条边的终点和边权;

next[]数组则模拟了链表指针;

//加入一条从x到y,权值为z有向边
void add (int x, int y, int z) {
	ver[++ tot] = y;
	edge[tot] = z;
	next[tot] = head[x];
	head[x] = tot;
}

//访问从x出发的所有边
for (int i = head[x]; i; i = next[i]) {
	int y = ver[i], z = edge[i];
}

邻接表的空间复杂度为O(n+m)

Dijkstra

基础

Dijkstra算法是求单源最短路的,单源的意思是只能知道某一个点到其他所有点的最短距离。

算法流程:每次找出未被标记,且距离源点最近的点,然后从这个点出发,更新这个点的所有出边的点到源点的距离,重复这个操作,直到所有节点都被标记。

接下来我们看代码:

void dijkstra (int s) { //s是我们的源点
	memset(dis, INF, sizeof(dis)); //我们把所有的点到源点的距离都初始化为无穷大 
	int cur = s; //cur为当前节点,初始化为源点
	dis[cur] = 0;
	vis[cur] = 1;
	for (int i = 1; i <= n; i ++) {
		for (int j = 1; j <= n; j ++)
			if (!vis[j] && dis[cur] + map[cur][j] < dis[j]) //此时找到了从cur出发能到达下一个点且未被标记的更优路径,所以更新这条路径 
				dis[j] = dis[cur] + map[cur][j];
		int mini = INF;
		for (int j = 1; j <= n; j ++)
			if (!vis[j] && dis[j] < mini) //此时找到了能到达的最短且未被标记的点 
				mini = dis[cur = j]; //更新下一次我们出发的点 
		vis[cur] = 1; //标记我们下一次出发的点 
	}
}

优化

上个程序的时间复杂度为O(n2),那么如何优化呢?

是否发现我们每次找下一次出发的点的时候,都是暴力来找的,那如果用优先队列来维护这个最近的点,则可以用O(logn)的时间来找出这个点。

同时我们还可以把邻接矩阵换成邻接表,优化空间复杂度。

代码如下:

struct Point {
	int id, val; //id是节点的编号,val是从源点到达id这个节点的路径 
	Point (int id, int val) : id(id), val(val) {}
	bool operator < (const Point &a) const { //重载运算符 
		return val > a.val;
	}
};
void dijkstra (int s) {
	for (int i = 1; i <= n; i ++)
		dis[i] = INF;
	priority_queue <Point> q;
	q.push(Point(s, 0));
	dis[s] = 0;
	while (!q.empty()) {
		int x = q.top().id; //用优先队列来找到我们的下一个目标节点cur 
		q.pop();
		if (vis[x]) continue;
		vis[x] = 1;
		for (int i = head[x]; i; i = next[i]) { //邻接表遍历 
			int y = ver[i];
			if (!vis[y] && dis[y] > dis[x] + edge[i]) {
				dis[y] = dis[x] + edge[i];
				q.push(Point(y, dis[y]));
			}
		}
	}
}

所以我们还是写个模板题练练手吧!

Dijkstra算法优化后虽然时间复杂度优越,但是对于有负权边的图则跑不了。

因为Dijkstra基于贪心思想,每次都只走当前最短的路,且节点被标记后就无法改变,但是如果有负权边,贪心思想则是错误的。

那么对于负权边的图,SPFA算法就可以解决这个问题。

SPFA

给定一张有向图,若图中的某一条边满足dis[x]+zdis[y]xy分别表示两端节点,z表示权值),则称这条边满足三角形不等式。

若所有边都满足三角形不等式,那么dis[]数组就是最短路。

SPFA算法正是让所有边都满足三角形不等式。

算法流程:建立一个队列,把源点放进队列里。每次从队列取出队头,从这个点出发,如果他的出边使得dis[x]+z<dis[y],就把这个出边的点放入队列,然后把队头弹出。重复以上操作,直到队列清空。

代码如下:

void spfa (int s) {
    for (int i = 1; i <= n; i ++)
        dis[i] = INF;
    queue <int> q;
    q.push(s);
    dis[s] = 0;
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        vis[x] = 0;
        for (int i = head[x]; i; i = next[i]) {
            int y = ver[i];
            if (dis[y] > dis[x] + edge[i]) {
                dis[y] = dis[x] + edge[i];
                if (!vis[y]) {
                	q.push(y);
                	vis[y] = 1;
                }
            }
        }
    }
}

Floyd

floyd算法与上面两个算法不同的是它求的是多源最短路,它的实质其实是动态规划,定义disi,jij的最短路,初始值disi,j=ai,ja为邻接矩阵。

状态转移方程disi,j=min(disi,j,disi,k+disk,j)k为沿途经过的城市。

代码也十分简单:

for (int k = 1; k <= n; k ++)
	for (int i = 1; i <= n; i ++)
		for (int j = 1; j <= n; j ++)
			dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);

其中值得注意的是,k这重状态的必须放在最外面循环。

因为k是阶段,如果将其放在最内层循环,dis[i][j]将在这n次循环后被确定,但是在这n次循环中,dis[i][k]dis[k][j]都不一定是最短路。

而如果放在最外层循环,dis[i][j]是一直在被更新的。

然后便是模板题了。

试题

T1:维修电路

T2:跳跳虎回家

T3:最优贸易

题解

维修电路:

可以把每个方格的周围四个点都看成一个节点,节点数量n=(r+1)(c+1)

如果在位置(ri,ci)的方格:

左上角的点:(ri1)(c+1)+ci

右上角的点:(ri1)(c+1)+ci+1

左下角的点:ri(c+1)+ci

右下角的点:ri(c+1)+ci+1

这样就可以把所有的点都表示出来了。

把\的电路的左上角和右下角的点以0边权相连,左下角和右上角的点以1边权相连,代表需要1花费。

同理,把/的电路也以相同的方法相连。

最后,用Dijkstra算法跑一遍最短路就可以找出最小花费了。

代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N = 300010;
int n, t, r, c, tot, head[N], ver[N << 2], edge[N << 2], nex[N << 2], vis[N], dis[N];
struct Point {
    int id, vel;
    Point (int id, int vel) : id(id), vel(vel) {}
    bool operator < (const Point &a) const {
        return vel > a.vel;
    }
};
inline void add (int x, int y, int z) {
    ver[++ tot] = y;
    edge[tot] = z;
    nex[tot] = head[x];
    head[x] = tot;
}
void dijkstra (int x) {
    for (int i = 1; i <= n; i ++)
        dis[i] = 0x3f3f3f3f;
    priority_queue <Point> q;
    q.push(Point(x, 0));
    dis[x] = 0;
    while (!q.empty()) {
        int cur = q.top().id;
        q.pop();
        if (vis[cur]) continue;
        vis[cur] = 1;
        for (int i = head[cur]; i; i = nex[i]) {
            int id = ver[i];
            if (!vis[id] && dis[id] > dis[cur] + edge[i]) {
                dis[id] = dis[cur] + edge[i];
                q.push(Point(id, dis[id]));
            }
        }
    }
}
int main () {
    scanf("%d", &t);
    while (t --) {
        memset(head, 0, sizeof(head));
        memset(ver, 0, sizeof(ver));
        memset(edge, 0, sizeof(edge));
        memset(nex, 0, sizeof(nex));
        memset(vis, 0, sizeof(vis));
        memset(dis, 0x3f, sizeof(dis));
        tot = 0;
        scanf("%d%d", &r, &c);
        n = (r + 1) * (c + 1);
        for (int i = 1; i <= r; i ++) {
            char s[N];
            scanf("%s", s + 1);
            for (int j = 1; j <= c; j ++) {
                if (s[j] == '\\') {
                    add((i - 1) * (c + 1) + j, i * (c + 1) + j + 1, 0);
                    add(i * (c + 1) + j + 1, (i - 1) * (c + 1) + j, 0);
                    add((i - 1) * (c + 1) + j + 1, i * (c + 1) + j, 1);
                    add(i * (c + 1) + j, (i - 1) * (c + 1) + j + 1, 1);
                } else {
                    add((i - 1) * (c + 1) + j + 1, i * (c + 1) + j, 0);
                    add(i * (c + 1) + j, (i - 1) * (c + 1) + j + 1, 0);
                    add((i - 1) * (c + 1) + j, i * (c + 1) + j + 1, 1);
                    add(i * (c + 1) + j + 1, (i - 1) * (c + 1) + j, 1);
                }
            }
        }
        if ((r + c) % 2) {
            printf("NO SOLUTION\n");
            continue;
        }
        dijkstra(1);
        printf("%d\n", dis[(r + 1) * (c + 1)]);
    }
    return 0;
}

跳跳虎回家:

这道题相比最短路模板,多出了限制使用的路。

我们让dis[i][j]表示从源点到i节点,使用了j次限制道路的最短路。

假设我们现在从x节点走到y节点,使用了j次限制道路:

若当前走的是普通道路,dis[y][j]=min(dis[x][j]+edge[x][y],dis[y][j])

若当前走的是限制道路,dis[y][j+1]=min(dis[x][j]+edge[x][y],dis[y][j])

最后,在使用限制道路的数量jk的前提下,找出最小的dis[n][j]就是答案了。

代码:

#include <cstdio>
#include <queue>
#include <iostream>
#include <vector>
using namespace std;
const int INF = 1e8;
const int MAXN = 510;
const int MAXM = 2010;
int n, m, q, k, ans, dis[MAXN][MAXN];
struct data {
	int u, j;
};
struct edge {
	int to, w, d;
};
vector <edge> e[MAXN];
inline int read () {
	int res = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9')
		ch = getchar();
	while (ch >= '0' && ch <= '9') {
		res = res * 10 + (ch - '0');
		ch = getchar();
	}
	return res;
}
void spfa () {
	for (int i = 1; i <= n; i ++)
		for (int j = 0; j <= k; j ++)
			dis[i][j] = INF;
	dis[1][0] = 0;
	queue <data> q;
	q.push((data){1, 0});
	while (!q.empty()) {
		int u = q.front().u, j = q.front().j;
		q.pop();
		for (int i = 0; i < e[u].size(); i ++) {
			int v = e[u][i].to, w = e[u][i].w, d = e[u][i].d;
			if (d) {
				if (dis[v][j + 1] > dis[u][j] + w) {
					dis[v][j + 1] = dis[u][j] + w;
					q.push((data){v, j + 1});
				}
			}
			else {
				if (dis[v][j] > dis[u][j] + w) {
					dis[v][j] = dis[u][j] + w;
					q.push((data){v, j});
				}
			}
		}
	}
}
int main () {
	n = read();
	m = read();
	q = read();
	k = read();
	k = min(k, min(n, q));
	for (int i = 1; i <= m; i ++) {
		int u, v, w;
		u = read();
		v = read();
		w = read();
		e[u].push_back((edge){v, w, 0});
	}
	for (int i = 1; i <= q; i ++) {
		int u, v, w;
		u = read();
		v = read();
		w = read();
		e[u].push_back((edge){v, w, 1});
	}
	spfa();
	ans = INF;
	for (int i = 0; i <= k; i ++)
		ans = min(ans, dis[n][i]);
	if (ans == INF)
		ans = -1;
	printf("%d\n", ans);
	return 0;
}

最优贸易:

题目需要我们从1号节点到n号节点的途中,以最优的差价买入和卖出水晶球。

我们不妨定义一个minn[]数组,minn[x]代表从1号节点到n号节点的途中,x节点可以在之前由最低的价格minn[x]买入。

minn[]数组的初始值分别是所在节点的水晶球的价格,如果我们从x节点到y节点,那么minn[y]=min(minn[y],minn[x])

因此我们在用SPFA走这张图的时候,可以更新minn[]数组。

再定义一个f[]数组,f[x]代表在走到x节点之前获得的最高差价。

如果我们从x节点到y节点,很显然f[y]可以这么转移:f[y]=max(f[x],a[y]minn[y])a[y]表示y节点的水晶球价格)。

由于必须在n节点结束,所以f[n]就是答案。

代码:

#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
const int N = 100010, M = 500010;
int n, m, a[N], head[N], ver[M << 1], nex[M << 1], tot, vis[N], minn[N], f[N];
inline void add (int x, int y) {
	ver[++ tot] = y;
	nex[tot] = head[x];
	head[x] = tot;
}
inline void spfa () {
	queue<int> q;
	vis[1] = 1;
	q.push(1);
	while (!q.empty()) {
		int x = q.front();
		q.pop();
		vis[x] = 0;
		for (int i = head[x]; i; i = nex[i]) {
			int y = ver[i];
			minn[y] = min(minn[y], minn[x]);
			if (a[y] - minn[y] > f[y] || f[x] > f[y]) {
				f[y] = max(a[y] - minn[y], f[x]);
				if (!vis[y]) {
					vis[y] = 1;
					q.push(y);
				}
			}
		}
	}
}
int main () {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i ++) {
		f[i] = -1;
		scanf("%d", &a[i]);
		minn[i] = a[i];
	}
	f[1] = 0;
	for (int i = 1; i <= m; i ++) {
		int x, y, z;
		scanf("%d%d%d", &x, &y, &z);
		if (z == 1)
			add(x, y);
		else {
			add(x, y);
			add(y, x);
		}
	}
	spfa();
	printf("%d", f[n]);
	return 0;
}
posted @   duoluoluo  阅读(39)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示