Robocom-2021复赛-塔防游戏

有一种简单的塔防游戏是这样的:给定一张由 n 行 m 列个方格子构成的地图,玩家可以任选一个格子放置自己的大本营,还可以在任意一个格子里放置自己的防御堡垒。大本营和每个防御堡垒都有自己的防御能力值 d,表示可以抵御 d 个僵尸的攻击。每一轮游戏开始时,玩家在规定时间内将本级别可以用的防御堡垒布置在地图中,然后僵尸们就从地图边界涌入地图中,向着大本营发起攻击。每轮进攻持续一个固定的时长,结束后剩余的僵尸就原地蒸发

每队僵尸可以向一个方格的上下左右四个方向移动。如果相邻的目标方格没有堡垒,它们就可以用 1 秒的时间移动过去,否则会被堡垒阻挡或者消灭。对每一队僵尸(从同一地点出发的所有僵尸)而言,每秒会被堡垒消灭 1 个队友,同时消耗掉该堡垒 1 个单位的防御能力。当防御能力降为 0,则该堡垒消失,剩下的僵尸则用 1 秒移动到这个方格继续行进注意:如果有多支僵尸队都进入了同一个方格,它们并不会合并成一支队伍。

所有的僵尸队都会根据进攻开始时的地图选择被歼灭最少的到达大本营的路线,并且一直按照这个路线行进,中途不因为地图状态的改变而改变。当这样的进攻路径不唯一时,选择能最快到达大本营的路径。题目保证这样的路径所打掉的堡垒的布局是唯一的。

本题就要求你计算出一轮攻击结束时,地图上的布局情况。

输入格式:

输入首先在第一行中给出三个正整数:不超过 100 的 n 和 m,为地图的尺寸;不超过 1000 的 T,为一轮攻击持续的时长。

随后给出 n+2 行,每行给出 m+2 个数字,每行中的数字都用空格分隔,表示攻击开始前地图上的布局。其中第 1 行、第 1 列、第 n+2 行、第 m+2 列是地图边界外僵尸们出发的位置,这些位置上,0 表示没有僵尸,其他正整数表示从该位置出发的僵尸们的数量。而地图中的每个位置上,0 表示没有堡垒,其它正整数表示该位置上堡垒的防御能力值。大本营是一个特殊的建筑,我们用一个负数 −D 表示这里是大本营,其防御能力值为 D。这里的防御值和任一队僵尸的数量都不超过 100。

注意:僵尸不可在地图边界外移动,它们的第一个移动目标必须在地图中,所以四个角落里出现的僵尸可以被忽略,因为它们没有进入地图的途径。

输出格式:

输出 n 行,每行 m 个数字,对应攻击结束后地图上每个方格的状态。状态的表示与输入相同:没有堡垒的地方输出 0,有堡垒的地方输出其剩余防御值,大本营的位置上输出其剩余防御值的负值。

注意每行数字间以 1 个空格分隔,行首尾不得有多余空格。

当大本营被攻陷时,游戏即刻结束。此时应输出结束时的地图状态,并且在最后一行输出一句 Game Over

输入样例 1:

7 5 17
0 0 0 0 13 0 0
0 0 0 0 0 0 0
0 0 0 8 0 0 0
0 0 0 0 2 1 0
0 0 0 7 5 3 0
8 0 1 4 -10 1 0
0 0 0 3 3 0 0
0 0 8 0 9 0 0
0 0 0 4 0 0 0

输出样例 1:

0 0 0 0 0
0 0 8 0 0
0 0 0 2 0
0 0 7 5 0
0 0 0 -1 0
0 0 0 2 0
0 8 0 9 0

样例说明:

地图布局如下图所示。

规模为 13 和 8 的两队僵尸都有两种选择,攻打蓝色或者紫色堡垒都是消耗最少的。在这种情况下,规模为 13 的僵尸队走蓝色比较快,需要 1+1+1+2+4+2=11 秒到达大本营边上;规模为 8 的僵尸队走紫色比较快,需要 1+2+5=8 秒到达大本营边上。

规模为 4 的僵尸队比较惨,只能选择绿色堡垒,最后被大本营边上的绿色堡垒消灭。注意到在攻击过程中,其实它们可以等到紫色堡垒被攻陷之后走紫色原始值为 4 的方格,但是因为路径是在初始状态下选定就不能改的,所以它们不能这样选择。

攻打大本营时,规模为 8 的僵尸队剩下了 3 只先到达,在第 11 秒被大本营消灭。此时大本营还剩 7 个单位的防御值,同时规模为 13 的僵尸队剩下的 8 只进入了大本营相邻的方格,开始攻击。但此时距离本轮结束只剩 6 秒,结果大本营在结束时还剩 1 个单位的防御值,玩家胜。


