Dijkstra 算法,“旅游规划”,另一种方式拯救詹姆斯邦德

“旅游规划”的题面如下:

有了一张自驾旅游路线图,你会知道城市间的高速公路长度、以及该公路要收取的过路费。
现在需要你写一个程序,帮助前来咨询的游客找一条出发地和目的地之间的最短路径。
如果有若干条路径都是最短的,那么需要输出最便宜的一条路径。

输入格式:
输入说明:输入数据的第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 15
10 -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 算法遍历图的行为与广度优先遍历相似。

猜你喜欢

转载自blog.csdn.net/asura319/article/details/79400966
今日推荐