“旅游规划”的题面如下:
有了一张自驾旅游路线图,你会知道城市间的高速公路长度、以及该公路要收取的过路费。
现在需要你写一个程序,帮助前来咨询的游客找一条出发地和目的地之间的最短路径。
如果有若干条路径都是最短的,那么需要输出最便宜的一条路径。
输入格式:
输入说明:输入数据的第1行给出4个正整数N、M、S、D,其中N(2≤N≤500)是城市的个数,顺便假设城市的编号为0~(N−1);
M是高速公路的条数;S是出发地的城市编号;D是目的地的城市编号。
随后的M行中,每行给出一条高速公路的信息,分别是:城市1、城市2、高速公路长度、收费额,中间用空格分开,数字均为整数且不超过500。
输入保证解的存在。
输出格式:
在一行里输出路径的长度和收费总额,数字间以空格分隔,输出结尾不能有多余空格。
输入样例:
4 5 0 3
0 1 1 20
1 3 2 30
0 3 4 10
0 2 2 20
2 3 1 20
输出样例:
3 40
这类给出点之间的关系并给出确定的起点,要求最短路径的问题,适用 Dijkstra 算法求解。
一般点使用序号表示,给出边长度 (这里是路程和花费) 的场合,可以使用邻接矩阵存储图的信息。
Dijkstra 算法保证访问包含起点在内且与起点连通的所有图中的点。每次访问一个点时,寻找当下所有未访问点中离起点最近的那一个,然后判断“如果通过现在访问的点走向那个最近点,会不会比原来的路径更短” 。完全访问所有与起点连通的点后,算法结束。
Dijkstra 算法的时间复杂度主要取决于“寻找当下未访问点中离起点最近的那一个”的方式。
Dijkstra 算法无法处理含负值边的图,存在负值边时会在图中形成一个负值圈,无法找到最短路 (因为加上一个负值总是会让被加的数更小)。
以下是使用 python 解题的代码。受限于算法表示和语言性能,测试点 "最大N和M, 随机完全图" 运行超时,如果使用 C/C++ 表示相同的逻辑则可以通过该测试点。
# 邻接矩阵描述图,有插入边的方法
class Graph:
INFINITY = 65535
# 初始化描述为一个值全为无穷大的空图
def __init__(self, vertex_num):
self.vertex_num = vertex_num
self.graph_far = []
self.graph_cost = []
for i in range(vertex_num):
self.graph_far.append([self.INFINITY]*vertex_num)
self.graph_cost.append([self.INFINITY]*vertex_num)
# 向无向图插入边传入的边信息包含在列表内
def insert_edge(self, edge):
start, end, long, cost = edge[0], edge[1], edge[2], edge[3]
self.graph_far[start][end] = long
self.graph_far[end][start] = long
self.graph_cost[start][end] = cost
self.graph_cost[end][start] = cost
class Dist:
def __init__(self, it_len, full_val=0):
self.body = []
for i in range(it_len):
self.body.append(full_val)
# 找一个在 dist 中存放路程最短的点,找不到则返回 INFINITY
def find_min_dist(graph, dist, collected):
min_vertex = graph.INFINITY
min_dist = graph.INFINITY
for ver in range(graph.vertex_num):
if not collected[ver] and dist.body[ver] < min_dist:
min_dist = dist.body[ver]
min_vertex = ver
return min_vertex
# dist 和 cost 分别描述路程和花费
def dijkstra(graph, dist, cost, path, start_city):
collected = [False]*graph.vertex_num
# 遍历图,记录从起点开始到其他点的代价并初始化路径
for i in range(graph.vertex_num):
dist.body[i] = graph.graph_far[start_city][i]
cost.body[i] = graph.graph_cost[start_city][i]
# 初始化路径,存在边意味着可以从起点出发到达
if dist.body[i] == graph.INFINITY:
path.body[i] = -1
else:
path.body[i] = start_city
# 起点的花费和路程都是0,设置起点为已访问
dist.body[start_city] = 0
cost.body[start_city] = 0
collected[start_city] = True
while True:
min_dist_point = find_min_dist(graph, dist, collected)
# 找不到下一个最短路径点时,说明可连通的点遍历完,算法结束
if min_dist_point == graph.INFINITY:
break
collected[min_dist_point] = True
for w in range(graph.vertex_num):
if not collected[w] and graph.graph_far[min_dist_point][w] < graph.INFINITY:
# 存在负边时不能完成算法
if graph.graph_far[min_dist_point][w] < 0:
return False
# 若 start_city -> min_dist_point -> w 短于 start_city -> w
if dist.body[min_dist_point] + graph.graph_far[min_dist_point][w] < dist.body[w]:
dist.body[w] = dist.body[min_dist_point] + graph.graph_far[min_dist_point][w]
cost.body[w] = cost.body[min_dist_point] + graph.graph_cost[min_dist_point][w]
path.body[w] = min_dist_point
# 出现新的路径长度相同但花费更少
elif dist.body[min_dist_point] + graph.graph_far[min_dist_point][w] == dist.body[w] and\
cost.body[min_dist_point] + graph.graph_cost[min_dist_point][w] < cost.body[w]:
dist.body[w] = dist.body[min_dist_point] + graph.graph_far[min_dist_point][w]
cost.body[w] = cost.body[min_dist_point] + graph.graph_cost[min_dist_point][w]
path.body[w] = min_dist_point
return True
def main():
city_num, road_num, start_city, end_city = (int(x) for x in input().split())
graph = Graph(city_num)
for i in range(road_num):
graph.insert_edge(list(map(int, input().split())))
dist = Dist(city_num)
cost = Dist(city_num)
path = Dist(city_num)
if dijkstra(graph, dist, cost, path, start_city):
print(dist.body[end_city], cost.body[end_city])
else:
print("Dijkstra fail.")
main()
扩展:
使用一系列点描述图时,比如给出一系列点在平面或者空间内的坐标,而边被描述为点之间连通的条件 (直观起见,比如当两个点之间距离足够近,就认为它们是连通的),这时给定起点依然可以使用 Dijkstra 算法计算从起点出发到各处的最短路径。
以 "Saving James Bond - Hard Version" 为例 (我在这里记录了这道题目和它的打开方式),下述为使用最短路径算法计算詹姆斯最短逃脱路径的代码描述:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <stdbool.h>
#define ERROR -404
#define THIS_INFINITY 65535
#define WIDTH_LIMIT 50 //横向坐标最大绝对值
#define HIGH_LIMIT 50 //纵向坐标最大绝对值
#define HALF_ISLAND 7.5 //岛屿半径为 15/2 = 7.5
/*队列和栈, 声明与函数*/
struct node
{
int* array;
int capacity;
int size, head, tail;
bool inserted;
};
typedef struct node* List;
List NewList(int lenth)
{
List L = (List)calloc(1, sizeof(struct node));
L->capacity = lenth;
L->size = 0, L->head = 0; L->tail = 0, L->inserted = false;
L->array = (int*)calloc(L->capacity, sizeof(int));
return L;
}
List DelList(List L)
{
free(L->array);
L->array = NULL;
free(L);
L = NULL;
}
bool EnQueue(List L, int freash)
{
if (L->head == L->tail && L->inserted)
return false;
L->array[L->head] = freash;
L->head = (L->head + 1) % L->capacity;
L->inserted = true;
return true;
}
int PopQueue(List L)
{
if (L->head == L->tail && !L->inserted)
return ERROR;
int popCell = L->array[L->tail];
L->tail = (L->tail + 1) % L->capacity;
L->inserted = false;
return popCell;
}
bool EnStack(List L, int freash)
{
if (L->size == L->capacity)
return false;
L->array[L->size] = freash;
L->size++;
return true;
}
int PopStack(List L)
{
if (L->size == 0)
return ERROR;
L->size--;
return L->array[L->size];
}
/*队列和栈, 声明与函数结束*/
struct croNode
{
int x;
int y;
};
typedef struct croNode* Crocodile;
struct mapNode
{
Crocodile crocodiles; //鳄鱼坐标的列表, 零号存放原点
int croNum; //鳄鱼数量
int jumpStep; //每次跳跃最大距离
int* path; //路径保存为经过的上一点的编号
int* dist; //从原点到每条鳄鱼的距离
bool* safe; //标记能安全逃出的点
};
typedef struct mapNode* Map;
//建立一个自定义 Map 实体, 传入图内点数量和跳跃距离
Map NewMap(int crocodilesNum, int jumpDistance)
{
Map M = (Map)calloc(1, sizeof(struct mapNode));
M->croNum = crocodilesNum;
M->jumpStep = jumpDistance;
M->path = (int*)calloc(M->croNum, sizeof(int));
M->dist = (int*)calloc(M->croNum, sizeof(int));
M->safe = (bool*)calloc(M->croNum, sizeof(bool));
M->crocodiles = (Crocodile)calloc(M->croNum, sizeof(struct croNode));
return M;
}
//释放 Map 实体所指空间
void DelMap(Map M)
{
free(M->crocodiles); M->crocodiles = NULL;
free(M->path); M->path = NULL;
free(M->dist); M->dist = NULL;
free(M->safe); M->safe = NULL;
free(M); M = NULL;
return;
}
//jump, a to b. 能跳意味着两点连线是一条边
bool CanJump(const struct croNode a, const struct croNode b, const int jumpStep)
{
int dx = b.x - a.x;
int dy = b.y - a.y;
double jump = jumpStep;
if (a.x == 0 && a.y == 0)
{
jump += HALF_ISLAND;
if (pow(b.x, 2) + pow(a.x, 2) <= pow(HALF_ISLAND, 2)) //鳄鱼在小岛内
return false;
}
return (jump*jump >= dx*dx + dy*dy);
}
//能够跳到边缘逃出
bool CanEscape(const struct croNode point, const int jumpStep)
{
return (abs(point.x) >= (WIDTH_LIMIT - jumpStep) || abs(point.y) >= (HIGH_LIMIT - jumpStep));
}
//返回两点距离, 结果四舍五入取整
int FarAtoB(const struct croNode a, const struct croNode b)
{
int dx = b.x - a.x;
int dy = b.y - a.y;
double far = 0.5;
far += sqrt((double)dx*dx + dy*dy);
return (int)far;
}
//从原点开始广度优先遍历,写入对应每条鳄鱼的 dist, path 和 safe 的值
bool BFS(Map map, int start)
{
bool safeProbably = false;
int i;
for (i = 0; i < map->croNum; i++)
{
map->dist[i] = THIS_INFINITY;
map->path[i] = ERROR;
}
map->dist[start] = 0;
map->path[start] = -1;
map->crocodiles[start].x = 0;
map->crocodiles[start].y = 0;
bool* visited = (bool*)calloc(map->croNum, sizeof(bool));
List collect = NewList(map->croNum);
visited[start] = true;
EnQueue(collect, start);
int v, w;
while (collect->head != collect->tail || collect->inserted) //Queue "collect" not empty
{
v = PopQueue(collect);
for (w = 1; w < map->croNum; w++)
{
if (CanJump(map->crocodiles[v], map->crocodiles[w], map->jumpStep) && !visited[w])
{
EnQueue(collect, w);
map->path[w] = v;
map->dist[w] = map->dist[v] + FarAtoB(map->crocodiles[v], map->crocodiles[w]);
visited[w] = true;
//printf("->path[%d] = %d", w, map->path[w]);
}
if (CanEscape(map->crocodiles[w], map->jumpStep))
{
map->safe[w] = true;
//printf("point %d is safe(%d).\n", w, map->safe[w]);
safeProbably = true;
}
}
}
free(visited);
DelList(collect);
return safeProbably;
}
//选取当下未选中过的 dist 值最小的下标
int ScanMinDist(Map map, bool collect[])
{
int minVer, v;
int minDist = THIS_INFINITY;
for (v = 0; v < map->croNum; v++)
{
if (!collect[v] && map->dist[v] < minDist)
{
minVer = v;
minDist = map->dist[v];
}
}
return (minDist < THIS_INFINITY)? minVer: ERROR;
}
//自定义的寻找最短路径函数, 改变了 map->dist 和 map->path, 返回路径尾或ERROR
int FindPath(Map map, int start)
{
if (!BFS(map, start))
{
return ERROR;
}
bool* collect = (bool*)calloc(map->croNum, sizeof(bool));
collect[start] = true;
int v, w;
while (1)
{
v = ScanMinDist(map, collect);
if (v == ERROR)
break;
collect[v] = true;
for (w = 0; w < map->croNum; w++)
{
if (!collect[w] && CanJump(map->crocodiles[v], map->crocodiles[w], map->jumpStep))
{
if (FarAtoB(map->crocodiles[v], map->crocodiles[w]) < 0)
{
free(collect);
return ERROR;
}
if (map->dist[v] + FarAtoB(map->crocodiles[v], map->crocodiles[w]) < map->dist[w])
{
map->dist[w] = map->dist[v] + FarAtoB(map->crocodiles[v], map->crocodiles[w]);
map->path[w] = v;
}
}
}
}
free(collect);
int pathTail = ERROR;
int tailMinDist = THIS_INFINITY;
for (v = 0; v < map->croNum; v++)
{
if (map->safe[v])
{
if (map->dist[v] < tailMinDist)
{
tailMinDist = map->dist[v];
pathTail = v;
}
}
//printf("path[%d] = %d\n", v, map->path[v]);
//printf("dist[%d] = %d\n", v, map->dist[v]);
}
return pathTail;
}
int main(void)
{
int n, d;
scanf("%d %d", &n, &d); //鳄鱼数量, 跳跃距离
getchar();
if(d >= 50) //能一步跳到岸的情形
{
printf("1\n");
exit(0);
}
Map map = NewMap(n+1, d);
const int start = 0; //零号位保存原点坐标, 为岛心
int xc, yc;
int count;
for(count = 1; count <= n; count++) //输入鳄鱼坐标, 序号 1~n
{
scanf("%d %d", &xc, &yc);
getchar();
map->crocodiles[count].x = xc;
map->crocodiles[count].y = yc;
}
List istack = NewList(n+1);
int pathTail = FindPath(map, start);
if (pathTail == ERROR)
{
printf("0");
}
else
{
int pathStep = pathTail;
int cell;
while (pathTail != start)
{
EnStack(istack, pathTail);
pathTail = map->path[pathTail];
}
printf("%d\n", istack->size+1); //显示跳跃步数
while(istack->size != 0)
{
cell = PopStack(istack);
printf("%d %d\n", map->crocodiles[cell].x, map->crocodiles[cell].y);
}
}
DelList(istack);
DelMap(map);
return 0;
}
测试用例输入和输出如下:
用例1的输入:
17 1510 -21
10 21
-40 10
30 -50
20 40
35 10
0 -10
-25 22
40 -40
-30 30
-10 22
0 11
25 21
25 10
10 10
10 35
-30 10
原题目用例1的输出:4
0 11
10 21
10 35
使用上述方法得到的用例1的输出:
4
10 10
25 10
35 10
用例2的输入:
4 13
-12 12
12 12
-12 -12
12 -12
用例2的输出:0
根据比较原本用例1的输出结果和实际得到的对应用例1的输出结果,使用最短路算法可以得到一条总距离相似,甚至更短的逃脱路径。而通过对比上文中 BFS() 函数和 FindPath() 函数的逻辑也可以发现 Dijkstra 算法遍历图的行为与广度优先遍历相似。