一、官方解答

        首先因为起点不一而终点一致,我们可以做从起点出发的最短路,确定每队僵尸的行走路线。最短路因为是网格图,建议还是用带优先队列的 Dijkstra,不容易被卡复杂度,标程里用的是类似 SPFA 的做法,在特别构造的情况下会被卡复杂度。做最短路时需要记录下最短路的路径。

        然后按照时间移动僵尸即可。我的做法是直接维护僵尸队在的位置,因为不会合并,所以每次扫一次所有僵尸队伍,对于还活着的僵尸队伍,尝试向最短路路径上的下一个点移动一步,如果有堡垒挡住就打一下堡垒即可。

  • 思路
  1. 首先,代码读取输入数据,包括地图的大小(n和m)以及游戏的时间限制(t)。同时,初始化一些变量和数据结构。

  2. 然后,通过优先队列和Dijkstra算法计算从大本营位置开始到达其他位置的最短路径。这里使用了一个二维数组dis来记录每个位置的最小消耗,一个二维数组path来记录路径上每个位置的前一个位置,一个二维数组tim来记录经过的步数。

  3. 接下来,遍历僵尸的向量v,对于每个僵尸,计算其到达大本营的最短路径,并将路径保存在p数组中。同时,使用一个map来记录每个坐标对应的编号,并用cnt数组记录每个编号对应的僵尸数量。

  4. 在游戏的每个时间段内,判断每个僵尸的当前位置是否为防御塔或大本营,如果是,则进行进攻操作;否则,移动到路径上的下一个位置。

  5. 最后,输出更新后的地图情况。如果大本营的血量为0,则输出"Game Over"。

  • 数据读入和预处理

  1. 读入f[i][j],若僵尸不在四个角上,则将坐标的和数量加入中,v.push_back({i,j,f[i][j]})
  • 用带优先队列的 Dijkstra,得到最伤害最小的路径,如伤害一致则比较距离,path记录下一个点的坐标

  1. 初始化:

    dis[e.x][e.y] = 0;//e是大本营

        q.push({0,{e.x,e.y}});

  2. while(q不为空)弹出堆顶g,对st进行判断和处理

  3. 遍历g上下左右,合法状态更新dis 和tim 和path

  4. 若距离相等,比较tim 更新path

  5. 加入q

  • 得到僵尸的路径p[id]

  1. 给僵尸团编号,存储数量
  2. 让僵尸走到地图中
  3. 直到走到大本营
  • 遍历每一秒

  1. 遍历每个时间

  2. 初始化ss

  3. 遍历僵尸:若数量为0则pass;根据now移动僵尸,若遇上阻挡,将坐标加入ss

  4. 遍历僵尸:若数量为0则pass;如果坐标不在ss中,对now进行+1,否则对防御值和僵尸数量进行更新;

  5. 如果大本营为0,则break;

