【2015多校】【hdoj 5336】XYZ and Drops 模拟 论时间驱动的模拟实现与事件驱动的模拟实现

题意就不细说了,背景和玩法和模拟细节就是 经典小游戏十滴水(http://www.7k7k.com/swf/15746.htm),大家自己去玩就行。


约定:以下文字中,称题面里的waterdrop为水滴,small drops为水珠。


题目描述里遗漏了很多细节,这里根据比赛里正确的Clarification里的回应补充一下:

  • 初始爆炸位置和其他有水滴的位置都互不重叠(都在不同位置上)
  • 每个水滴的初始大小为1~4的整数
  • 水珠撞到边上的时候直接消失,不反弹
  • 某一秒,水滴体积大于4就在这一秒炸裂,炸裂的时候只会产生4个体积为1的水珠(守恒是什么,可以吃吗?反正游戏规则这么来的)
  • 水珠撞到水滴的时候会被吸收,水滴吸收完这一秒到它上的水珠后再考虑是否炸裂
  • 水滴炸裂后就当不存在了
  • (x,y)位置的水滴在第T秒破裂后,四个小水珠在第T秒都在(x,y),不过运动方向不同
  • 关于(x,y)的坐标系方向:左上角为(1,1),向下x增大,向右y增大(或者说,r约束x的范围,c约束y的范围)(这个地方值得吐槽比赛时的出题人,懒得理人就算了,之后答了别人类似的问题还打错了,顺利的误导人了是什么样的心态)


接下来讨论一下这种推演一定时间后的情况的模拟题应该如何去实现。一般有2种思路:

  1. 每一秒,检查每个水滴和水珠的状态,应该移动的移动,应该炸裂的炸裂
  2. 当水珠撞上水滴的时候,做相应处理:改被吸收的吸收掉,该炸裂的炸裂,该继续前进的继续前进
个人喜好,称前者为时间驱动的模拟,后者为事件驱动的模拟。

下面来讨论一下这两种模拟的具体实现:

1. 时间驱动的模拟

一般的实现思路就和梦幻西游、问道这类回合制游戏类似:维护一个对象队列,每一秒轮训每个对象(或者说,扫一遍这个对象队列),访问到的对象采取相应的行动。
注意到,在这题里,水珠是同时移动的对象,并没有什么同一秒内还先来后到的情况。
可以这样处理:先判断水珠是继续向前还是被吸收,采取相应行动,然后检查每个水滴是否应该爆炸。
这样实现,时间复杂度是O(Tn)
代码如下:

#include <stdio.h>
#include <queue>
using namespace std;
const int di[4][2]={{-1,0},{1,0},{0,-1},{0,1}};
const int MAXN=100;
struct Drop{
    int x,y,d;
    Drop(){}
    Drop(int a,int b,int c):x(a),y(b),d(c){}
    Drop next(){
        return Drop(x+di[d][0],y+di[d][1],d);
    }
}input[MAXN+5];

int mp[MAXN+5][MAXN+5];
int crackTime[MAXN+5];

int main(){
    int r,c,n,T;
    int tmpx,tmpy;
    for(;~scanf("%d%d%d%d",&r,&c,&n,&T);){
        for(int i=0;i<=r;i++){
            for(int j=0;j<=c;j++){
                mp[i][j]=0;
            }
        }
        for(int i=0;i<n;i++){
            scanf("%d%d%d",&input[i].x,&input[i].y,&input[i].d);
            mp[input[i].x][input[i].y]=input[i].d;
            crackTime[i]=0;
        }
        scanf("%d%d",&tmpx,&tmpy);
        queue<Drop> q;
        for(int i=0;i<4;i++){
            q.push(Drop(tmpx,tmpy,i));
        }
        for(int time=1;time<=T;time++){
            int len=q.size();
            if(len==0)break;
            for(int i=0;i<len;i++){
                Drop tmp=q.front();q.pop();
                tmp=tmp.next();
                if(tmp.x<=0||tmp.y<=0||tmp.x>r||tmp.y>c)continue;
                if(mp[tmp.x][tmp.y]){
                    mp[tmp.x][tmp.y]++;
                }else{
                	q.push(tmp);
                }
            }
            for(int i=0;i<n;i++){
                if(mp[input[i].x][input[i].y]>4){
                	for(int j=0;j<4;j++){
                        q.push(Drop(input[i].x,input[i].y,j));
                    }
                    crackTime[i]=time;
                	mp[input[i].x][input[i].y]=0;
                }
            }
        }
        for(int i=0;i<n;i++){
            if(crackTime[i]){
                printf("0 %d\n",crackTime[i]);
            }else printf("1 %d\n",mp[input[i].x][input[i].y]);
        }
    }
    return 0;
}
(喂,一个Drop结构体,或者说,类,做了2件事情啊!)
(喂,里面悄悄加了个剪枝啊!)

2.事件驱动的模拟

之前如果是需要事件驱动的模拟其实我是拒绝的,没干过,就都扔给队友写了……
不过既然题解里提到了r,c,n<=100000,T<=1000000000这样的数据范围的模拟,不妨来思考一下怎么写。
时间驱动写法虽然容易想清楚,但时间复杂度肯定消受不起。

事件驱动的话,这里要处理的事件就是水珠撞上水滴了。
具体到这一题上,
建立一个时间为唯一关键词的小根堆,小根堆里放水珠信息的结构体,时间为一个水珠预期撞上水滴的时间,还有水珠撞上的位置、方向,以及要撞上的水滴编号(方便处理)
然后不停取堆顶元素,如果那个位置有水滴,则被吸收,否则继续前进,同时吸收后要考虑是否会炸裂,炸裂同理要记录时间与产生新的小水珠。
在产生小水珠的时候,往其前进方向找是否有水滴,如果没有,就不需要入队直接抛弃,否则计算一下到达时间T并入队。
为了快速查找,这里采用set<>的数组,按行和按列都要,水滴在2个数组都要记录,删除也要记得同时删除。
这里的时间复杂度是O(n*logn*logn),如果查询下一个事件发生时间是O(1)的,则为O(n*logn)
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <math.h>
#include <algorithm>
#include <set>
#include <queue>
using namespace std;
typedef long long ll;

const int MAXN=100;

struct INDEX{
	int key,value;
	INDEX(){}
	INDEX(int k,int v):key(k),value(v){}
	bool operator<(const INDEX&b)const{
		return key<b.key;
	}
};

set<INDEX> row[MAXN+5],col[MAXN+5];
const int di[4][2]={{-1,0},{1,0},{0,-1},{0,1}};

struct Drop{
    int t,x,y,d,idx;
    Drop(){}
    Drop(int T,int a,int b,int c,int IDX):t(T),x(a),y(b),d(c),idx(IDX){}
    bool operator<(const Drop&b)const{
    	return t>b.t;
	}
	Drop forward(){
		set<INDEX>::iterator it;
		if(d<=1){
			it=col[y].lower_bound(INDEX(x,0));
			if(d==0)it--;
			if(it->value<0)return Drop(-1,0,0,0,-1);
			return Drop(t+abs(x-(it->key)),it->key,y,d,it->value);
		}else{
			it=row[x].lower_bound(INDEX(y,0));
			if(d==2)it--;
			if(it->value<0)return Drop(-1,0,0,0,-1);
			return Drop(t+abs(y-(it->key)),x,it->key,d,it->value);
		}
	}
};

int size[MAXN+5];
int crackTime[MAXN+5];

int main(){
	int Case=1;
    int r,c,n,T;
    int tmpx,tmpy,tmpz;
    for(;~scanf("%d%d%d%d",&r,&c,&n,&T);){
    	for(int i=0;i<=r;i++){row[i].clear();row[i].insert(INDEX(c+1,-1));row[i].insert(INDEX(0,-1));}
    	for(int i=0;i<=c;i++){col[i].clear();col[i].insert(INDEX(r+1,-1));col[i].insert(INDEX(0,-1));}
		for(int i=0;i<n;i++){
			scanf("%d%d%d",&tmpx,&tmpy,&tmpz);
			crackTime[i]=0;
			size[i]=tmpz;
			row[tmpx].insert(INDEX(tmpy,i));
			col[tmpy].insert(INDEX(tmpx,i));
		}
		scanf("%d%d",&tmpx,&tmpy);
		priority_queue<Drop> q;
		for(int i=0;i<4;i++){
			Drop t(0,tmpx,tmpy,i,0);
			t=t.forward();
			if(t.t>=0&&t.t<=T) q.push(t);
		}
		while(!q.empty()){
			Drop x=q.top();q.pop();
			int idx=x.idx;
			if(crackTime[idx]){
				if(crackTime[idx]<x.t){
					x=x.forward();
					if(x.t>=0&&x.t<=T) q.push(x);
				}
			}else{
				if(size[idx]){
					size[idx]++;
					if(size[idx]>4){
						crackTime[idx]=x.t;
						size[idx]=0;
						row[x.x].erase(INDEX(x.y,0));
						col[x.y].erase(INDEX(x.x,0));
						for(int i=0;i<4;i++){
							Drop t(x.t,x.x,x.y,i,0);
							t=t.forward();
							if(t.t>=0&&t.t<=T) q.push(t);
						}
					}
				}else{
					x=x.forward();
					if(x.t>=0&&x.t<=T) q.push(x);
				}
			}
		}
		for(int i=0;i<n;i++){
            if(crackTime[i]){
                printf("0 %d\n",crackTime[i]);
            }else printf("1 %d\n",size[i]);
        }
    }
    return 0;
}

两者相比,时间驱动很容易想到也很容易实现(特别是条件过于复杂的时候相对事件驱动来讲),但是无意义对象去除太迟,也花了很多时间在一点点移动水珠上,做了很多无用功
事件驱动方式就省去了很多无用功,运算飞快(这一题上,时间驱动60ms~200ms,取决于实现时是否剪枝了,事件驱动0~15ms)。但是复杂过程模拟上的实现难度就有挑战了(当然这个题的要求还不算什么复杂的)

你可能感兴趣的:(ACM,2015多校)