BZOJ1033:[ZJOI2008]杀蚂蚁
我对模拟的理解:https://www.cnblogs.com/AKMer/p/9064018.html
题目传送门:https://www.lydsy.com/JudgeOnline/problem.php?id=1033
这几天杀蚂蚁杀得我都杀马特了\(emmm\)
为了写这题,我玩了大概\(3\)天\(antbuster\)(也就是找了个借口颓了三天),最高得分也只有\(13000\)多。说实话不看题面的话我真的以为写这个程序的程序员脑子\(water\)了……为什么蚂蚁会背着蛋糕往我的炮阵里冲……蠢萌蠢萌的。
还有,蚂蚁的血量是指数级增长的,这对新手玩家极其不友好……
我昨天上午\(10\)点左右开始写的,然后一边上文化课一边在文科课和无关紧要的课以及晚自习偷渡来机房杀蚂蚁……然后一天下来敲了个框架……可以\(A\)三个点。
然后今天上午\(9\)点左右开始改的,一个一个找细节一个一个改。一个上午过完啥都没改出来(也就只有一节地理课和政治课跑机房来了)。一个下午过完把\(30\)分代码变成了\(40\)分代码,多\(A\)了没有炮台的那个点。
说难的话这题也并不难,大模拟嘛,一眼秒。
说简单的话……那可以去死了。这题代码平均长度\(5K\),容易写难调,如果没有\(std\)和\(data\)的话我应该一辈子都调不出来。如果有大佬在现场\(A\)掉的话请收下我的膝盖,毕竟可以在没有数据的情况下考虑完所有细节不是一般人可以做到的,这种神犇具有福尔摩斯的分析能力好呗。
对于每一秒钟,我们有如下操作:
①如果地图上没有六只蚂蚁并且\((0,0)\)没有蚂蚁就生产一只新蚂蚁。
细节:记得要用表示已死亡蚂蚁或者不存在蚂蚁的数组来存新生蚂蚁,不要把已经出现的活着的蚂蚁数据覆盖掉了。
②所以蚂蚁按照出生顺序开始依次移动。
细节:“并且那个点不能是蚂蚁上一秒所在的点(除非上一个时刻蚂蚁就被卡住,且这个时刻它仍无法动)”意思是不能去上一秒所在的点,除非上一面卡在原地了,然后这一秒又卡在原地,才可以算是被迫待在了“上一秒所在点”。在这种情况里,如果这只蚂蚁是被卡在了\((n,m)\),需要特判会不会拿起蛋糕。比如之前蚂蚁\(A\)从\((n,m)\)拿走了蛋糕,然后\(B\)走到了\((n,m)\)并被卡在了该处,然后\(A\)挂了,蛋糕归位,就被\(B\)背起了。然后背起蛋糕记得马上加血(玩家经验,这血加得贼鸡儿恐怖)
③炮塔开始攻击。
细节:炮塔攻击全部是同时的,也就是说你枚举炮塔开始攻击的时候,即使某个蚂蚁\(hp\)被之前你枚举的炮塔打到了负数,但是它如果是你的\(target\)你还是会打他。因为在你选择它成为你的\(target\)的时候它还是活的。然后判断蚂蚁是否是\(target\)的时候蚂蚁是点,判断激光会否伤害到蚂蚁的时候蚂蚁就是半径为\(0.5\)的圆了。找\(target\)的步骤按照题面就可以了。但是判断该条激光会否伤害蚂蚁就非常有趣了。
迄今为止,我见过有人说自己用点到直线距离公式做的,有人说自己用勾股定理做的,还有人说自己暴力判断的(我是真的不知道怎么暴力判断)
判断一个圆与一条线段是否有交点,我用了点积和叉积。分为如下两种情况:
如果不能过该蚂蚁所在点作代表激光线段的垂线:
如图,假设蚂蚁在点\(c\),\(target\)在点A,炮塔在点\(B\),向量\(v·u\)为负数,说明角\(c\)是钝角,那么\(c\)到激光线段的距离就是线段\(v\)的长度,判断是否大于\(0.5\)就可以了,如果角\(ABC\)是钝角同理判断即可。
能过该蚂蚁所在点作激光线段的垂线:
\(f\)为激光,\(C、B\)一个是炮塔一个是\(target\),\(A\)是蚂蚁,角\(c\)是锐角,那么\(A\)到\(f\)的距离就是过\(A\)点作\(f\)的垂线的长度。那么可以用\(abs(v\times u)/|f|\)来表示,因为\(abs(v\times u)/2\)就是三角形\(ABC\)的面积(不懂的同学自己去看叉积),然后判断是否大于\(0.5\)就可以了。
于是乎,攻击就完了_
④判断蚂蚁是否死亡,判断游戏是否结束。
细节:如果是蛋糕进入蚂蚁窝导致游戏结束,那么这一秒没有完全过完就\(game over\)了,那么这一秒对于蚂蚁的寿命不会有贡献。就好比你还差\(3\)个月\(17\)岁,但是你还是\(16\)岁。然后背上有蛋糕的蚂蚁挂掉了蛋糕就归位。蚂蚁死掉了记得清除障碍记录。
那么,漫长的一秒就这么结束了。
因为我只记录了蚂蚁的出生时间,寿命就用结束时间\(-\)出生时间\(+1\)来表示。但是如果游戏提前结束,那么结束时间应该再减一,因为最后一秒不算数。
完结撒花_
时间复杂度:\(O(n*m*T)\)
空间复杂度:\(O(n*m)+O(乱七八糟的储存空间)\)
代码如下:
#include <cmath>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
#define sqr(a) ((a)*(a))
const int inf=2e9;
double HP=1;//存1.1^k
int phe[9][9];//存每个点的信息素
int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};//从0到3分别是向东、西、南、北
int n,m,s,d,r,T,ant_cnt,ant_tot;
//n,m是地图大小,s是炮塔总个数,d是炮塔A一下的伤害,r是炮塔攻击范围,T是时间;
//ant_cnt动态储存场上蚂蚁的信息,ant_tot存蚂蚁总数,用于计算蚂蚁等级
bool obs[9][9],cake_in_map=1,game_over;
//obs[i][j]储存(i,j)是否有障碍物(蚂蚁或者炮塔),cake_in_map表示蛋糕是否在(n,m),game_over如字面意思。
int read() {
int x=0,f=1;char ch=getchar();
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<1)+(x<<3)+ch-'0';
return x*f;
}//快读
struct Map {
int x,y,id;//x,y表示坐标,id表示id号方向
Map(){};
Map(int _x,int _y,int _id) {
x=_x,y=_y,id=_id;
}
bool operator <(const Map &a)const {
if(phe[x][y]==phe[a.x][a.y])return id<a.id;
return phe[x][y]>phe[a.x][a.y];
}//按照phe为第一关键字,id为第二关键字排序
};//存可达点信息
struct Ant {
int x,y,birth,lv,hp,cake,hp_max,lstx,lsty;
//x,y存蚂蚁坐标,birth是出生时间,lv是等级,hp是当前生命值,hp_max是最大生命值
//cake表示是否背上了蛋糕,lstx,lsty表示该蚂蚁上一秒在哪个点
bool operator <(const Ant &a)const {
return birth<a.birth;
}//按出生时间排序
void clear() {
birth=inf;
lstx=lsty=x=y=lv=hp=cake=hp_max=0;
}//初始化
bool check(int X,int Y) {
return X<0||X>n||Y<0||Y>m||obs[X][Y];//(X,Y)是否可以到达
}
void new_born(int tim) {
ant_cnt++;ant_tot++;//场上蚂蚁数+1,该蚂蚁编号为ant_tot;
obs[0][0]=1;lstx=lsty=x=y=0;//初始化
if((ant_tot-1)%6==0)HP=1.1*HP;//每过6只蚂蚁升一级
hp_max=hp=(int)(HP*4);
birth=tim;cake=0;
lv=(ant_tot-1)/6+1;//初始化新生蚂蚁信息
}
int calc(int a,int b) {
if(a<b)return 4-(b-a);
return a-b;
}//算从a号方向逆时针旋转几次可以到b号方向
void move(int tim,bool way) {
if(cake)phe[x][y]+=5;
else phe[x][y]+=2;//先在原格子上留下信息素
Map a[4];int top=-1;
for(int i=0;i<4;i++) {
int X=x+dx[i],Y=y+dy[i];
if(check(X,Y))continue;//如果该点为不可达点
if(X==lstx&&Y==lsty)continue;//或者该点为上一秒所在的点
a[++top]=Map(X,Y,i);//该点可达
}if(top==-1){//如果被卡在原地了
lstx=x,lsty=y;//一定要记得更新lstx,lsty
if(x==n&&y==m&&cake_in_map) {//一定要记得更新是否拿起了蛋糕
cake_in_map=0;
cake=1;
hp=min(hp+hp_max/2,hp_max);
}
return;
}
obs[x][y]=0;//移动的时候记得更新obs数组
sort(a,a+top+1);//按phe为第一关键字,id为第二关键字排序
int goal=0;//0号点就是目标点
if(way) {//如果活动时间是5的倍数
swap(a[0],a[goal]);goal=-1;//首先把goal放到0来,然后从剩下的几个点里找可以逆时针到达的
for(int i=1;i<=top;i++)
if(goal==-1)goal=i;//因为goal初始值是-1,所以必须这么写,不然会RE
else {
if(calc(a[0].id,a[goal].id)>calc(a[0].id,a[i].id))
goal=i;//如果从0开始逆时针旋转到i相对于goal用了更少的次数那么i肯定更优
}
if(goal==-1)goal=0;//如果不存在了,说明转了一圈,就回到了原本的goal
}
lstx=x,lsty=y;
x=a[goal].x,y=a[goal].y;//移动
if(x==n&&y==m&&cake_in_map) {
cake_in_map=0;
cake=1;
hp=min(hp+hp_max/2,hp_max);
}//判断是否背起了蛋糕
obs[x][y]=1;//新到达点成为障碍点
}
void check_life() {
if(hp>=0&&cake&&(!x)&&(!y)) {
game_over=1;return;
}//如果该蚂蚁活着把蛋糕送到了蚂蚁窝那么游戏结束
if(hp>=0)return;//如果他是活着的蚂蚁就不管他
birth=inf;//birth改成inf
obs[x][y]=0;//清楚障碍记录
if(cake)cake_in_map=1;//如果背着蛋糕那么蛋糕归为
ant_cnt--;//场上蚂蚁数-1
}
}ant[7];
struct Turret {
int x,y;//炮塔坐标
double dis(int x1,int y1,int x2,int y2) {
return sqrt(sqr(x1-x2)+sqr(y1-y2));
}//求(x1,y1)(x2,y2)两点之间的距离
int dot_pro(int x1,int y1,int x2,int y2) {
return x1*x2+y1*y2;
}//求向量(x1,y1)(x2,y2)的点积
int cross_pro(int x1,int y1,int x2,int y2) {
return abs(x1*y2-y1*x2);
}//求向量(x1,y1)(x2,y2)的叉积
bool check_attack(int x1,int y1,int x2,int y2,int X,int Y) {
if(dot_pro(X-x1,Y-y1,x2-x1,y2-y1)<0)
return dis(x1,y1,X,Y)<=0.5;//如果点积小于0说明该角是钝角
if(dot_pro(X-x2,Y-y2,x1-x2,y1-y2)<0)
return dis(X,Y,x2,y2)<=0.5;//同上
return 1.0*cross_pro(x1-X,y1-Y,x2-X,y2-Y)/dis(x1,y1,x2,y2)<=0.5;//否则直接求距离
}
void Attack() {
int goal=0;double Dis=inf;//goal存目标蚂蚁编号,Dis存该蚂蚁离炮塔的距离
for(int i=1;i<=ant_cnt;i++) {
double tmp=dis(ant[i].x,ant[i].y,x,y);//i号蚂蚁与当前炮塔距离
if(tmp>r)continue;//距离大于攻击范围直接pass
if(ant[i].cake) {
goal=i;
break;
}//如果该蚂蚁背着蛋糕并且在攻击范围内直接成为target
if(tmp<Dis) {
Dis=tmp;
goal=i;
}//否则就选最近的蚂蚁攻击
}
if(!goal)return;//如果打不到蚂蚁就return
int X=ant[goal].x,Y=ant[goal].y;//存下target的坐标,表示激光的线段就是(x,y)——>(X,Y)
for(int i=1;i<=ant_cnt;i++) {//枚举蚂蚁
if(check_attack(x,y,X,Y,ant[i].x,ant[i].y))
//如果以(ant[i].x,ant[i].y)为圆心,半径为0.5的圆和线段(x,y)——>(X,Y)有交点
ant[i].hp-=d;//受到一次伤害
}
}
}tur[21];
void clear_phe() {
for(int i=0;i<=n;i++)
for(int j=0;j<=m;j++)
if(phe[i][j])phe[i][j]--;
}//信息素流失
int main() {
n=read(),m=read();//地图大小读入
s=read(),d=read(),r=read();
for(int i=1;i<=s;i++) {
tur[i].x=read(),tur[i].y=read();
obs[tur[i].x][tur[i].y]=1;
}//读入炮塔信息
T=read();int tim;
for(int i=1;i<=6;i++)ant[i].clear();//先初始化,我用在inf秒出生表示该蚂蚁不存在或者挂掉了。
for(tim=1;tim<=T;tim++) {
sort(ant+1,ant+7);//先按出生时间排序
if(ant_cnt<6&&(!obs[0][0]))
ant[ant_cnt+1].new_born(tim);//如果不够6只并且(0,0)没有蚂蚁就生产新蚂蚁
for(int i=1;i<=ant_cnt;i++)
ant[i].move(tim,(tim-ant[i].birth+1)%5==0);//移动
for(int i=1;i<=s;i++)tur[i].Attack();//炮塔一起攻击
for(int i=1;i<=ant_cnt;i++)ant[i].check_life();//判断蚂蚁是否死亡,游戏是否结束
if(game_over)break;//游戏提前结束
clear_phe();//信息素流失
}
if(tim<=T)printf("Game over after %d seconds\n",tim),tim--;//如果游戏是提前结束的那么最后一秒就不算数
else puts("The game is going on"),tim=T;//否则T就是结束时间
printf("%d\n",ant_cnt);
sort(ant+1,ant+7);
for(int i=1;i<=ant_cnt;i++)
printf("%d %d %d %d %d\n",tim-ant[i].birth+1,ant[i].lv,ant[i].hp,ant[i].x,ant[i].y);
return 0;
}