#include <iostream>
#include <vector>
#include <map>
#include <queue>
#include <set>
#define x first
#define y second
using namespace std;
typedef pair<int,int>PII;
typedef pair<int,PII>PIII;
struct Node{
    int x , y , cc ;
};
const int N = 200;
int f[N][N];//地图
int n,m,t;
int dx[] = {-1,1,0,0},dy[] = {0,0,-1,1};//方向
PII e ;//用来记录大本营坐标
vector<Node>v;//用来记录僵尸
int dis[N][N];//用来记录经过防御塔的点数,即消耗量
int vis[N][N];//用来记录接下来是否经历过该坐标
int tim[N][N];//用来记录走过的步数
PII path[N][N];//用来记录路径
map<pair<int,int>,int>st ;//
int idx = 1  ;//用来计数
int cnt[N*N];//僵尸个数
int now[N*N];
vector<PII>p[N*N];//记录每个坐标僵尸的路线
int get(PII xx)
{
    if(st.count(xx) == 0) st[xx] = idx++ ;
    return st[xx] ;
}
void solve(){
    //输入数据
    scanf("%d %d %d",&n,&m,&t);
    for(int i=0;i<=n+1;i++)
        for(int j=0;j<=m+1;j++)
        {
            vis[i][j] = false;//每个点没经过
            dis[i][j] = 0x3f3f3f3f;//把消耗设置为最大值
            path[i][j] = {-1,-1};//把路径先弄成-1
            tim[i][j] = 0 ;//经过的格子先设置为0
            scanf("%d",&f[i][j]);
            if(f[i][j]<0)e = {i,j};//记录大本营坐标
            if((i==0&&j==m+1)||(i==0&&j==0)||(i==n+1&&j==0)||(i==n+1&&j==m+1))//4个角落的数据不用管
                continue;
            if(i < 1 || i > n || j < 1 || j > m && f[i][j] > 0)//把僵尸放进一个向量v里
                v.push_back({i,j,f[i][j]});
        }
    priority_queue<PIII,vector<PIII>,greater<PIII>>q;//优先队列,自动从小到大排序(排序第一个元素)
    //用带优先队列的 Dijkstra,不容易被卡复杂度
    dis[e.x][e.y] = 0;
    q.push({0,{e.x,e.y}});//以大本营位置开始往四周位置广搜,其中第一个为消耗,后面的为坐标
    while(!q.empty()){
        auto g = q.top();q.pop();
        PII l = g.y;
        if(vis[l.x][l.y])continue;//如果遍历过之后就不用再考虑了
        vis[l.x][l.y] = true;
        for(int i=0;i<4;i++){
            int a = l.x+dx[i] , b = l.y+dy[i];
            if(a < 1 || a > n  || b < 1 || b > m ) continue;//非法情况
            if(dis[a][b] > dis[l.x][l.y] + f[a][b]){//如果找到到达该点消耗更小的,则进行更新
                dis[a][b] = dis[l.x][l.y] + f[a][b];
                path[a][b] =  l;
                tim[a][b] = tim[l.x][l.y]+1;
            }
            else if(dis[a][b]==dis[l.x][l.y]+f[a][b]){//由题意可知,如果有消耗相等的,则找到到达大本营最快的
                if(tim[a][b]>tim[l.x][l.y]+1){
                    tim[a][b] = tim[l.x][l.y]+1;
                    path[a][b] = l;
                }
            }
            q.push({dis[a][b],{a,b}});
        }
    }
    for(auto it:v){//遍历向量v
        int id = get({it.x,it.y});
        cnt[id] = it.cc ;
        PII kk ;
        for(int i = 0 ; i < 4 ; i++) {//由僵尸边上走到地图里面去
            if(it.x + dx[i] >= 1 && it.x + dx[i] <= n && it.y + dy[i] >= 1 && it.y + dy[i] <= m){
                kk.x = it.x + dx[i] , kk.y = it.y  + dy[i];
                break ;
            }
        }
        while(true){
            p[id].push_back(kk);
            if(kk.x==e.x && kk.y == e.y)break;//找到大本营的位置了
            kk = path[kk.x][kk.y];
        }
    }
    //考虑每段时间的变化
    for(int i=1;i<=t;i++){
        set<pair<int,int>>ss;
        for(auto it:v){
            int no = get({it.x,it.y});
            if(cnt[no]==0)continue;
            int x = p[no][now[no]].x,y = p[no][now[no]].y;
            if(f[x][y] > 0 && !(x == e.x && y == e.y)){
                ss.insert({x,y});
            }else if(f[x][y] < 0 && (x == e.x && y == e.y)){
                ss.insert({x,y});
            }
        }
        for(auto it:v){
            int no = get({it.x,it.y});
            if(cnt[no]==0)continue;
            int x = p[no][now[no]].x,y = p[no][now[no]].y;
            if(ss.count({x,y}) == 0){
                now[no] = min(now[no]+1,(int)p[no].size()-1);
            }else{//对防御塔和大本营进行进攻
                if(f[x][y] > 0) f[x][y]--;//防御塔
                else if(f[x][y] < 0) f[x][y]++;//大本营
                cnt[no]--;//僵尸数量减少
            }
        }
        if(f[e.x][e.y] == 0){//如果大本营没血了gameover了
            break;
        }
    }
    //输出
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            printf("%d",f[i][j]);
            if(j<m)printf(" ");//保证最后一格没有空格
        }
        printf("\n");
    }
    if(f[e.x][e.y]==0)printf("Game Over");
}
signed main() {
    solve();
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
根据提供的引用内容,CSP-J2021复赛有两道题目,分别是分糖果和小熊的果篮。 对于第一题分糖果,题目来源是CCF,难度为入门。根据给出的代码,这是一个基于循环的算法,通过遍历[l,r]区间内的数,计算数对n取模后的最大值。具体的实现细节可以参考引用中的代码。这道题目属于入门级别,比较简单。 第二题是关于小熊的果篮。给定一个长度为n的数组a,其中连续的相同元素被视为一个块,要求按照块的顺序输出每个块的头元素,并删除已输出的元素。具体的实现细节可以参考引用中的代码。这道题目需要使用双链表来处理,时间复杂度为O(n)。 综上所述,CSP-J2021复赛的题目包括分糖果和小熊的果篮,具体的解题思路和代码实现可以参考上述引用内容。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [[CSP-J 2021]比赛题解](https://blog.csdn.net/weixin_56550385/article/details/126811201)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [新鲜出炉的 CSP-J 2021 复赛题目 题解](https://blog.csdn.net/qq_23109971/article/details/121024436)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Blossom๑.๑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值