前言
由于久仰大名+一时兴起+最近考试总是打挂细节,于是终于对这道题下手了.jpg
实际上调试过程比想象中要好点,再加上有LOJ数据帮忙,码基础函数花了4h左右,然后从第一次提交到AC也就花了3h。
算是退役前,了却一个心愿吧。
正文
关于题目内容大家可以自己去看,我就不说了。我在这里主要阐述一下打这道题代码的核心思路。
众所周知,打这种大模拟题必须思路清晰,并且要有一些基础函数的帮忙,才能在保证代码量和调试难度的基础上AC。
那么那道题,我的基础思路是这样的:
1、猪的身份、是否表现、装备、HP ---->结构体
2、摸牌阶段、出牌阶段、字符转数字 ----->switch、for、if
3、锦囊牌、基本牌——函数实现
这是一个最基础的大体架构,即我们打算以怎样的复杂度来实现。尽管模拟题一般都不会卡你的复杂度,但是追求一种更优秀的方法总是好的。
于是在敲定这一点后,我就开始动手打代码了。
一、基础部分
我本来一开始就打算写锦囊牌的,但是写到一半就发现要实现的很多东西都会在别的地方重复实现,于是我就先开了个叫Basic的namespace来实现一些基本的操作。
基本的操作有哪些呢?
抽牌,询问某玩家有多少张某种牌,扣除某玩家的某数量的某种牌,将手牌向左靠拢(填补空缺)。这些是与手牌相关的。
是否死亡的判定,死亡之后的奖惩机制,游戏结束。这是与死亡相关的。
除此之外,还有用来判断a对b的看法的recognize函数,字符数字相互转换的get/reget id函数。由于我采用的变量名算得上是浅显易懂,所以直接看代码应该也没问题,这里就不多阐述了:
struct Pig{
int HP,num,hand[1002];
//生命值,手牌数,手牌
int type,equ,show,re;
//身份,是否装备,是否跳忠/反/类反,是否需要洗手牌
//type=1/2/3分别对应主公,忠臣,反贼
//show=0/1/2/3/4分别对应未知,主公,跳忠,跳反,类反
//re是因为在装备/杀人摸牌之后优先使用最左侧的能用的牌
//因此需要重新从最左边开始扫
int l,r;
//顺时针的人,逆时针的人
}pig[13];
namespace Basic
{
int card[2010],card_num,num_fan;
namespace Trans{//数字和字符的转换
iint get_id(char c)
{
switch(c){
case 'K':return 1;break;
case 'D':return 2;break;
case 'P':return 3;break;
case 'F':return 4;break;
case 'N':return 5;break;
case 'W':return 6;break;
case 'J':return 7;break;
case 'Z':return 8;break;
}
}
iint reget_id(int x)
{
switch(x){
case 1:return 'K';break;
case 2:return 'D';break;
case 3:return 'P';break;
case 4:return 'F';break;
case 5:return 'N';break;
case 6:return 'W';break;
case 7:return 'J';break;
case 8:return 'Z';break;
}
}
}using namespace Trans;
namespace Card{//手牌相关
ivoid get_card(int id,int num)
{
for(rint i=1;i<=num;i++){
if(!card_num)card_num++;
pig[id].hand[++pig[id].num]=card[card_num--];
}
}
ivoid refresh(int id)
{
int *p=pig[id].hand,pos=0;
for(rint i=1;i<=pig[id].num;i++)
if(p[i])p[++pos]=p[i];
pig[id].num=pos;
}
iint query(int id,int type)
{
int *p=pig[id].hand,num=0;
for(rint i=1;i<=pig[id].num;i++)
if(p[i]==type)num++;
return num;
}
ivoid dec_some(int id,int type,int num)
{
int *p=pig[id].hand;
for(rint i=1;i<=pig[id].num;i++)
if(p[i]==type&&num)num--,p[i]=0;
}
}using namespace Card;
namespace GG{//游戏结束
ivoid game_over()
{
if(!pig[1].HP)cout<<"FP"<<endl;
//如果是主公死了输出FP
else cout<<"MP"<<endl;
//否则就是主公赢了
for(rint i=1;i<=n;i++){
if(pig[i].HP<=0)cout<<"DEAD"<<endl;
else{
refresh(i);
for(rint j=1;j<=pig[i].num;j++)
cout<<(char)reget_id(pig[i].hand[j])<<" ";
cout<<endl;
}
}
exit(0);
}
}using namespace GG;
namespace Die{//死亡相关
ibool if_alive(int id)
{
if(pig[id].HP>0)return 1;
int num=query(id,3);
if(pig[id].HP+num>0){dec_some(id,3,1-pig[id].HP);pig[id].HP=1;return 1;}
return 0;
}
ivoid judge(int id1,int id2)
{
pig[pig[id2].l].r=pig[id2].r;
pig[pig[id2].r].l=pig[id2].l;
if(pig[id2].type==1)game_over();
if(pig[id2].type==3&&!(--num_fan))game_over();
//要是反贼死完了||主公挂了可以直接GG
if(pig[id2].type==3)
{get_card(id1,3);pig[id1].re=1;return;}
//一旦通过杀反贼摸了牌就要重新从左边开始考虑.jpg
if(pig[id1].type==1&&pig[id2].type==2)
{pig[id1].num=0;pig[id1].equ=0;return;}
}
}using namespace Die;
namespace Think{
iint recognize(int id1,int id2){
//0是未知,1是友军,2是敌人
if(pig[id1].type==1&&pig[id2].show==4)return 2;
//特判类反贼
if(!pig[id2].show||pig[id2].show==4)return 0;
if(pig[id1].type!=3&&pig[id2].show!=3)return 1;
if(pig[id1].type==3&&pig[id2].show==3)return 1;
return 2;
}
}using namespace Think;
namespace Prevent{
inline bool prevent(int id1,int id2,int type){
int pos=id1;
do{
if(recognize(pos,id2)==1+(type^1)){
if(query(pos,7)){
dec_some(pos,7,1);
pig[pos].show=pig[pos].type;
return 1^prevent(pos,id2,type^1);
}
}
pos=pig[pos].r;
}while(pos!=id1);
return 0;
}
}using namespace Prevent;
}
using namespace Basic;
比较特殊的是我把无懈可击也给写到了basic里面,实际上它应该归到锦囊牌里面,但由于这个牌太特殊了且对于所有别的锦囊牌都有影响,我就把他提上来写了。
二、锦囊牌
1、无懈可击
这张牌在游戏里算是最特殊的,首先无懈可击可以对所有的锦囊牌(包括无懈可击)使用,其次只有使用无懈可击才可以献殷勤。
那么首先,对于一张锦囊牌,我们要判断这对它的生效者是有害还是有利。如果有害,就是与生效者为友军的人使用无懈可击,否则就是他的敌人使用无懈可击。这一点我们可以利用递归,生效与否和利害关系则可以通过01异或来解决。
inline bool prevent(int id1,int id2,int type){//type为1是有害
int pos=id1;//从出牌人开始逆时针
do{
if(recognize(pos,id2)==1+(type^1)){//判断友军出还是敌军出
if(query(pos,7)){//如果手上有
dec_some(pos,7,1);
pig[pos].show=pig[pos].type;
return 1^prevent(pos,id2,type^1);//递归
}
}
pos=pig[pos].r;
}while(pos!=id1);
return 0;
}
2、决斗
相比起无懈可击,决斗的思维难度会小很多,大致分为以下几个步骤:
1、判定是否有人出无懈可击,有则返回
2、检索双方手牌中杀的数量,并结合谁是发起者进行判定。具体的讲,如果发起者杀的数量>=接受者数量,那么是接受者受伤,否则是发起者受伤。
3、受伤之后一系列关于死亡的判定就顺便更新啦。
4、特别判断题目中给出的特殊情况,即主公对忠臣使用决斗时,忠臣不会出牌这一点。
另外,这些单体攻击的牌都会造成身份的更新,为了避免遗漏我是写在一开始的地方的~
ivoid Fight(int id1,int id2)
{
pig[id1].show=pig[id1].type;//跳忠/反
if(pig[id1].type==1&&pig[id2].type==2){//特判:忠臣被主公diss
pig[id2].HP--;
if(!if_alive(id2))judge(id1,id2);
return;
}
if(prevent(id1,id2,1))return;
rint num1,num2,tmp=0;
num1=query(id1,1);num2=query(id2,1);
if(num1<num2){
dec_some(id1,1,num1);dec_some(id2,1,num1+1);
pig[id1].HP--;
if(!if_alive(id1))judge(id2,id1);
}
else{
dec_some(id1,1,num2);dec_some(id2,1,num2);
pig[id2].HP--;
if(!if_alive(id2))judge(id1,id2);
}
}
3、南蛮入侵,万箭齐发
这两张锦囊牌本质上讲是一样的,只是要求出的牌不同。流程大概如下:
1、对每一个人依次进行判定
2、从发起者(这是重点!)开始逆时针判断是否有人出无懈可击
3、若无,判断手牌是否有闪,有则打出
4、若没有闪,HP–,判定死亡。如果受伤者是主公且发起者的身份未知,还要更新发起者身份为类反
ivoid Loktar_Ogar(int id1)//南(shou)蛮(ren)入侵
{
int pos=pig[id1].r;
while(pos!=id1){
if(prevent(id1,pos,1)){pos=pig[pos].r;continue;}
if(query(pos,1))dec_some(pos,1,1);
else{
pig[pos].HP--;
if(!if_alive(pos))judge(id1,pos);
if(pos==1&&!pig[id1].show)pig[id1].show=4;
}
pos=pig[pos].r;
}
}
ivoid Xiu_xiu_xiu(int id1)//万箭齐发
{
int pos=pig[id1].r;
while(pos!=id1){
if(prevent(id1,pos,1)){pos=pig[pos].r;continue;}
if(query(pos,2))dec_some(pos,2,1);
else{
pig[pos].HP--;
if(!if_alive(pos))judge(id1,pos);
if(pos==1&&!pig[id1].show)pig[id1].show=4;
}
pos=pig[pos].r;
}
}
三、其他牌&流程实现
1、杀
这个倒没什么好说的,具体的判定其实很上文类似,这里不再多提
ivoid Kill(int id1,int id2)
{
pig[id1].show=pig[id1].type;
if(query(id2,2)){
dec_some(id2,2,1);}
else {
pig[id2].HP--;
if(!if_alive(id2))judge(id1,id2);
}
}
2、诸葛连弩
写个equip函数即可。(不知道说什么.jpg)
ivoid equip(int id){if(!pig[id].equ)pig[id].re=1;pig[id].equ=1;return;}
//注意更新re为1,有可能前面有很多杀
3、出牌
主要是对要出的牌进行判定,能否打出&对谁打出。
ivoid Use(int id,int type)
{
switch(type){
case 1:{
if(!use_kill[id]||pig[id].equ)
//use_kill是判断本回合是否打出过杀
if(recognize(id,pig[id].r)==2){
dec_some(id,type,1);
Kill(id,pig[id].r);use_kill[id]=1;
}
break;
}
case 3:
if(pig[id].HP<4){dec_some(id,type,1);pig[id].HP++;}
break;
case 4:{
if(pig[id].type==3){dec_some(id,type,1);Fight(id,1);}
else{
int pos=id;
while((pos=pig[pos].r)!=id){
if(recognize(id,pos)==2){
dec_some(id,type,1);
Fight(id,pos);
break;
}
}
}
break;
}
case 5:{
dec_some(id,type,1);
Loktar_Ogar(id);
pig[id].re=1;
break;
}
case 6:{
dec_some(id,type,1);
Xiu_xiu_xiu(id);
pig[id].re=1;
break;
}
case 8:{
dec_some(id,type,1);
equip(id);
break;
}
}
}
4、主流程
这一部分就是实现一个回合,即从主公开始逆时针一圈依次出牌的过程。
需要特别注意:因为决斗可以对自己造成伤害,所以存在打牌打到一半自己没了的情况,这需要特殊判断。
ivoid Use_cards()
{
int pos=1;
do{
refresh(pos);use_kill[pos]=0;get_card(pos,2);
//洗手牌,标记归零,摸牌
for(rint i=1;i<=pig[pos].num;i++){
int now=pig[pos].hand[i];
if(!now)continue;
//因为中途没有洗手牌,所以中间的某一张可能为空
Use(pos,now);if(!pig[pos].HP)break;
if(pig[pos].re){pig[pos].re=0;i=0;}
//要是摸到了新牌,要从左边开始重新扫
}
refresh(pos);pos=pig[pos].r;
//结束的时候再洗一次手牌,为了降低扫描时的复杂度
}while(pos^1);
}
5、主函数
这题的主函数其实没什么可讲的,就是普通的读入而已,然后在主流程的外面套个while(1)即可。
char hand[N];
signed main()
{
n=rad();m=rad();
for(rint i=1;i<=n;i++){
scanf("%s",hand+1);
pig[i].HP=4;
if(hand[1]=='M')pig[i].type=pig[i].show=1;
if(hand[1]=='Z')pig[i].type=2;
if(hand[1]=='F')pig[i].type=3,num_fan++;//记录反贼数量
pig[i-1].r=i;pig[i].l=i-1;//一开始的顺序
for(rint j=1;j<=4;j++){//读入每个人的手牌
scanf("%s",hand+1);
pig[i].num++;
pig[i].hand[pig[i].num]=get_id(hand[1]);
}
}
pig[1].l=n;pig[n].r=1;
for(rint i=1;i<=m;i++){//读入牌库
scanf("%s",hand+1);
card_num++;card[m-card_num+1]=get_id(hand[1]);
}
while(1)Use_cards();
return 0;
}
最后再说几句:写这道题其实并不会锻炼调试能力,因为这题样例不好出,要发现问题多半是靠数据;但是,此题对思维能力和实现能力有很大帮助,因为这题会要求你想的尽可能的周全,而想好了之后,在实现上有些地方还可以继续简化。
要退役了.jpg,把这道题调完也算是了却了自己的心愿吧~
另附:调试本题一直出事的同学,可以去loj下载数据,然后借助luogu讨论区的战场模拟器(没错就是输出每一步发生了啥)来核查每一步。如果是TLE几个点的可以把cerr给关了再试试。
没错我就是因为没关cerr,T了半天还不知道为啥QAQ