这题输入地铁路线图,图中没有自环,但是有环。
地铁线有交叉点,即中转站。 但是一个中转站最多只有5条线交叉
重要的信息 Each station interval belongs to a unique subway line, 也就是相邻站点的这一段线路只可能属于一条线。
输出:
最优路线站数 (站数最少,尽量换乘最少)
Take Line#3 from 1306 to 2302.
只需要起点终点中转点的 id
这里需要找最短路,图的规模不大,100*100最多。用BFS,DFS,Dijkstra 都行。
还有一个问题是如何判断搭乘哪条线?单纯依靠站点本身是否为换乘站是不行的。但是可以通过当前站和下一站来判断目前在哪条线。
BFS的话,在搜索过程中,节点被标记为三种状态, visited, visiting, unvisit。
如果直接把访问过的节点标记为 visited的话,会阻止同层搜索这个节点。导致同样站数换乘更少的解法被略去了。
我采取更加灵活的方式,通过跟踪 level, level_last 两个变量判断现在搜索的第几层。访问过的节点保存的是在第几层搜索过。
当前搜索的 level > 节点level,说明已访问; 相等则正在访问,小于则未访问。 所以初始化的时候要把节点level设置无穷大。
在搜索到一个可行的走法的时候,为了获得路径,需要从终点向前沿着父节点找到起点。 因此在搜索过程中,需要当前节点需要保存指向父节点的下标。
#include <cstdio>
#include <vector>
#include <map>
#include <cstring>
#define infinite 10000000
using namespace std;
vector<int> graph[10000];
map< int, int > stop_line;
int visited[10000]; // 记录节点被BFS的哪个层次访问过
vector<int> ans;
struct node{ //bfs 队列中的节点,previous记录前节点的下标
int stop_id, previous;
};
// 这里获得的 path 是倒着的,终点在0, 起点在最后
void get_path_from( const vector<node>& q, int end, vector<int>& path )
{
while( end >= 0 )
{
path.push_back( q[end].stop_id );
end = q[end].previous;
}
}
int find_next_transfer( const vector<int> &a, int pos )
{
int line_1 = stop_line[ (a[pos] << 16) + a[pos-1] ];
while ( --pos > 0 )
{
int line_2 = stop_line[ (a[pos] << 16) + a[pos-1] ];
if( line_1 != line_2 ) return pos;
line_1 = line_2;
}
return pos; //没有transfer
}
int count_transfer( vector<int>& path )
{
int count = 0;
for( int i=path.size()-1; i>0; i=find_next_transfer(path, i) )
++count;
return count;
}
void BFS( int st, int ed )
{
memset( visited, 0x7f, sizeof(visited) );
ans.clear();
vector< node > q;
int front=0; //队列头
int level_last=0, level=1, best_level=infinite, transfer_count=infinite; //层次遍历中某一层的最后一个
q.push_back( {st, -1} ); //起点的前节点标记-1
visited[ st ] = level;
while( front < q.size() )
{
// printf("front=%d q[front]=%d level=%d last=%d\n", front, q[front].stop_id, level, level_last );
if ( q[front].stop_id == ed ) //找到可行解, 和之前的比较一下优劣, 可能先找到换乘数较多的路线
{
vector<int> possible_solution;
get_path_from( q, front, possible_solution );
auto tmp = count_transfer( possible_solution ) ;
if ( tmp < transfer_count )
{
best_level=level;
transfer_count=tmp;
swap( possible_solution, ans );
}
}
else //还没到终点,继续搜索
for( int i=0; i<graph[ q[front].stop_id ].size(); i++ )
{
auto& cur_stop = graph[ q[front].stop_id ];
if ( visited[ cur_stop[i] ] < level ) continue; //当前节点被前一层的访问过,那就没必要继续搜。 如果当前节点仅被同层的搜过,还是可以搜的
visited[ cur_stop[i] ] = level;
q.push_back( {cur_stop[i], front} );
}
if ( level_last == front ) //上一层的最后一个节点也被搜了,说明接下来就是新一层的,队列中最后那个就是新层的最后
{
level_last = q.size()-1;
level++;
if ( level > best_level ) break; //不可能还有更优解了,直接走人
}
++front;
}
}
void print( const vector<int> &a )
{
printf("%zd\n", a.size()-1);
// Take Line#X1 from S1 to S2.
int pos = a.size()-1, next_pos;
for( next_pos = find_next_transfer( a, pos );
pos>0;
next_pos = find_next_transfer( a, next_pos ) )
{
printf( "Take Line#%d from %04d to %04d.\n", stop_line[ (a[pos]<<16) + a[pos-1]] , a[pos], a[next_pos] );
pos = next_pos;
}
}
int main()
{
int N;
scanf("%d", &N);
for ( int i=0, m, st; i<N; i++ )
{
scanf("%d %d", &m, &st); // 祈祷不会有 M = 0 的情况
for ( int j=1, next; j<m; j++ )
{
scanf("%d", &next);
graph[ st ].push_back( next );
graph[ next ].push_back( st );
stop_line.insert( { (st<<16) + next, i+1 } ); //记录 两个相邻站点属于哪条线
stop_line.insert( { (next<<16) + st, i+1 } );
st = next;
}
}
int k, st, ed;
scanf("%d", &k);
while( k-- )
{
scanf("%d %d", &st, &ed);
BFS( st, ed );
print( ans );
}
}
/* test case
4
7 1001 3212 1003 1204 1005 1306 7797
9 9988 2333 1204 2006 2005 2004 2003 2302 2001
13 3011 3812 3013 3001 1306 3003 2333 3066 3212 3008 2302 3010 3011
4 6666 8432 4011 1306
3
6666 3212
6666 2001
9988 2001
----------------------------
2
4 1 2 3 4
6 5 2 6 7 3 8
4
5 8
----------------------
6
3 1 2 3
3 1 4 5
3 1 6 7
3 1 8 9
4 2 4 6 8
3 2 5 7
5
*/
网上看到了别人不同的思路:
BFS之中用一个优先队列,队列中的节点会保存到达该节点时的距离和换线数目。因此搜到的第一个解就是最优解了,比我的做法更好,无需搞这么多复杂的标记、level。
还有下面这种用SPFA的最短路算法,类似 BFS + Dijkstra 的混合体,有类似BFS的两层循环和队列,有Dijkstra的松弛操作。
#include<cstdio>
#include<vector>
#include<cstring>
#include<queue>
using namespace std;
vector<int>e[10005], line[10005],s2l[10005];
int L, Q,n,u,v,l,start,ed,dis[10005],preS[10005],preL[10005],cnt[10005],tmp;
void add(int a, int b, int c)
{
e[a].push_back(b);
e[b].push_back(a);
line[a].push_back(c);
line[b].push_back(c);
}
void spfa(int s, int d)
{
memset(dis, 0x3f, sizeof(dis));
memset(preS, -1, sizeof(preS));
memset(preL, -1, sizeof(preL));
memset(cnt, 0x3f, sizeof(cnt));
dis[s] = 0;
cnt[s] = 0;
queue<int>q;
q.push(s);
while (!q.empty())
{
u = q.front();
q.pop();
for (int i = 0; i < e[u].size(); i++)
{
v = e[u][i];
l = line[u][i];
if (dis[v] > dis[u] + 1)
{
dis[v] = dis[u] + 1;
preS[v] = u;
preL[v] = l;
if (preL[v] != preL[u])
cnt[v] = cnt[u] + 1;
else
cnt[v] = cnt[u];
q.push(v);
}
else if (dis[v] == dis[u] + 1)
{
if (preL[u] != l)
{
tmp = cnt[u] + 1;
}
else
{
tmp = cnt[u];
}
if (tmp <=cnt[v])
{
dis[v] = dis[u] + 1;
preS[v] = u;
preL[v] = l;
if (preL[v] != preL[u])
cnt[v] = cnt[u] + 1;
else
cnt[v] = cnt[u];
q.push(v);
}
}
}
}
printf("%d\n", dis[d]);
u = d;
int curL=preL[d],tmpS=d;
for (int i = d; i != s; i = preS[i])
{
l = preL[i];
if (l != curL)
{
printf("Take Line#%d from %04d to %04d.\n", curL, tmpS, i);
curL = preL[i];
tmpS = i;
}
}
printf("Take Line#%d from %04d to %04d.\n", curL, tmpS, s);
}
int main()
{
scanf("%d", &L); //while (L ==9);//
for (int i = 1; i <= L; i++)
{
scanf("%d", &n);
scanf("%d", &u);
//start = u;
for (int j = 1; j < n; j++)
{
scanf("%d", &v);
add(u, v, i);
u = v;
}
}//printf("Q");
scanf("%d", &Q); //while (Q < 5);
for (int i = 0; i < Q; i++)
{
scanf("%d%d", &start, &ed);
if (start != ed)
spfa(ed, start);
else
printf("0\n");
}//while (1);
}
BFS框架:
初始化数据{ 起点入队, 标记, 记录层次,第一层的last元素下标 }
遍历队列 {
找到解 / 头节点拓展入队,做标记,记录每个节点被访问层次
更新level,level_last。
下一个队头
}
BFS的变体:
1、需要记录路径, 那么队列中保存父节点的下标。
2、不止要找一条最短路,而是找出全部最短路。
3、双向bfs。 用两个队列,分别从起点 终点开始搜索。另外用一个数组记录到达节点步数,是哪个方向的遍历到达的。
当两队列有交集,就是搜索到了。
4、迭代加深的搜索,避免搜索的分支太多,占空间太多。
5、A* 这种启发式搜索,本质上统一了dfs和bfs,bfs和dfs只不过是估价函数的不同而已。