CSP-迪杰斯特拉算法和变形求解最短路问题
知识简述
迪杰斯特拉(Dijkstra)算法,是一种求解正权边单源最短路的常用算法,稍有了解的人都比较熟悉,这里稍作简述Dij算法的实现过程。
1、首先我们要明确在进行迪杰斯特拉算法的过程中要维护的数据:一个小根堆用来存储当前已经更新过的点,一个dis[n]数组用来存储源点s到其他点的最短距离,一个vis[n]记录每个点是否被更新过。
2、现在开始算法的具体步骤:将dis[n]全部置为inf(极大值),dis[s]=0;将s点放入空的小根堆,并置vis[s]=1。
3、每次从小根堆中取出根元素a(最小值),并遍历该点的邻接边,如果对某个点y和边w(a,y),有dis[y]>dis[a]+w(a,y),则说明该点可以更新,一般在迪杰斯特拉算法中将更新的操作成为松弛,某个点被松弛要完成两个操作:更新dis[y]和将y加入小根堆中。
4、重复操作3,直至小根堆为空。遍历dis[n]数组,其值即为最短路距离,如果有点其dis[]值仍为inf,说明该点不可达。
迪杰斯特拉的算法特点:由于所有边均为正边,由此有如果堆先后弹出了a和b点,则一定有dis[a]<=dis[b],因此不可能存在dis[b]+正边<dis[a]。这个特点可以总结为Dij算法最显著的特征:所有点只会被弹出一次,且只要某个点被弹出,说明到该点的距离已经是最小值。
具体实现:在使用Dij过程中,用到了小根堆数据结构,但考虑到小根堆的初始化比较复杂,可以使用大根堆,将(-dis[i])传入大根堆即可起到与小根堆相同的效果。
拓展:单源正值最长路Dij做法
Dij由于其松弛的性质,也可以用来计算单源最长路,但有几个tips需要注意:
1、计算最长路,松弛操作应该变为dis[y]<dis[a]+w(a,y),且存储点集使用大根堆
2、在最长路的计算时,已经不再满足只要某个点被弹出,说明到该点的距离已经是最大值这条性质,每个点可以被多次弹出。
3、由于在计算最长路时,每个点会被多次弹出,这时有可能出现无意义的更新,为了提高Dij的性能,在出现堆顶x距离<dis[x],说明该点已经被其他更新过,不是最长路了,因此更新无意义,可以直接剪枝去除。
Dijkstra基本实现(C++)
const int inf=1e8;
const int M=1e6;
const int N=1e5;
struct Edge
{
int to,nxt,value;
}e[M];
int head[N];
int tot=1;
int dis[N];
int n;//点数
int m;//边数
void init(int num)
{
for(int i=1;i<=num;i++)
head[i]=-1;
}
void add(int x,int y,int value)
{
tot++;
e[tot].to=y;
e[tot].value=value;
e[tot].nxt=head[x];
head[x]=tot;
}
priority_queue<pair<int,int>> q;
void dijkstra(int s)
{
while(q.size()) q.pop();
//初始化
for(int i=1;i<=n;I++)
vis[i]=0,dis[i]=inf;
dis[s]=0;
q.push(make_pair(0,s));
//初始化堆和访问数组
while(q.size())
{
int x=q.top().second;
q.pop();
//每个点只能被弹出一次
//在dijkstra的最短路中,一个点被堆弹出一次,则它已经到达最短路
if(vis[x]==1) continue;
vis[x]=1;
//取出堆
for(int i=head[x];i>=0;i=e[i].nxt)
{
int y=e[i].to;
int value=e[i].value;
if(dis[y]>dis[x]+value)//松弛
{
dis[y]=dis[x]+value;
q.push(make_pair(-dis[y],y));
}
}
}
}
题目概述
众所周知,TT 有一只魔法猫。
今天他在 B 站上开启了一次旅行直播,记录他与魔法猫在喵星旅游时的奇遇。 TT 从家里出发,准备乘坐猫猫快线前往喵星机场。猫猫快线分为经济线和商业线两种,它们的速度与价钱都不同。当然啦,商业线要比经济线贵,TT 平常只能坐经济线,但是今天 TT 的魔法猫变出了一张商业线车票,可以坐一站商业线。假设 TT 换乘的时间忽略不计,请你帮 TT 找到一条去喵星机场最快的线路,不然就要误机了!
INPUT&输入样例
输入包含多组数据。每组数据第一行为 3 个整数 N, S 和 E (2 ≤ N ≤ 500, 1 ≤ S, E ≤ 100),即猫猫快线中的车站总数,起点和终点(即喵星机场所在站)编号。
下一行包含一个整数 M (1 ≤ M ≤ 1000),即经济线的路段条数。
接下来有 M 行,每行 3 个整数 X, Y, Z (1 ≤ X, Y ≤ N, 1 ≤ Z ≤ 100),表示 TT 可以乘坐经济线在车站 X 和车站 Y 之间往返,其中单程需要 Z 分钟。
下一行为商业线的路段条数 K (1 ≤ K ≤ 1000)。
接下来 K 行是商业线路段的描述,格式同经济线。
所有路段都是双向的,但有可能必须使用商业车票才能到达机场。保证最优解唯一。
输入样例:
4 1 4
4
1 2 2
1 3 3
2 4 4
3 4 5
1
2 4 3
OUTPUT&输出样例
对于每组数据,输出3行。第一行按访问顺序给出 TT 经过的各个车站(包括起点和终点),第二行是 TT 换乘商业线的车站编号(如果没有使用商业线车票,输出"Ticket Not Used",不含引号),第三行是 TT 前往喵星机场花费的总时间。
输出样例:
1 2 4
2
5
题目重述
仔细读题我们可以将题目概括如下:
两张全正边的有向有权图,一张图A可以随意行动,一张图B只能挑选一条边走,从a到b的最短路径和路径点。
思路概述(分层Dij或枚举变式)
将题目分解我们可以发现:
如果去掉带有限制的一张图B,就是一道简单的Dij板子题,但加入了这张限制图后就会对路径产生影响,A图上的某些路径可能可以通过替换B上的一条边变得更短。
这里列举两种做法:
枚举法:适用于B上只能选1条边的情况
由于最多只能从B中选出一条,也就是选择情况是0/1,我们可以将问题化简成:
1、分别从两个端点出发对A图进行两次Dij,得到两个距离数组。
2、枚举B图中的每一条边,并进行相应的松弛更新,遍历完B中所有边即可确定是否选择或选择哪条边。
问题:如果B图可以选择n条边,则无法解决。
分层Dij:稍后更新
题目源码(C++)
枚举+Dij方法源码
#include<iostream>
#include<stdio.h>
#include<queue>
#include<string>
#include<vector>
using namespace std;
const int M=1e4+5;
const int N=1e3+5;
const int inf=1e8;
int point_number;//点个数
int eco_line;//经济线条数
int busin_line;//商务线条数
int eco_tot;//经济线add计数
int busin_tot;//商务线add计数
bool cout_flag=true;
vector<int> vec;
struct Edge{
int to,txt,value;
};
Edge eco_edge[M];//经济线边集
Edge busin_edge[M];//商务线边集
int eco_head[N];
int busin_head[N];
void eco_init()
{
for(int i=0;i<=point_number;i++)
eco_head[i]=-1;
}
void eco_add(int x,int y,int value)
{
eco_tot++;
eco_edge[eco_tot].to=y;
eco_edge[eco_tot].value=value;
eco_edge[eco_tot].txt=eco_head[x];
eco_head[x]=eco_tot;
}
int vis[N];
int pre[N];
int back[N];
priority_queue<pair<int,int>> q;
void dijkstral(int s,int* dis,int*pre)
{
while(q.size()) q.pop();
for(int i=1;i<=point_number;i++)
dis[i]=inf,vis[i]=0,pre[i]=0;
dis[s]=0;
vis[s]=0;
pre[s]=s;
q.push(make_pair(0,s));
while(q.size())
{
int x=q.top().second;
q.pop();
if(vis[x]==1) continue;
vis[x]=1;
for(int i=eco_head[x];i;i=eco_edge[i].txt)
{
int y=eco_edge[i].to;
int value=eco_edge[i].value;
if(dis[y]>dis[x]+value)
{
dis[y]=dis[x]+value;
pre[y]=x;
q.push(make_pair(-dis[y],y));
}
}
}
}
int main()
{
cout_flag=false;
while(cin>>point_number)
{
int start,end=0;
eco_tot=0;
busin_tot=0;
cin>>start>>end;
cin>>eco_line;
eco_init();
for(int i=0;i<eco_line;i++)
{
int x,y,z;
scanf("%d %d %d",&x,&y,&z);
eco_add(x,y,z);
eco_add(y,x,z);
}
int dis_start[N];
int dis_end[N];
dijkstral(start,dis_start,pre);
dijkstral(end,dis_end,back);
vec.clear();
int dis_min=dis_start[end];
int switch_start=0;
int switch_end=0;
int switch_line=0;
cin>>busin_line;
for(int i=0;i<busin_line;i++)
{
int x,y,z;
scanf("%d %d %d",&x,&y,&z);
if(dis_start[x]+dis_end[y]+z<dis_min)
{
dis_min=dis_start[x]+dis_end[y]+z;
switch_start=x;
switch_end=y;
switch_line=1;
}
if(dis_start[y]+dis_end[x]+z<dis_min)
{
dis_min=dis_start[y]+dis_end[x]+z;
switch_start=y;
switch_end=x;
switch_line=1;
}
}
if(cout_flag==true) printf("\n");
else cout_flag=true;
if(switch_line==0)
{
int flag=end;
while(flag!=start)
{
vec.push_back(flag);
flag=pre[flag];
}
vec.push_back(start);
for(int i=vec.size()-1;i>=1;i--)
printf("%d ",vec[i]);
printf("%d\nTicket Not Used\n",vec[0]);
printf("%d\n",dis_min);
}
else
{
int flag=switch_start;
while(flag!=start)
{
vec.push_back(flag);
flag=pre[flag];
}
vec.push_back(start);
flag=switch_end;
while(flag!=end)
{
vec.insert(vec.begin(),flag);
flag=back[flag];
}
vec.insert(vec.begin(),end);
for(int i=vec.size()-1;i>=1;i--)
printf("%d ",vec[i]);
printf("%d\n%d\n",vec[0],switch_start);
printf("%d\n",dis_min);
}
vec.clear();
}
return 0;
}
/*
6 1 6
4
1 2 3
2 3 4
3 5 3
5 6 4
1
2 5 5
6 1 6
4
1 2 3
2 3 4
3 5 3
5 6 4
1
2 5 8
*/