DAY6-8学习报告 图论:最短路(上)
DAY6-10的集训内容按照原来的进度应该还是像之前一样一天一更新,但是由于图论对于我上手难度相当大(尤其图论是我学竞赛第一个大量学板子的东西),因此等到复习提高了之后再来写。一篇两篇博客都不可能研究通最短路的内涵,YBT参考书的题解写法实在过于灵活,我这种停留在套板子阶段的选手显然是参不透的(也许以后会写一篇博客专门发阅读理解,捂脸)。主要还是对目前所学的一些内容作一个整理总结,尤其是一些细节上的探讨。
一.基础:最短路板子
最短路分为单源最短路和多源最短路(前者单一起点,后者任意起点),前者主要有Dijkstra和SPFA两种算法,后者主要是Floyd这一种算法。
1.Dijkstra算法
Dijkstra算法的原理是用一个优先队列存储起点s到各点的边权值,每次都取出边权最小的那个点继续操作,直到所有的点都被操作过一遍为止。可以说,Dijkstra算法的实质是贪心。
需要注意的是,由于Dijkstra贪心的性质,这种算法对任何一个点最多都只会做一次操作。
板子如下:
typedef pair<int,int> pr;
priority_queue<pr,vector<pr>,greater<pr> > Q;
struct yjx{
void D(){
int i,now,temp;
memset(dis,0x3f,sizeof(dis));
dis[s] = 0;
Q.push(make_pair(0,s));
while(!Q.empty()){
now = Q.top().second;
Q.pop();
if(judge[now]) continue;
judge[now] = 1;
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(dis[temp] > dis[now] + e[i].c){
dis[temp] = min(dis[temp],dis[now] + e[i].c);
Q.push(make_pair(dis[temp],temp));
}
}
}
}
这个板子是加了单调队列和pair(相当于struct)的优化版本,数组的设置根据链式前向星存图产生。
按照网上的说法,pair需要引用一个叫做utility的库,不知道是否必要,有了解的朋友欢迎在评论区补充。
2.SPFA算法
SPFA算法的原理是,从起点开始依次找最短路并存入队列,然后再从队列中取出元素依次找最短路。SPFA的原理实际上就是BFS。由于这种原理,使得SPFA可能多次经过并更新已更新过的点。
void S(){
int i,now,temp;
memset(dis,0x3f,sizeof(dis));
dis[s] = 0;
Q.push(s);
while(!Q.empty()){
now = Q.front();
Q.pop();
judge[now] = 0;
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(dis[temp] > dis[now] + e[i].c){
dis[temp] = dis[now] + e[i].c;
if(!judge[temp]){
Q.push(temp);
judge[temp] = 1;
}
}
}
}
}
从代码的角度上来说,SPFA和Dijkstra的主要区别在于judge的使用和队列的类型;队列的区别就在于贪心和DFS的不同,而judge则决定了是否会多次经过同一点。只要分清这两点区别,相信记住并使用这两个板子不会太难。
3.Floyd算法
Floyd的原理是DP。 对于从i到j的最短路,如果要更新它,则应该取这条路线与从i到k再到j的另一条路线的最小值。这样一来,状态转移方程就已经得出来了。
注意:由于先枚举中转点k,故循环的顺序应该是k i j。
板子如下:
void F(){
int i,j,k;
for(k = 1;k <= n;k++){
for(i = 1;i <= n;i++){
for(j = 1;j <= n;j++){
dp[j][i] = dp[i][j] = min(dp[i][j],dp[i][k] + dp[k][j]);
}
}
}
}
二.初次实战:细节探讨
1.Dijkstra和SPFA的选择
对于这两种算法,我确实没能够记住它们的时间复杂度是如何推导出来的(有会的朋友欢迎在聊天区补充),但是我记得结论如下:Dijkstra时间复杂度为O(nlogn),SPFA时间复杂度为O(km),k不超过n。综上所述,Dijkstra的时间复杂度比SPFA稳定,而且即使是在SPFA时间最短的情况下,也只慢一个logn的时间, 但是SPFA时间最长的情况下远远慢于Dijkstra,这可能导致被卡时间限制。直白点说,能不用SPFA就别用SPFA。
2.存图的方法
对于存图,一般来讲为了方便、省时间一般用的都是链式前向星存法。原理在此就不介绍了,不过放一个模版作为参考:
int cnt = -1,head[200001];//head数组初始值应提前设为-1
struct yjx{
int nxt,to,c;
}e[200001];
void save(int x,int y,int w){
e[++cnt].nxt = head[x];
e[cnt].to = y;
e[cnt].c = w;
head[x] = cnt;
}
学到最短路一般图论的这种基础内容都很熟练了,但是这里面还是有一些东西值得我们探讨,那就是怎么存储边权。即使是权,也分为边权和点权两种情况:边权是边有权而点无权,点权是点无权而边有权。但是我们为了方便还是得存边权,那么怎么转化就是一个值得思考的问题。
为了研究细节,我们还是要实践出真知,不妨看一看下面这道题,顺便练一练板子:(洛谷P1073)
此题当中有两个要素:一个是买,一个是卖。我们希望买的价格尽量小,同时希望卖出的价格尽可能的大;并且我们一定是先买一个便宜的,然后再往后找卖的价格贵的。因此我们需要预处理一下从1到i的过程中买入的最小值,记为dis1[i];然后处理一下从i到n过程中卖出的最大值,记为dis2[i]。这样一来,每一个点的(dis1[i]-dis2[i])都代表了一种选择,其中最大的就是我们可以获得的最大收益。
这样一来思路就出来了:先正向最短路一次,找最小值;再反向存图一次,找最大值,最后得结果。
整个过程中权不传递,只需要我们每次比较,这省去了很多的麻烦;但是由于此题是点权问题,那么我们就必须探讨一下怎么存这个点权。(以下问题当中,我们记i点的点权为c[i])
首先考虑一下朴素思想:对于从x到y的一条路,直接取c[x]或c[y]当边权。
经过一段时间的实验,结论如下:如果用Dijkstra,那么存c[y]可以AC,存c[x]一部分会WA;如果用SPFA,不管存哪个都能AC。
这是为什么呢?
按照我自己分析的情况来看,这地方最主要的问题就是我们如果以边权代替点权,那么必然有一部分会被忽略,而这个被忽略的不是开头就是结尾。对于以上现象,我个人的分析是:对于SPFA来讲,由于一些点会被经过多次,因此或许可以弥补一部分数据的点权;Dijkstra一个点只经过一遍,因此这肯定救不回来了。至于为啥存c[x]还是c[y],我确实不会解释,只能认为这属于偶然。
总而言之,朴素算法这种粗糙的处理我认为是不对的,但是如果有朋友清楚为什么可以忽略掉起点或终点(尤其是起点),非常欢迎在评论区留言。
附一个朴素算法的写法:
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <functional>
#include <vector>
#include <utility>
using namespace std;
typedef pair<int, int> pr;
struct yjx {
int nxt, to, c;
} e1[1000001], e2[1000001];
int ecnt1 = -1, ecnt2 = -1, judge[100001], a[100001], dis1[100001], dis2[100001], head1[100001],
head2[100001], m, n;
priority_queue<pr, vector<pr>, greater<pr> > Q;
void save1(int x, int y, int w) {
e1[++ecnt1].nxt = head1[x];
e1[ecnt1].to = y;
e1[ecnt1].c = w;
head1[x] = ecnt1;
}
void save2(int x, int y, int w) {
e2[++ecnt2].nxt = head2[x];
e2[ecnt2].to = y;
e2[ecnt2].c = w;
head2[x] = ecnt2;
}
void D() {
int i, now, temp;
memset(dis1, 0x3f, sizeof(dis1));
memset(dis2, -1, sizeof(dis2));
dis1[1] = a[1];
Q.push(make_pair(dis1[1], 1));
while (!Q.empty()) {
now = Q.top().second;
Q.pop();
if (judge[now])
continue;
judge[now] = 1;
for (i = head1[now]; ~i; i = e1[i].nxt) {
temp = e1[i].to;
if (dis1[temp] > min(dis1[now], e1[i].c)) {
dis1[temp] = min(dis1[now], e1[i].c);
Q.push(make_pair(dis1[temp], temp));
}
}
}
memset(judge, 0, sizeof(judge));
dis2[n] = a[n];
Q.push(make_pair(dis2[n], n));
while (!Q.empty()) {
now = Q.top().second;
Q.pop();
if (judge[now])
continue;
judge[now] = 1;
for (i = head2[now]; ~i; i = e2[i].nxt) {
temp = e2[i].to;
if (dis2[temp] < max(dis2[now], e2[i].c)) {
dis2[temp] = max(dis2[now], e2[i].c);
Q.push(make_pair(dis2[temp], temp));
}
}
}
}
int main() {
int i, j, x, y, z, res = 0;
scanf("%d %d", &n, &m);
memset(head1, -1, sizeof(head1));
memset(head2, -1, sizeof(head2));
for (i = 1; i <= n; i++) scanf("%d", &a[i]);
for (i = 1; i <= m; i++) {
scanf("%d %d %d", &x, &y, &z);
if (z == 2)
save1(y, x, a[x]), save2(x, y, a[y]);
save1(x, y, a[y]), save2(y, x, a[x]);
}
D();
for (i = 1; i <= n; i++) {
res = max(res, dis2[i] - dis1[i]);
// printf("%d %d\n",dis1[i],dis2[i]);
}
printf("%d", res);
return 0;
}
那么如何严谨地处理这个问题呢?可以额外添加一个点指向所有入度为0的点,边权赋为那个点的点权就可以了。反向操作的时候同理。
以上就是我想到的最短路基础知识点(欢迎在评论区加以补充),下半部分我们将会继续补充一些更高级也更复杂的最短路用法。 To be continued…
Thank you for reading!
1480

被折叠的 条评论
为什么被折叠?



