[洛谷 2482,loj 2885][SDOI2010]猪国杀

题目链接

https://www.luogu.org/problem/P2482

https://loj.ac/problem/2885

题解

第一次刷毒瘤模拟题,也碰了好几次壁,这里把自己AC此题的惨痛经历写在这里。

Part 1 存储结构

我们用一个 pig 结构体来存储每只猪的所有信息。

(下面的注释本来都是英文的(主要是 coding 的时候嫌换输入法麻烦),为了方便理解,下面把注释全部换成了中文)

struct pig
{
 int front,next;//以链式结构存储该猪之前的猪和下一只猪
 int id;//猪的身份
        //0为主猪(MP),1为忠猪(ZP),2为反猪(FP)
 int hp;//当前生命值
 int used_card;//该猪累计摸到的牌数(不是当前手中牌数)
 int global_id;//对外展示的身份
               //0为未暴露身份,1为已经暴露身份,2为类反猪
 bool zb;//是否装备诸葛连弩
 bool alive;//是否存活
 char card[2005];//该猪摸过的所有牌
 bool is_used[2005];//对应位置的牌是否使用过
}p[15];

这部分其实问题不算太大,理清思路就可以搞定。

Part 2 准备阶段

在准备阶段,首先需要读入参加游戏的猪的数量 \(n\),以及牌堆大小 \(m\)。

然后,读入每只猪的身份及初始手牌,最后读入牌堆。

void get_initial_info(int cur)//读取每只猪的身份,初始手牌,并做初始化
{
 judge_identity(cur);
 get_initial_card(cur);
 if(cur==1)p[cur].front=n;
 else p[cur].front=cur-1;
 if(cur==n)p[cur].next=1;
 else p[cur].next=cur+1;
 p[cur].alive=true;
 p[cur].hp=4;
}
void judge_identity(int cur)//判断身份
{
 char s[5];
 scanf("%s",s);
 if(s[0]=='M')p[cur].id=0,p[cur].global_id=1;
 else if(s[0]=='Z')p[cur].id=1;
 else p[cur].id=2,alive_fp++;
}
void get_initial_card(int cur)//读入初始牌
{
 char s[5];
 for(int i=1;i<=4;i++)
 {
  scanf("%s",s);
  p[cur].used_card++;
  p[cur].card[i]=s[0];
 }
}
void get_card_queue()//读入牌堆
{
 for(int i=1;i<=m;i++)
 {
  char s[5];
  scanf("%s",s);
  que[i]=s[0];
 }
}

这几个模块实现难度也很低,并没有什么坑点。(唯一的问题就是牌堆事实上并不够用,这个问题后面再说)

Part 3 游戏阶段

游戏阶段是本题的重头戏,坑点众多。下面将会详细讲述。

Part 3.1 游戏阶段框架

框架并不算太复杂,因为绝大多数功能都被封装在函数当中实现了。

//游戏阶段开始
while(res==-1)
 for(int i=1;i<=n;i=p[i].next)
 {
  get_card(i),get_card(i);//摸牌阶段,摸两张牌
  int use_kill_num=0;//没有装备诸葛连弩的猪一回合只能使用一张杀
  for(int j=1;j<=p[i].used_card;j++)
   if(!p[i].is_used[j])//没有使用该手牌就使用它
   {
    bool is_used=false;
    if(p[i].card[j]=='P')is_used=use_peach(i);//吃桃
    else if(p[i].card[j]=='K')//打出杀
    {
     if(use_kill_num==0||p[i].zb)is_used=use_kill(i);
     if(is_used)use_kill_num++;
    }
    else if(p[i].card[j]=='F')is_used=use_fight(i);//打出决斗
    else if(p[i].card[j]=='Z')is_used=use_zgln(i);//装备诸葛连弩
    else if(p[i].card[j]=='N')is_used=use_nzrq(i);//打出南蛮入侵
    else if(p[i].card[j]=='W')is_used=use_wjqf(i);//打出万箭齐发
    if(is_used)
    {
     if(i==1&&clear_mark)clear_mark=false;//这句话是为了避免一个小bug,下面会讲到
     else p[i].is_used[j]=true;
     j=0;//使用一张手牌可能会导致前面的手牌变为可用,因此要从头开始判断每一张牌
    }
    if(!p[i].alive)break;//如果该猪已经死亡,就结束它的阶段
    if(res!=-1)break;//一旦达成胜利条件,游戏立刻终止
   }
  if(res!=-1)break;//一旦达成胜利条件,游戏立刻终止
 }
