代码随想录算法训练营第62天 | 1、小明的逛公园,2、骑士的攻击

目录

1、小明的逛公园

2、骑士的攻击


1、小明的逛公园

题目描述

小明喜欢去公园散步,公园内布置了许多的景点,相互之间通过小路连接,小明希望在观看景点的同时,能够节省体力,走最短的路径。 

给定一个公园景点图,图中有 N 个景点(编号为 1 到 N),以及 M 条双向道路连接着这些景点。每条道路上行走的距离都是已知的。

小明有 Q 个观景计划,每个计划都有一个起点 start 和一个终点 end,表示他想从景点 start 前往景点 end。由于小明希望节省体力,他想知道每个观景计划中从起点到终点的最短路径长度。 请你帮助小明计算出每个观景计划的最短路径长度。

输入描述

第一行包含两个整数 N, M, 分别表示景点的数量和道路的数量。 

接下来的 M 行,每行包含三个整数 u, v, w,表示景点 u 和景点 v 之间有一条长度为 w 的双向道路。 

接下里的一行包含一个整数 Q,表示观景计划的数量。 

接下来的 Q 行,每行包含两个整数 start, end,表示一个观景计划的起点和终点。

输出描述

对于每个观景计划,输出一行表示从起点到终点的最短路径长度。如果两个景点之间不存在路径,则输出 -1。

输入示例

7 3
2 3 4
3 6 6
4 7 8
2
2 3
3 4

输出示例

4
-1

提示信息

从 2 到 3 的路径长度为 4,3 到 4 之间并没有道路。

1 <= N, M, Q <= 1000.

1 <= w <= 10000.

思路:这道题是求多源最短路径,所以介绍以下Floyd算法。

不同于之前学的dijsktra算法求单源最短路径,这里使用Floyd来求多源最短路径。

核心思想就是使用动态规划的思路,将从i到j的最短路径分成i到k的最短路径以及k到j的最短路径之和。当然每次得从里面选择最短的距离。

这里采用了三维空间来记录路径,因为i到j的路径上可能存在有1到k的结点作为中间过渡结点的情况,需要依次进行比较,最终取最短值。

注意这里的初始化以及遍历顺序,初始化就相当于在三维空间中,将k固定,i、j平面的grid不断比较更新得到最短路径,所以当前层的k会使用到它的前一层k-1,因为它的k-1已经是前面情况里面最短的情况了。

#include<iostream>
#include<vector>
using namespace std;

int main(){
    int n, m, u, v, w, q, start, end;
    while(cin >> n >> m){
        //定义邻接矩阵来存储图,三维存储
        //dp[i][j][k]表示从i到j的最短路径,以1到k区间的结点作为中间结点过渡
        vector<vector<vector<int>>> grid(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, 10005)));
        for(int i = 0; i < m; i ++){
            cin >> u >> v >> w;
            grid[u][v][0] = w;
            grid[v][u][0] = w;//注意这里是双向图,可以把它看作无向图,两个结点到达对方的距离一样
        }
        
        for(int k = 1; k <= n; k ++){
            for(int i = 1; i <= n; i ++){
                for(int j = 1; j <= n; j ++){
                    //当i到j的最短路径不经过k时,那么就等于前面i到k-1的最短路径,然后k-1到j的最短路径,即grid[i][j][k-1],这是之前已经计算出来的最短距离
                    //当i到j的最短路径经过k时,那么就等于i到k的最短路径,以k-1作为中间过渡点,加上k到j的最短路径,以k-1作为过渡点
                    grid[i][j][k] = min(grid[i][j][k - 1], grid[i][k][k - 1] + grid[k][j][k - 1]);
                }
            }
        }
        
        cin >> q;
        for(int i = 0; i < q; i ++){
            cin >> start >> end;
            if(grid[start][end][n] == 10005) cout << -1 << endl;
            else cout << grid[start][end][n] << endl;
        }
    }
}

 当然还可以对空间进行优化,如下所示。

#include<iostream>
#include<vector>
using namespace std;

int main(){
    int n, m, u, v, w, q, start, end;
    while(cin >> n >> m){
        //对空间进行优化,定义邻接矩阵来存储图,二维存储
        vector<vector<int>> grid(n + 1, vector<int>(n + 1, 10005));
        for(int i = 0; i < m; i ++){
            cin >> u >> v >> w;
            grid[u][v] = w;
            grid[v][u] = w;//注意这里是双向图,可以把它看作无向图,两个结点到达对方的距离一样
        }
        
        for(int k = 1; k <= n; k ++){
            for(int i = 1; i <= n; i ++){
                for(int j = 1; j <= n; j ++){
                    //当i到j的最短路径不经过k时,那么就等于之前的值grid[i][j]
                    //当i到j的最短路径经过k时,那么就等于i到k的最短路径加上k到j的最短路径,即grid[i][k]+grid[k][j]
                    grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]);
                }
            }
        }
        
        cin >> q;
        for(int i = 0; i < q; i ++){
            cin >> start >> end;
            if(grid[start][end] == 10005) cout << -1 << endl;
            else cout << grid[start][end] << endl;
        }
    }
}

2、骑士的攻击

题目描述

在象棋中,马和象的移动规则分别是“马走日”和“象走田”。现给定骑士的起始坐标和目标坐标,要求根据骑士的移动规则,计算从起点到达目标点所需的最短步数。

棋盘大小 1000 x 1000(棋盘的 x 和 y 坐标均在 [1, 1000] 区间内,包含边界)

输入描述

第一行包含一个整数 n,表示测试用例的数量,1 <= n <= 100。

