PAT (Advanced Level) Practice 1131 Subway Map DFS遍历图找最短路径

一、概述

图论求最短路径问题。

采用三种方法分别解决。

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];
		}
	}
 } 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值