//游戏阶段结束

以上代码模拟了每个回合的进行过程,几个注意事项还是有必要再说明一下:

  1. 使用一张手牌可能会导致前面的手牌变为可用,因此要从头开始判断每一张牌;
  2. 如果当前出牌的猪挂了,立刻结束它的阶段。

在接下来的部分中,我将详细介绍每个函数的功能及其注意事项。

Part 3.2 摸牌

摸牌事实上并不算难,只需要将摸到的牌给对应的猪即可。

但有一个坑点:牌堆事实上并不够用,如果没牌的情况下,我们要一直摸最后一张牌

void get_card(int cur)//编号为cur的猪摸一张牌
{
 p[cur].card[++p[cur].used_card]=que[curc];
 if(curc!=m)curc++;//牌堆用完的时候,要一直摸最后一张牌
}

Part 3.3 吃桃

这个也很容易实现,根据题意,如果生命值未满,有桃一定吃掉。

bool use_peach(int cur)//编号为cur的猪使用一张桃
{
 if(p[cur].hp<4)//生命未满的时候就吃掉
 {
  p[cur].hp++;
  return true;
 }
 else return false;//生命满的时候就不吃桃了
}

Part 3.4 装备诸葛连弩

虽然这个只需要一行,但为了美观,我们还是封装一下。

bool use_zgln(int cur)//编号为cur的猪装备诸葛连弩
{
 p[cur].zb=true;
 return true;
}

Part 3.5 打出杀/以闪来响应杀

前面几个函数都十分简单,原因在于,这些操作都是对自身的操作,而且不会暴露身份。

从这部分开始,下面的操作都会有选择目标的问题,以及可能造成的身份的暴露。

首先需要注意的是:在本题中,距离是单向的。因此我们在打出杀的时候,只需考虑能否对当前猪的下家使用杀即可。

第二点就是,没有诸葛连弩的猪一回合只能打出一张杀。

第三点,一旦一只猪打出了杀,它的身份就暴露了。

bool use_kill(int cur)//编号为cur的猪打出一张杀
{
 if(p[cur].id==2)//反猪会对主猪和跳忠的猪打出杀
 {
  if(p[p[cur].next].global_id==1&&(p[p[cur].next].id==1||p[p[cur].next].id==0))
  {
   p[cur].global_id=1;//打出杀会暴露身份
   use_shan(p[cur].next);//被杀的猪响应闪
   if(p[p[cur].next].hp<=0)die(p[cur].next,cur);//濒死状态判定
   return true;
  }
 }
 else if(p[cur].id==1)//忠猪会对跳反的猪打出杀
 {
  if(p[p[cur].next].global_id==1&&p[p[cur].next].id==2)
  {
   p[cur].global_id=1;
   use_shan(p[cur].next);
   if(p[p[cur].next].hp<=0)die(p[cur].next,cur);
   return true;
  }
 }
 else //主猪会对跳反的猪或类反猪打出杀
 {
  if((p[p[cur].next].global_id==2)||(p[p[cur].next].global_id==1&&p[p[cur].next].id==2))
  {
   use_shan(p[cur].next);
   if(p[p[cur].next].hp<=0)die(p[cur].next,cur);
   return true;
  }
 }
 return false;//没有可以打出杀的对象
}

以闪来响应杀的过程就简单多了。只需寻找是否有闪,并弃置闪即可。

void use_shan(int cur)//编号为cur的猪需要打出一张闪来响应杀
{
 bool used_shan=false;
 for(int i=1;i<=p[cur].used_card;i++)
  if(p[cur].card[i]=='D'&&p[cur].is_used[i]==false)
  {
   used_shan=true;
   p[cur].is_used[i]=true;
   break;
  }
 if(!used_shan)p[cur].hp--;//没有闪时要掉血
}

