一、概述
图论求最短路径问题。
采用三种方法分别解决。
1、Dijkstra加DFS,邻接矩阵法(只能证明样例正确,因为开10000*10000的矩阵直接报内存溢出)
2、Dijsktra加DFS,邻接表法
3、直接DFS。
二、分析
1、Dijkstra加DFS,邻接矩阵法
由于是求图的最短路径问题,第一时间想到Dijkstra和DFS联合使用。思路如下:
①、邻接矩阵记录两点间的线路。eg.G[3212][3008]=3,G[3212][1001]=1。不相邻的则是INF。
②、Dijkstra遍历,求出起点到所有点的最短距离与最小路径。保存在vector数组pre中。
③、DFSpre数组,求出转换站最少的路径。
首先,我们要确定思路。是用结构体定义节点来存储站点信息,比如说是不是可以换线路的站,他属于几号线什么的。后来发现不需要,直接在G中存储线路就可以了。这样做的好处是任何相邻站点间的线路信息都可以清晰存储,判断一条路径上某一个点b是不是换线路的站,只需要判断他前面一个站a和后面一个站c,看G[a][b]和G[b][c]是否相等即可。因此选择邻接矩阵存储。如下:
fill(G[0],G[0]+10000*10000,INF);
fill(MIN,MIN+10000,INF);
int N;
scanf("%d",&N);
for(int i=1;i<=N;i++)
{
int L;
scanf("%d",&L);
int head;
scanf("%d",&head);
for(int j=1;j<L;j++)
{
int num;
scanf("%d",&num);
G[head][num]=i;
G[num][head]=i;
head=num;
}
}
注意初始化,以及无向图要存两个值。
第二,开始Dijkstra。Dijkstra需要min数组,vis数组,pre数组,由于题中要查询多次,因此需要多次Dijkstra,每次进行Dijkstra之前这三个数组要初始化,如下:
fill(MIN,MIN+10000,INF);
fill(vis,vis+10000,0);
for(int j=0;j<10000;j++)
pre[j].clear();
其实从这一步就可以看出来这种方法肯定不好了,每次查询的初始化时间实在是太长。
然后开始Dijkstra,如下:
void Dijkstra(int root)
{
MIN[root]=0;
while(1)
{
int min=INF;
int MINnode=-1;
for(int i=0;i<10000;i++)
{
if(MIN[i]<min&&vis[i]==0)
{
min=MIN[i];
MINnode=i;
}
}
if(MINnode==-1)
return;
vis[MINnode]=1;
for(int i=0;i<10000;i++)
{
if(G[MINnode][i]!=INF&&vis[i]==0)
{
if(MIN[i]>MIN[MINnode]+1)
{
MIN[i]=MIN[MINnode]+1;
pre[i].clear();
pre[i].push_back(MINnode);
}
else if(MIN[i]==MIN[MINnode]+1)
{
pre[i].push_back((MINnode));
}
}
}
}
}
唯一需要改变的就是更新MIN数组时的判断条件,是加一而不是加G的值,因为计算的是停靠站点,而不是距离。
这样就得到了pre数组。
第三,对pre数组进行DFS。
注意这里的第二优先级是依靠换乘数量决定的,换乘越少越好。而DFSpre则是从终点站往起点站走。因此如果每换乘一次,要加一。这个换乘次数是随着DFS变化的。因此要作为参数输入。同时注意判断换乘要判断上一次的线路和本次线路,因此参数要再加一个上一站到本站的线路号。这样DFS的参数就齐了,如下:
vector<int> temp,quick;
int tranMIN=0x3f3f3f3f;
void DFS(int d,int s,int tran,int preline)
{
if(d==s)
{
temp.push_back(d);
if(tran<tranMIN)
{
quick=temp;
tranMIN=tran;
}
temp.pop_back();
return;
}
else
{
temp.push_back(d);
vector<int>::iterator it;
for(it=pre[d].begin();it!=pre[d].end();it++)
{
if(G[d][*it]!=preline)
{
DFS(*it,s,tran+1,G[d][*it]);
}
else
{
DFS(*it,s,tran,G[d][*it]);
}
}
temp.pop_back();
}
}
通过DFS我们得到了终点到起点,换乘最少的路径。下面要把这条路径按规定输出。
首先判断是不是这条路径中只有两个站点,如果只有两个,那么不用判断直接输出。
如果大于两个,则可能换乘。
开两个vector,ans保存换乘站点,因为只有这个要输出,ansline保存换乘线路,这个也要输出。
首先将起始站点输入ans,起始站点到下一站的线路输入ansline。
然后从尾到头遍历路径。
同样以G[a][b]!=G[b][c]来判断换乘站点。更新两个vector。
然后输出即可。如下:
vector<int>::reverse_iterator rit;
vector<int> ans,ansline;
ans.push_back(s);
if(quick.size()<=2)
{
printf("2\n");
printf("Take Line#%d from %04d to %04d.",G[s][d],s,d);
}
else
{
rit=quick.rbegin();
int nowline=G[*rit][*(rit+1)];
ansline.push_back(nowline);
for(;rit!=quick.rend()-2;rit++)
{
if(G[*rit][*(rit+1)]!=G[*(rit+1)][*(rit+2)])
{
ans.push_back(*(rit+1));
nowline=G[*(rit+1)][*(rit+2)];
ansline.push_back(nowline);
}
}
ans.push_back(d);
printf("%d\n",quick.size()-1);
vector<int>::iterator iter,iterline;
for(iter=ans.begin(),iterline=ansline.begin();iter!=ans.end(),iterline!=ansline.end();iter++,iterline++)
{
printf("Take Line#%d from %04d to %04d.\n",*iterline,*iter,*(iter+1));
}
}
至少跑一边样例是过了。= =
2、Dijsktra加DFS,邻接表法
由于1中开的邻接矩阵会溢出,因此选择使用邻接表优化。
去他大爷的邻接表。我改到最后改的心态爆炸。见下面这段代码:
for(;rit!=quick.rend()-2;rit++)
{
if(G[*rit][*(rit+1)]!=G[*(rit+1)][*(rit+2)])
{
ans.push_back(*(rit+1));
nowline=G[*(rit+1)][*(rit+2)];
ansline.push_back(nowline);
}
}
是用于从尾到头遍历最后选出来的路径,选出路径中所有的换乘站点的。注意他的判断,如果使用邻接表,那么则需要遍历*rit的vector,找到名为*(rit+1)的,然后再由*(rit+1)的,找到*(rit+2)的,可以得到两个line值,然后判断它们是否相等。这个简直麻烦到爆炸,也说明了我这个邻接表开得不好。去找一下使用邻接表做的。
Dijkstra加DFS,邻接表从这里看到的代码。
3、DFS
直接进行DFS。
我们看一下,一般求图论中的最短路径都是Dijkstra,那什么时候用Dijkstra什么时候用DFS呢?
Dijkstra一般适用于节点少于1k个,求单源最短路径的情况。虽然Dijkstra也可以保存路径,但是需要节点个数的vector。像本题,不知道节点个数,就得开一万个vector,这太扯淡了,每次都要初始化,时间复杂度完全无法接受。
那么如何使用DFS求最短路径呢?
其实和普通的DFS相比,就差一步,那就是在一个节点的所有出边都遍历完了之后,令他的vis重新为0。以下面这张图为例:
假设源点是1,目标点是5,
第一次dfs,vis[1]=1,访问2,vis[2]=1,访问4,vis[4]=1,访问5,得到一条路径。
到目标点之后就return,返回4,访问3,vis[3]=1,访问2,不行,访问1,不行,访问5,行,又找到一条路径。
3的所有出边遍历完了。vis[3]=0,返回4,4的也遍历完了,返回2,2可以访问3,3接着访问4,4访问5,又找到一条,返回4,返回3,3直接到5,又找到一条,返回3,返回2,返回1。
然后由1访问3。。。。。
这个vis数组,就是为了防止刚从一个节点出去,又访问回来的。它不会死循环,是因为最开始的节点总有把所有出边访问完全的时候,访问完了自然就退出递归了。
这也可以看出来,在边稠密的时候,这方法不是很好用。这也是为什么这道题可以用DFS直接做,因为它边少。
DFS代码如下:
void DFS(int s,int d,int len,int tra,int pre)
{
if(s==d)
{
temp.push_back(s*1000+pre);
if(len<lenmin)
{
lenmin=len;
tramin=tra;
ans=temp;
}
else if(len==lenmin)
{
if(tra<tramin)
{
ans=temp;
tramin=tra;
}
}
temp.pop_back();
return;
}
else
{
vis[s]=1;
for(int i=0;i<v[s].size();i++)
{
if(vis[v[s][i]]==0)
{
if(m[s*10000+v[s][i]]==pre)
{
DFS(v[s][i],d,len+1,tra,pre);
}
else
{
temp.push_back(s*1000+pre);
DFS(v[s][i],d,len+1,tra+1,m[s*10000+v[s][i]]);
temp.pop_back();
}
}
}
vis[s]=0;
}
}
由于长度和换乘站的数量都与递归次数相关,因此把它们都作为参数,同时还要一个参数保存“本个节点与上一个节点之间路径号”为了与“本个节点与下一个节点路径号”相比较。
当然为了直观方便,本题还用到了一个十分巧妙的技巧。即利用unordered_map记录两节点之间的线路号。由于节点都是四位,因此可以用一号节点*10000加二号节点得到一个八位数字,作为map的键,而路径号作为键值,这样无论是查找还是还原都十分方便。这一思想也用在了求路径上。
我的最短路径只保存了换乘站,那么由换乘站求路径其实挺麻烦的,因此用换乘站*1000+路径值也可以既储存换乘站又储存路径号,这样就很方便了。
输入数据如下:
vector<int> v[10010];//邻接表
unordered_map<long long int,int> m;//储存路径号
int N;
scanf("%d",&N);
for(int i=1;i<=N;i++)
{
int M;
scanf("%d",&M);
int head;
for(int j=0;j<M;j++)
{
int a;
scanf("%d",&a);
if(j!=0)
{
v[head].push_back(a);
v[a].push_back(head);
m[head*10000+a]=i;
m[a*10000+head]=i;
}
head=a;
}
}
对于unordered_map,就当做是一个很好用的hash表就可以。
输出如下:
int K;
scanf("%d",&K);
for(int i=0;i<K;i++)
{
int s,d;
scanf("%d %d",&s,&d);
temp.clear();
ans.clear();
lenmin=0x3f3f3f3f;
tramin=0x3f3f3f3f;
fill(vis,vis+10010,0);
//temp.push_back(s);
DFS(s,d,0,0,-1);
printf("%d\n",lenmin);
int head=s*1000;
for(int i=1;i<ans.size();i++)
{
printf("Take Line#%d from %04d to %04d.\n",ans[i]%1000,head/1000,ans[i]/1000);
head=ans[i];
}
}
最近越来越喜欢出这种一道题里面很多查询类的题目了。
三、总结
对于那种“节点序号是1-N”的题,直接Dijkstra(邻接矩阵)就可以了,但是这种没有顺序的题,使用DFS或者Dijkstra(邻接表)做。要留心这种x*1000+y的形式,存储三元关系还是很好用的。
PS:代码如下:
#include<stdio.h>
#include<cstdio>
#include<string>
#include<cstring>
#include<map>
#include<set>
#include<queue>
#include<vector>
#include<iostream>
#include<math.h>
#include<algorithm>
#include<unordered_map>
using namespace std;
vector<int> v[10010];//邻接表
unordered_map<long long int,int> m;//储存路径号
int vis[10010]={0};
vector<int> ans,temp;
int lenmin=0x3f3f3f3f;
int tramin=0x3f3f3f3f;
void DFS(int s,int d,int len,int tra,int pre)
{
if(s==d)
{
temp.push_back(s*1000+pre);
if(len<lenmin)
{
lenmin=len;
tramin=tra;
ans=temp;
}
else if(len==lenmin)
{
if(tra<tramin)
{
ans=temp;
tramin=tra;
}
}
temp.pop_back();
return;
}
else
{
vis[s]=1;
for(int i=0;i<v[s].size();i++)
{
if(vis[v[s][i]]==0)
{
if(m[s*10000+v[s][i]]==pre)
{
DFS(v[s][i],d,len+1,tra,pre);
}
else
{
temp.push_back(s*1000+pre);
DFS(v[s][i],d,len+1,tra+1,m[s*10000+v[s][i]]);
temp.pop_back();
}
}
}
vis[s]=0;
}
}
int main()
{
int N;
scanf("%d",&N);
for(int i=1;i<=N;i++)
{
int M;
scanf("%d",&M);
int head;
for(int j=0;j<M;j++)
{
int a;
scanf("%d",&a);
if(j!=0)
{
v[head].push_back(a);
v[a].push_back(head);
m[head*10000+a]=i;
m[a*10000+head]=i;
}
head=a;
}
}
int K;
scanf("%d",&K);
for(int i=0;i<K;i++)
{
int s,d;
scanf("%d %d",&s,&d);
temp.clear();
ans.clear();
lenmin=0x3f3f3f3f;
tramin=0x3f3f3f3f;
fill(vis,vis+10010,0);
//temp.push_back(s);
DFS(s,d,0,0,-1);
printf("%d\n",lenmin);
int head=s*1000;
for(int i=1;i<ans.size();i++)
{
printf("Take Line#%d from %04d to %04d.\n",ans[i]%1000,head/1000,ans[i]/1000);
head=ans[i];
}
}
}