接下来的 n 行,每行包含四个整数 a1, a2, b1, b2,分别表示骑士的起始位置 (a1, a2) 和目标位置 (b1, b2)。

输出描述

输出共 n 行,每行输出一个整数,表示骑士从起点到目标点的最短路径长度。

输入示例

6
5 2 5 4
1 1 2 2
1 1 8 8
1 1 8 7
2 1 3 3
4 6 4 6

输出示例

2
4
6
5
1
0

提示信息

骑士移动规则如图,红色是起始位置,黄色是骑士可以走的地方。

思路:一般看到这种最短路径的问题,都会第一时间想到广搜,因为广搜所得到的到达目标的路径一定是最短的。

所以如下所示为采用广搜方法的代码。

#include<iostream>
#include<queue>
#include<string.h>
using namespace std;

//dir记录可能的方向
int dir[8][2] = {-1, -2, -2, -1, -2, 1, -1, 2, 1, 2, 2, 1, 2, -1, 1, -2};
//moves数组记录步数
int moves[1005][1005];
void bfs(int ax, int ay, int bx, int by){
    queue<int> que;
    que.push(ax);
    que.push(ay);
    while(!que.empty()){
        int curx = que.front(); que.pop();
        int cury = que.front(); que.pop();
        if(curx == bx && cury == by) break;//找到了目标结点,直接break终止循环
        for(int i = 0; i < 8; i ++){
            int nextx = curx + dir[i][0];
            int nexty = cury + dir[i][1];
            if(nextx < 1 || nextx > 1000 || nexty < 1 || nexty > 1000) continue;
            if(moves[nextx][nexty] == 0){
                moves[nextx][nexty] = moves[curx][cury] + 1;
                que.push(nextx);
                que.push(nexty);
            }
        }
    }
}

int main(){
    int n, a1, a2, b1, b2;
    cin >> n;
    while(n --){
        cin >> a1 >> a2 >> b1 >> b2;
        memset(moves, 0, sizeof(moves));//每次计算起点到终点的最短路时都需要将move数组初始化为0,以免受到上次结果的干扰
        bfs(a1, a2, b1, b2);
        cout << moves[b1][b2] << endl;
    }
}

 当提交上述代码后,会发现明显超时了,原因就是因为广搜漫无目的的走过了太多没有必要走的区域,导致起始点与终点距离增大,以及n的次数增大时,会出现超时问题。

所以这里介绍使用A*算法来让这个搜索更加有方向性。

A*算法的关键在于启发式函数,启发式函数使用得好,那解决问题的效率也就增大了。

这里对每一个结点采用赋权的操作,由g部分(源点到当前结点的距离)以及h部分(当前结点到终止点的距离)组成,因为这两部分能够让搜索更加有指向性,朝着越来越靠近终点的方向前进。

为了更好地找到这样的点,所以需要对放入队列的元素按f排序,所以需要用到优先队列,每次从队列里面取的时候也是取的f最小的,从而能够大大简化搜索效率。

#include<iostream>
#include<queue>
#include<string.h>
using namespace std;

//dir记录可能的方向
int dir[8][2] = {-1, -2, -2, -1, -2, 1, -1, 2, 1, 2, 2, 1, 2, -1, 1, -2};
//moves记录步数
int moves[1005][1005];

//启发式函数中权值f = g + h;
//其中g等于起始点到当前结点的距离;
//h等于当前结点到终点的距离
//注意这里的距离采用的是欧拉距离
//并且距离没有开根号,因为想要最大限度保证精度

struct Knight{
    int x, y;
    int f, g, h;
    //重载小于运算符,实现从小到大的排列
    //注意const需要添加的地方,少了的话可能会编译不成功
    bool operator < (const Knight& k) const{
       return k.f < f;
    }
};

priority_queue<Knight> pq;//定义优先队列,使用自定义Knight作为参数类型

//计算欧拉距离,注意没有开根号
int distance(Knight& a, Knight& b){
    return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
    
}


void A_star(Knight b){
    while(!pq.empty()){
        Knight cur = pq.top(); pq.pop();
        if(cur.x == b.x && cur.y == b.y) break;//找到了目标结点,直接break终止循环
        for(int i = 0; i < 8; i ++){
            Knight next;
            next.x = cur.x + dir[i][0];
            next.y = cur.y + dir[i][1];
            if(next.x < 1 || next.x > 1000 || next.y < 1 || next.y > 1000) continue;
            if(moves[next.x][next.y] == 0){
                moves[next.x][next.y] = moves[cur.x][cur.y] + 1;
                
                //开始将结点加入优先队列
                next.g = cur.g + 5;//因为骑士每一步都是走的日字,所以直接在原来基础上加上5即可
                next.h = distance(next, b);
                next.f = next.g + next.h;
                pq.push(next);
            }
        }
    }
}

int main(){
    int n, a1, a2, b1, b2;
    cin >> n;
    while(n --){
        cin >> a1 >> a2 >> b1 >> b2;
        memset(moves, 0, sizeof(moves));//每次计算起点到终点的最短路时都需要将move数组初始化为0,以免受到上次结果的干扰
        //开始对输入进行初始化
        Knight a, b;
        a.x = a1; a.y = a2;
        b.x = b1; b.y = b2;
        a.g = 0;
        a.h = distance(a, b);
        a.f = a.g + a.h;
        pq.push(a);
        A_star(b);
        while(!pq.empty()) pq.pop();//这里需要在每一次A_star结束后将pq队列清空,否则会对后面的数据产生影响
        cout << moves[b1][b2] << endl;
    }
}

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值