Part 3.6 打出南蛮入侵/万箭齐发

这两个锦囊牌几乎相同,所以这里就放一起讲了。

需要注意的是:打出这两张锦囊牌并不会直接暴露自己的身份,但之前没有跳忠/跳反的猪,有可能在打出这两张锦囊牌之后会被主猪判断为类反猪。(已经跳忠/跳反的猪,它们再对主公造成伤害时,不会被判定为类反猪)

这里的几个坑点:

  1. 无懈可击只会免除对一个目标的锦囊效果,并不会使整个锦囊失效;
  2. 在结算南蛮入侵/万箭齐发的时候,一旦游戏结束条件达成,就立刻终止锦囊结算过程。
//这里只给出南蛮入侵的实现,万箭齐发的实现略去
bool use_nzrq(int cur)//编号为cur的猪打出南蛮入侵
{
 for(int i=p[cur].next;i!=cur;i=p[i].next)
 {
  bool used_kill=false;
  if(p[i].global_id==1)//只有暴露身份的猪才有其他猪对它使用无懈可击,原因见下文
   if(use_wxkj(cur,p[i].id==0?1:p[i].id))continue;
  //需要注意,无懈可击只会免除对一个目标的锦囊效果,并不会使整个锦囊失效
  for(int j=1;j<=p[i].used_card;j++)
   if(p[i].card[j]=='K'&&p[i].is_used[j]==false)
   {
    p[i].is_used[j]=true;
    used_kill=true;
    break;
   }
  if(!used_kill)
  {
   p[i].hp--;
   if(p[i].hp<=0)die(i,cur);
   if(i==1&&p[cur].global_id==0)p[cur].global_id=2;
   //没暴露身份,且对主猪造成伤害的猪会被判断为类反猪
   if(res!=-1)return true;//一旦游戏结束条件达成,立刻终止锦囊结算过程
  }
 }
 return true;
}

Part 3.7 决斗

关于寻找决斗目标,坑点也不少:一旦找到决斗目标,该锦囊就立刻被使用(即使该锦囊被无懈,也不能找下一个目标)。

bool use_fight(int cur)//编号为cur的猪打出一张决斗
{
 if(p[cur].id==2)//反猪决斗时总是找主猪
 {
  p[cur].global_id=1;
  bool flag=use_wxkj(cur,1);
  if(!flag)battle(cur,1);//执行决斗过程
  return true;//无论该锦囊是否被无懈,该锦囊都已经被打出
 }
 else if(p[cur].id==1)
 {
  for(int i=p[cur].next;i!=cur;i=p[i].next)
   if(p[i].global_id==1&&p[i].id==2)//忠猪找跳反的猪决斗
   {
    p[cur].global_id=1;
    bool flag=false;
    if(p[i].global_id==1)flag=use_wxkj(cur,2);
    if(!flag)battle(cur,i);
    return true;
   }
 }
 else
 {
  for(int i=p[cur].next;i!=cur;i=p[i].next)
   if((p[i].global_id==1&&p[i].id==2)||p[i].global_id==2)//主猪找跳反的猪和类反猪
   {
    bool flag=false;
    if(p[i].global_id==1)flag=use_wxkj(cur,2);
    if(!flag)battle(1,i);
    return true;
   }
 }
 return false;//没有目标可以进行决斗
}

执行决斗过程时,需要注意:

  1. 决斗的伤害来源是没有受到伤害的一方
  2. 忠猪在与主猪进行决斗时,必定掉血(于是忠猪就白白成为了主猪的牺牲品)。
void battle(int attacker,int defender)//执行决斗过程
{
 if(attacker==1&&p[defender].id==1)//忠猪在与主猪决斗时,必定掉血
 {
  p[defender].hp--;
  if(p[defender].hp<=0)die(defender,attacker);
  return;
 }
 int curp=defender;
 while(1)
 {
  bool flag=false;
  for(int i=1;i<=p[curp].used_card;i++)
   if(p[curp].card[i]=='K'&&p[curp].is_used[i]==false)
   {
    p[curp].is_used[i]=true;
    flag=true;
    break;
   }
  if(flag)curp=(curp==defender?attacker:defender);//轮到对方弃置杀
  else
  {
   p[curp].hp--;
   if(p[curp].hp<=0)die(curp,attacker==curp?defender:attacker);//没有受到伤害的一方为伤害来源
   return;
  }
 }
}

Part 3.8 濒死结算

在看下面的内容之前,请认真阅读下面这句话至少三遍:

注意,一旦达成胜利条件,游戏立刻结束,因此即使会摸3张牌或者还有牌可以用也不用执行了。

这意味着,以下几种情况都是不该发生的:

  1. 把最后一只反猪杀死后,杀死反猪的猪先摸 \(3\) 张牌,然后结束游戏。
  2. 在游戏胜利条件达成后,未完成结算的锦囊继续进行结算,又让几只猪挂掉。

情况2的应对方法刚才在讲南蛮入侵的实现时已经给出了解决方案。这里不再赘述。

现在就可以给出濒死结算的判断代码了:

void die(int cur,int killer)//编号为cur的猪正处在濒死状态,杀手为killer
{
 for(int i=1;i<=p[cur].used_card;i++)
  if(p[cur].card[i]=='P'&&p[cur].is_used[i]==false)
  {
   p[cur].hp++;
   p[cur].is_used[i]=true;
   if(p[cur].hp>0)return;//该猪摆脱了濒死状态
  }
 p[cur].alive=false;//将该猪标记为死亡
 p[p[cur].front].next=p[cur].next;//改变出牌顺序
 p[p[cur].next].front=p[cur].front;
 if(p[cur].id==0)//主猪死亡,反猪胜利
 {
  res=1;
  return;
 }
 if(p[cur].id==2)
 {
  alive_fp--;
  if(alive_fp==0)//先判断游戏胜利条件是否达成,如果达成,就不再摸牌了
  {
   res=0;
   return;
  }
  get_card(killer),get_card(killer),get_card(killer);//杀死反猪的猪摸3张牌
 }
 else if(p[cur].id==1&&killer==1)//主猪杀死忠猪,弃置所有手牌和装备
 {
  p[1].used_card=0;//清空牌堆
  memset(p[1].card,0,sizeof(p[1].card));
  memset(p[1].is_used,false,sizeof(p[1].is_used));
  p[1].zb=false;//卸掉装备
  clear_mark=true;//防止牌打出状态被意外改变
 }
}

你也许会奇怪开始程序框架中的 clear_mark 是干什么的,现在可以给出答案了。

这其实是一个特殊的标记。

由于 mark 已经使用的牌是在牌结算完成之后(也在濒死结算之后),mark 操作会导致已经被清空的主猪牌堆中的一张不存在的牌被打上一个错误的标记。(这个小 bug 导致我本来 AC 的程序变成了 \(95\) 分)

所以,通过这个标记,我们判断主猪的标记是否被清空,如果主猪的标记已经被清空,就不要打错误的标记了。

Part 3.9 打出无懈可击

如果你完成了上面几个部分的内容,那么恭喜你已经拿到了 \(30\) 分!

接下来就是整个程序的重头戏:无懈可击。

似乎无懈可击的使用规则很复杂?那就重新读一遍下面的内容吧!

献殷勤:使用无懈可击挡下南猪入侵、万箭齐发、决斗;使用无懈可击抵消表敌意;

表敌意:……使用无懈可击抵消献殷勤;

共性:……不会对未表明身份的猪献殷勤(包括自己)

从上面这段话中,我们可以总结出以下几个规律:

  1. 对于一个没有暴露身份的猪,没有猪能给它出无懈可击。(它自己也不行)
  2. 因为在该问题中,所有的锦囊都会对锦囊目标造成伤害,那么假如锦囊牌的目标为 A,那么和 A 一派的猪,一定会使锦囊无效(即对 A 献殷勤);不和 A 一派的猪,一定会使锦囊生效(即对 A 表敌意)。
  3. 一只猪打出了无懈可击,就会暴露自己的身份。

似乎有了一点头绪:我们可以将出无懈可击的过程用递归实现。

怎么实现呢?设主猪一派为 \(1\) 方,反猪一派为 \(2\) 方。假如有人对 \(1\) 方的猪出了一张锦囊,那么就先由 \(1\) 方的猪出无懈可击抵销该锦囊的效果,然后由 \(2\) 方抵销 \(1\) 方出的无懈可击,以此类推。(反之也同理)

实现时有几个注意事项:

  1. 出无懈可击的顺序是:从使用锦囊的猪开始,按逆时针顺序出无懈可击。(之前误以为是从锦囊的目标开始,结果错的很惨)
  2. 别忘了把出无懈可击的猪的身份暴露出来。
bool use_wxkj(int cur,int sit)
//轮到编号cur的猪出无懈可击,锦囊牌的目标为sit一方(1代表主猪一方,2代表反猪一方)
{
 int curp=cur;
 do
 {
  if(p[curp].id!=2&&sit==1)
  {
   for(int i=1;i<=p[curp].used_card;i++)
    if(p[curp].card[i]=='J'&&p[curp].is_used[i]==false)
    {
     p[curp].is_used[i]=true;
     if(p[curp].global_id!=1)p[curp].global_id=1;
     return !use_wxkj(curp,2);//轮到相反一方出无懈可击
    }
  }
  if(p[curp].id==2&&sit==2)
  {
   for(int i=1;i<=p[curp].used_card;i++)
    if(p[curp].card[i]=='J'&&p[curp].is_used[i]==false)
    {
     p[curp].is_used[i]=true;
     if(p[curp].global_id!=1)p[curp].global_id=1;
     return !use_wxkj(curp,1);
    }
  }
  curp=p[curp].next;
 }while(curp!=cur);
 return false;
}

Part 4 输出结果阶段

如果你看完了前面这一大段内容,这剩下的内容就简单多了。

//输出结果阶段开始
if(res==0)puts("MP");
else puts("FP");
for(int i=1;i<=n;i++)
{
 if(!p[i].alive)
 {
  puts("DEAD");
  continue;
 }
 for(int j=1;j<=p[i].used_card;j++)
  if(!p[i].is_used[j])printf("%c ",p[i].card[j]);
 puts("");
}
//输出结果阶段结束

完整代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct pig
{
 int front,next;//以链式结构存储该猪之前的猪和下一只猪
 int id;//猪的身份
        //0为主猪(MP),1为忠猪(ZP),2为反猪(FP)
 int hp;//当前生命值
 int used_card;//该猪累计摸到的牌数(不是当前手中牌数)
 int global_id;//对外展示的身份
               //0为未暴露身份,1为已经暴露身份,2为类反猪
 bool zb;//是否装备诸葛连弩
 bool alive;//是否存活
 char card[2005];//该猪摸过的所有牌
 bool is_used[2005];//对应位置的牌是否使用过
}p[15];
int n,m,curc=1;//curc:牌堆顶端位置
int alive_fp;//存活猪的数量
int res=-1;//游戏结果
           //-1:游戏未结束,0:主猪胜,1:反猪胜
char que[2005];//card queue(a.k.a. pai dui)
bool clear_mark;//主猪牌堆清空标记(防止意外标记)
void judge_identity(int cur)//判断身份
{
 char s[5];
 scanf("%s",s);
 if(s[0]=='M')p[cur].id=0,p[cur].global_id=1;
 else if(s[0]=='Z')p[cur].id=1;
 else p[cur].id=2,alive_fp++;
}
void get_initial_card(int cur)//读入初始牌
{
 char s[5];
 for(int i=1;i<=4;i++)
 {
  scanf("%s",s);
  p[cur].used_card++;
  p[cur].card[i]=s[0];
 }
}
void get_card(int cur)//编号为cur的猪摸一张牌
{
 p[cur].card[++p[cur].used_card]=que[curc];
 if(curc!=m)curc++;//牌堆用完的时候,要一直摸最后一张牌
}
void die(int cur,int killer)//编号为cur的猪正处在濒死状态,杀手为killer
{
 for(int i=1;i<=p[cur].used_card;i++)
  if(p[cur].card[i]=='P'&&p[cur].is_used[i]==false)
  {
   p[cur].hp++;
   p[cur].is_used[i]=true;
   if(p[cur].hp>0)return;//该猪摆脱了濒死状态
  }
 p[cur].alive=false;//将该猪标记为死亡
 p[p[cur].front].next=p[cur].next;//改变出牌顺序
 p[p[cur].next].front=p[cur].front;
 if(p[cur].id==0)//主猪死亡,反猪胜利
 {
  res=1;
  return;
 }
 if(p[cur].id==2)
 {
  alive_fp--;
  if(alive_fp==0)//先判断游戏胜利条件是否达成,如果达成,就不再摸牌了
  {
   res=0;
   return;
  }
  get_card(killer),get_card(killer),get_card(killer);//杀死反猪的猪摸3张牌
 }
 else if(p[cur].id==1&&killer==1)//主猪杀死忠猪,弃置所有手牌和装备
 {
  p[1].used_card=0;//清空牌堆
  memset(p[1].card,0,sizeof(p[1].card));
  memset(p[1].is_used,false,sizeof(p[1].is_used));
  p[1].zb=false;//卸掉装备
  clear_mark=true;//防止牌打出状态被意外改变
 }
}
bool use_peach(int cur)//编号为cur的猪使用一张桃
{
 if(p[cur].hp<4)//生命未满的时候就吃掉
 {
  p[cur].hp++;
  return true;
 }
 else return false;//生命满的时候就不吃桃了
}
bool use_wxkj(int cur,int sit)
//轮到编号cur的猪出无懈可击,锦囊牌的目标为sit一方(1代表主猪一方,2代表反猪一方)
{
 int curp=cur;
 do
 {
  if(p[curp].id!=2&&sit==1)
  {
   for(int i=1;i<=p[curp].used_card;i++)
    if(p[curp].card[i]=='J'&&p[curp].is_used[i]==false)
    {
     p[curp].is_used[i]=true;
     if(p[curp].global_id!=1)p[curp].global_id=1;
     return !use_wxkj(curp,2);//轮到相反一方出无懈可击
    }
  }
  if(p[curp].id==2&&sit==2)
  {
   for(int i=1;i<=p[curp].used_card;i++)
    if(p[curp].card[i]=='J'&&p[curp].is_used[i]==false)
    {
     p[curp].is_used[i]=true;
     if(p[curp].global_id!=1)p[curp].global_id=1;
     return !use_wxkj(curp,1);
    }
  }
  curp=p[curp].next;
 }while(curp!=cur);
 return false;
}
void use_shan(int cur)//编号为cur的猪需要打出一张闪来响应杀
{
 bool used_shan=false;
 for(int i=1;i<=p[cur].used_card;i++)
  if(p[cur].card[i]=='D'&&p[cur].is_used[i]==false)
  {
   used_shan=true;
   p[cur].is_used[i]=true;
   break;
  }
 if(!used_shan)p[cur].hp--;//没有闪时要掉血
}
bool use_kill(int cur)//编号为cur的猪打出一张杀
{
 if(p[cur].id==2)//反猪会对主猪和跳忠的猪打出杀
 {
  if(p[p[cur].next].global_id==1&&(p[p[cur].next].id==1||p[p[cur].next].id==0))
  {
   p[cur].global_id=1;
   use_shan(p[cur].next);//被杀的猪响应闪
   if(p[p[cur].next].hp<=0)die(p[cur].next,cur);//濒死状态判定
   return true;
  }
 }
 else if(p[cur].id==1)//忠猪会对跳反的猪打出杀
 {
  if(p[p[cur].next].global_id==1&&p[p[cur].next].id==2)
  {
   p[cur].global_id=1;
   use_shan(p[cur].next);
   if(p[p[cur].next].hp<=0)die(p[cur].next,cur);
   return true;
  }
 }
 else //主猪会对跳反的猪或类反猪打出杀
 {
  if((p[p[cur].next].global_id==2)||(p[p[cur].next].global_id==1&&p[p[cur].next].id==2))
  {
   use_shan(p[cur].next);
   if(p[p[cur].next].hp<=0)die(p[cur].next,cur);
   return true;
  }
 }
 return false;//没有可以打出杀的对象
}
void battle(int attacker,int defender)//执行决斗过程
{
 if(attacker==1&&p[defender].id==1)//忠猪在与主猪决斗时,必定掉血
 {
  p[defender].hp--;
  if(p[defender].hp<=0)die(defender,attacker);
  return;
 }
 int curp=defender;
 while(1)
 {
  bool flag=false;
  for(int i=1;i<=p[curp].used_card;i++)
   if(p[curp].card[i]=='K'&&p[curp].is_used[i]==false)
   {
    p[curp].is_used[i]=true;
    flag=true;
    break;
   }
  if(flag)curp=(curp==defender?attacker:defender);//轮到对方弃置杀
  else
  {
   p[curp].hp--;
   if(p[curp].hp<=0)die(curp,attacker==curp?defender:attacker);//没有受到伤害的一方为伤害来源
   return;
  }
 }
}
bool use_fight(int cur)//编号为cur的猪打出一张决斗
{
 if(p[cur].id==2)//反猪决斗时总是找主猪
 {
  p[cur].global_id=1;
  bool flag=use_wxkj(cur,1);
  if(!flag)battle(cur,1);//执行决斗过程
  return true;//无论该锦囊是否被无懈,该锦囊都已经被打出
 }
 else if(p[cur].id==1)
 {
  for(int i=p[cur].next;i!=cur;i=p[i].next)
   if(p[i].global_id==1&&p[i].id==2)//忠猪找跳反的猪决斗
   {
    p[cur].global_id=1;
    bool flag=false;
    if(p[i].global_id==1)flag=use_wxkj(cur,2);
    if(!flag)battle(cur,i);
    return true;
   }
 }
 else
 {
  for(int i=p[cur].next;i!=cur;i=p[i].next)
   if((p[i].global_id==1&&p[i].id==2)||p[i].global_id==2)//主猪找跳反的猪和类反猪
   {
    bool flag=false;
    if(p[i].global_id==1)flag=use_wxkj(cur,2);
    if(!flag)battle(1,i);
    return true;
   }
 }
 return false;//没有目标可以进行决斗
}
bool use_zgln(int cur)//编号为cur的猪装备诸葛连弩
{
 p[cur].zb=true;
 return true;
}
bool use_nzrq(int cur)//编号为cur的猪打出南蛮入侵
{
 for(int i=p[cur].next;i!=cur;i=p[i].next)
 {
  bool used_kill=false;
  if(p[i].global_id==1)//只有暴露身份的猪才有其他猪对它使用无懈可击
   if(use_wxkj(cur,p[i].id==0?1:p[i].id))continue;
  //需要注意,无懈可击只会免除对一个目标的锦囊效果,并不会使整个锦囊失效
  for(int j=1;j<=p[i].used_card;j++)
   if(p[i].card[j]=='K'&&p[i].is_used[j]==false)
   {
    p[i].is_used[j]=true;
    used_kill=true;
    break;
   }
  if(!used_kill)
  {
   p[i].hp--;
   if(p[i].hp<=0)die(i,cur);
   if(i==1&&p[cur].global_id==0)p[cur].global_id=2;
   //没暴露身份,且对主猪造成伤害的猪会被判断为类反猪
   if(res!=-1)return true;//一旦游戏结束条件达成,立刻终止锦囊结算过程
  }
 }
 return true;
}
bool use_wjqf(int cur)
{
 for(int i=p[cur].next;i!=cur;i=p[i].next)
 {
  bool used_kill=false;
  if(p[i].global_id==1)
   if(use_wxkj(cur,p[i].id==0?1:p[i].id))continue;
  for(int j=1;j<=p[i].used_card;j++)
   if(p[i].card[j]=='D'&&p[i].is_used[j]==false)
   {
    p[i].is_used[j]=true;
    used_kill=true;
    break;
   }
  if(!used_kill)
  {
   p[i].hp--;
   if(p[i].hp<=0)die(i,cur);
   if(i==1&&p[cur].global_id==0)p[cur].global_id=2;
   if(res!=-1)return true;
  }
 }
 return true;
}
void get_initial_info(int cur)//读取每只猪的身份,初始手牌,并做初始化
{
 judge_identity(cur);
 get_initial_card(cur);
 if(cur==1)p[cur].front=n;
 else p[cur].front=cur-1;
 if(cur==n)p[cur].next=1;
 else p[cur].next=cur+1;
 p[cur].alive=true;
 p[cur].hp=4;
}
void get_card_queue()//读入牌堆
{
 for(int i=1;i<=m;i++)
 {
  char s[5];
  scanf("%s",s);
  que[i]=s[0];
 }
}
int main()
{
 //准备阶段开始
 scanf("%d%d",&n,&m);
 for(int i=1;i<=n;i++)
  get_initial_info(i);
 get_card_queue();
 //准备阶段结束
 //游戏阶段开始
 while(res==-1)
  for(int i=1;i<=n;i=p[i].next)
  {
   get_card(i),get_card(i);//摸牌阶段,摸两张牌
   int use_kill_num=0;
   //没有装备诸葛连弩的猪一回合只能使用一张杀
   for(int j=1;j<=p[i].used_card;j++)
    if(!p[i].is_used[j])//没有使用该手牌就使用它
    {
     bool is_used=false;
     if(p[i].card[j]=='P')is_used=use_peach(i);//吃桃
     else if(p[i].card[j]=='K')//使用杀
     {
      if(use_kill_num==0||p[i].zb)is_used=use_kill(i);
      if(is_used)use_kill_num++;
     }
     else if(p[i].card[j]=='F')is_used=use_fight(i);//打出决斗
     else if(p[i].card[j]=='Z')is_used=use_zgln(i);//装备诸葛连弩
     else if(p[i].card[j]=='N')is_used=use_nzrq(i);//打出南蛮入侵
     else if(p[i].card[j]=='W')is_used=use_wjqf(i);//打出万箭齐发
     if(is_used)
     {
      if(i==1&&clear_mark)clear_mark=false;//这句话是为了避免一个小bug
   else p[i].is_used[j]=true;
      j=0;//使用一张手牌可能会导致前面的手牌变为可用,因此要从头开始判断每一张牌
     }
     if(!p[i].alive)break;//如果该猪已经死亡,就结束它的阶段
     if(res!=-1)break;//一旦达成胜利条件,游戏立刻终止
    }
   if(res!=-1)break;//一旦达成胜利条件,游戏立刻终止
  }
 //游戏阶段结束
 //输出结果阶段开始
 if(res==0)puts("MP");
 else puts("FP");
 for(int i=1;i<=n;i++)
 {
  if(!p[i].alive)
  {
   puts("DEAD");
   continue;
  }
  for(int j=1;j<=p[i].used_card;j++)
   if(!p[i].is_used[j])printf("%c ",p[i].card[j]);
  puts("");
 }
 //输出结果阶段结束
 return 0;
}

后记

为什么突然要想到刷这么大一道模拟题呢?其实是在 NOIp2018 原地爆炸之后突然兴起的一个毒瘤想法(?)。

11 月 13 号就开始打这道题,打了一个星期,绝大多数功能都实现的差不多了(然而这部分内容也就是 10pts 的部分…),由于新的训练计划的到来,A 掉这道毒瘤题的计划,就暂时咕咕了。(结果咕了整整一个月)

前几天重新把这题拾起来,把剩下的部分实现之后,就是漫长的调试过程。

第一次编译的时候,跳出来一堆错误信息(重构函数的时候忘了改调用)。好不容易排除了编译错误之后,样例(意料之中)挂了。

于是开始单步跟踪每只猪的状态,这个时候多亏之前有人留下的模拟器,让调试过程方便了许多。

就这样把一个接一个的bug翻出来,提交的分数从刚开始的 \(5\) 分,到最后的 \(100\) 分(中间还有两次提交忘了删文件 IO),真是艰难的历程。(提交记录戳这里

这题虽然极其毒瘤,但对于代码能力提升的帮助,也是很大的。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据