浅谈猪国杀

叠甲 & 观前提醒

大模拟不是什么好东西,能不碰尽量别碰,写这类题耗费的时间精力是无法想象的。你难以想象一个人中午吃完饭回家就打开电脑调题直到下午上课,晚上回家调题到12点,没空看手机看电视持续近一周。估计世界上也没那么多M。
给自己写难受了导致的任何后果与 headless_piston 无关。

在阅读本文时,你可能会有:
头晕,恶心,乏力,肌痛,情绪紊乱等症状。
如果有以上症状,请立即退出本文,站起来活动一下,出去呼吸一下新鲜空气,并反思你为什么要来吃屎,为什么要当 M。


我们第一次听说体量较大的模拟题大概就是 [SDOI2010] 猪国杀了。这是我切掉的第一道紫题,过程可以说非常困难,写了不到一个星期吧。看大家都对这道题比较感兴趣,我就浅谈一下这道题。才不是毁人不倦呢。
那我们开始享受这道题叭~
先读题:[SDOI2010] 猪国杀

题目分析

注意到一个性质:这是一道非常困难的题。
即使你熟知三国杀的规则也一定要认真读题,因为很多规则和三国杀有出入。
这道题的码量较大,代码实现的细节较多。我们可以仔细读题,提取一些细节。关于此题的分析洛谷题解区中的题解已经叙述的比较详细了,这里不多赘述。
以下是我做这道题的一些经验:

  • 出牌时以摸上来的顺序从左往右排列,每次出最靠左的可出的牌。
  • 我们发现从除了桃以外,其他牌再出之后都需要重新遍历手牌。原因大家可以自己想一下,另外大家不需要担心时间复杂度问题,毕竟大模拟还卡时间那么出题人的母亲的身体情况可能无法得到保证,如果你TLE了,请多考虑一下是否出现死循环而不是考虑时间复杂度。
  • 这道题的一个小难点是无懈可击,不难想到可以使用递归解决,具体方式详见代码。
  • 另外,我觉得这种轮流出牌的方式很像链表,而且当有猪死亡时可以快捷地修改,可以有效避免一些死去的猪仍在出牌的状况。
  • 此外就是复杂的身份系统,在出牌时要时刻注意修改身份,尤其注意无懈可击对身份的影响。处理各种身份就是大分讨。
  • 还有就是类反猪这个概念只对主猪有,而对其他猪无效。
  • 使用无懈可击递归时从打出无懈可击的那个猪开始。
  • 决斗的发起者死亡时要立刻结束当前回合。
  • 最容易出错的牌是决斗,如果出错请仔细检查决斗的逻辑。

当然这些只是众多细节中的一小部分,大量细节这里无法展开谈,大家在写这道题时有疑问可以向我提出。其实我个人觉得放代码没什么意义,毕竟每个人的变量命名方式、代码实现方式等等肯定都有差异,我就把丑陋的代码放在这里,略加一些注释方便阅读,当然我希望大家能自己写(如果你真心想写的话)。
总的来说大模拟就是需要细。你越细,就能越少犯错。

代码

BB 了这么多大家也看烦了吧,那么接下来就是大家最爱(?)的代码环节了:

#include<vector>
#include<string>
#include<iostream>
#include<queue>
using namespace std;
const int N=15,M=2010;
queue<char> pai_dui;//牌堆 
int n,m;//与题目中的n,m同义,即开局n只猪,牌堆中m张牌 
int now=1;//当前是哪只猪在出牌 
int cnt;//回合数 
int pigs;//场上剩余总猪数 
int fpigs;//场上剩余反猪数 
char pai_d;//存牌堆里最后一张牌 
bool hui_he_over;//当前回合是否强制结束 
struct pig{
	int shen_fen;//1表示主猪 2表示忠猪 3表示反猪 
	int health=4;//血量 
	bool dead;//是否死亡 
	int tiao;//跳身份:1主猪阵营,2反猪阵营 
	bool zhuang_bei;//是否装备猪哥连弩 
	int bef,nxt;//前一只猪,后一只猪 
	vector<char> shou_pai;//手牌 
}p[N];
bool lei_fan[N];//类反猪 
//桃 P,杀 K,闪 D,决斗 F,南猪 N,万箭 W,无懈 J,猪哥 Z 
void read(){//读入 
	char pai_;//读入的牌 
	string shen_f;//身份 
	scanf("%d%d",&n,&m);
	pigs=n;//总猪数 
	for(int i=1;i<=n;i++){
		cin>>shen_f;
		if(shen_f=="MP")
			p[i].shen_fen=1;//主猪身份为1 
		else if(shen_f=="ZP")
			p[i].shen_fen=2;//忠猪身份为2 
		else
			p[i].shen_fen=3,fpigs++;//反猪身份为3 
		for(int j=1;j<=4;j++){
			cin>>pai_;
			p[i].shou_pai.push_back(pai_);//手牌 
		}
		p[i].bef=i-1;
		p[i].nxt=i+1;//维护链表 
	}
	p[1].tiao=1;//主猪身份明确,属于主猪阵营 
	p[n].nxt=1;
	p[1].bef=n;//调整成环形结构 
	while(m--){
		cin>>pai_d;
		pai_dui.push(pai_d);//牌堆 
	}
}
void tao(int need){//出桃 need为桃的使用者 
	auto it=p[need].shou_pai.begin();//你可以把it视为一个指针 
	while(it!=p[need].shou_pai.end()&&p[need].health<4){//这个结构下面会常见,意思是遍历手牌 
		if(*it=='P'){//*为解引用运算符,*it相当于下标访问it所指向的那个值 
			p[need].health++;
			p[need].shou_pai.erase(it);//打出 
		}
		else
			it++;
	}//注意:一旦使用桃,要一直使用直到不能再用(手牌中桃耗尽或已经回满血),所以上面会有一个p[need].health<4 
	return;
}
void mo_pai(int who,int num){//摸牌 who为摸牌者 num为摸牌数 
	for(int i=1;i<=num;i++){
		p[who].shou_pai.push_back(pai_dui.front());
		pai_dui.pop();
		if(pai_dui.empty())//数据出锅:题目中说若牌已被摸完,则默认一直摸最后一张,这就是上文使用pai_d存储最后一张牌的原因 
			pai_dui.push(pai_d);
	}
	return;
}
bool wu_xie1(int who){//这是真正递归的无懈可击,也就是第一层之后的 
	if(p[who].tiao==1){
		int who1=who;//这里跑环的逻辑很诡异 
		bool huan=1;//首先定义who1用来跑环 
		while(who1!=who||huan){//huan的目的是为了防止who1第一次等于who时,也就是一开始就跳出 
			huan=0; 
			if(p[who1].shen_fen==3){//反猪 
				auto it=p[who1].shou_pai.begin();
				while(it!=p[who1].shou_pai.end()){
					if(*it=='J'){
						p[who1].shou_pai.erase(it);
						p[who1].tiao=2;//跳反 
						lei_fan[who1]=0;//清除类反标记 
						return !wu_xie1(who1);
					}
					it++;
				}
			}
			who1=p[who1].nxt;
		}
	}
	else if(p[who].tiao==2){//同上,区别只在身份的判断上 
		int who1=who;
		bool huan=1;
		while(who1!=who||huan){
			huan=0;
			if(p[who1].shen_fen==1||p[who1].shen_fen==2){
				auto it=p[who1].shou_pai.begin();
				while(it!=p[who1].shou_pai.end()){
					if(*it=='J'){
						p[who1].shou_pai.erase(it);
						p[who1].tiao=1;
						lei_fan[who1]=0;
						return !wu_xie1(who1);
					}
					it++;
				}
			}
			who1=p[who1].nxt;
		}
	}
	return 0;//无人响应,返回false 
}
bool wu_xie(int who){//攻击承受者也就是无懈需求者 
	//这是第一层无懈,之所以这么设计是因为身份的处理较为复杂
	//而之后的无懈只需要无脑向相反阵营出牌就好 
	if(p[who].tiao==0)
		return 0;//没有跳身份的不会有人帮它出 
	if(p[who].tiao==1){//之后的和wu_xie1函数大同小异 
		bool huan=1;
		int who1=now;
		while(who1!=now||huan){
			huan=0;
			if(p[who1].shen_fen==1||p[who1].shen_fen==2){
				auto it=p[who1].shou_pai.begin();
				while(it!=p[who1].shou_pai.end()){
					if(*it=='J'){
						p[who1].shou_pai.erase(it);
						p[who1].tiao=1;
						lei_fan[who1]=0;
						return !wu_xie1(who1);
					}
					else
						it++;
				}
			}
			who1=p[who1].nxt;
		}
	}
	else{
		bool huan=1;
		int who1=now;
		while(who1!=now||huan){
			huan=0;
			if(p[who1].shen_fen==3){
				auto it=p[who1].shou_pai.begin();
				while(it!=p[who1].shou_pai.end()){
					if(*it=='J'){
						p[who1].shou_pai.erase(it);
						p[who1].tiao=2;
						lei_fan[who1]=0;
						return !wu_xie1(who1);
					}
					else
						it++;
				}
			}
			who1=p[who1].nxt;
		}
	}
	return 0;
}
bool bin_si(int who,int lai_yuan){//who濒死者 lai_yuan伤害来源 
	auto it=p[who].shou_pai.begin();
	while(it!=p[who].shou_pai.end()){
		if(*it=='P'){
			p[who].health++;
			p[who].shou_pai.erase(it);
			return 0;
		}
		else
			it++;
	}//濒死者是否有桃 
	if(p[who].shen_fen==1){//主猪死亡,游戏结束 
		p[who].dead=1;
		printf("FP\n");
		for(int i=1;i<n;i++){
			if(p[i].dead)
				printf("DEAD");
			else if(p[i].shou_pai.size()){
				auto it=p[i].shou_pai.begin();
				printf("%c",*it),it++;
				while(it!=p[i].shou_pai.end())
					printf(" %c",*it),it++;
			}
			printf("\n");
		}
		if(p[n].dead)
				printf("DEAD");
		else if(p[n].shou_pai.size()){
			auto it=p[n].shou_pai.begin();
			printf("%c",*it),it++;
			while(it!=p[n].shou_pai.end())
				printf(" %c",*it),it++;
		}
		exit(0);
	}
	else{
		p[who].dead=1;
		p[p[who].bef].nxt=p[who].nxt;
		p[p[who].nxt].bef=p[who].bef;//维护链表 
		if(p[who].shen_fen==3){
			fpigs--;//剩余反猪数 
			if(fpigs==0){//反猪全部死亡 
				printf("MP\n");
				for(int i=1;i<n;i++){
					if(p[i].dead)
						printf("DEAD");
					else if(p[i].shou_pai.size()){
						auto it=p[i].shou_pai.begin();
						printf("%c",*it),it++;
						while(it!=p[i].shou_pai.end())
							printf(" %c",*it),it++;
					}
					printf("\n");
				}
				if(p[n].dead)
						printf("DEAD");
				else if(p[n].shou_pai.size()){
					auto it=p[n].shou_pai.begin();
					printf("%c",*it),it++;
					while(it!=p[n].shou_pai.end())
						printf(" %c",*it),it++;
				}
				exit(0);
			}
			mo_pai(lai_yuan,3);//任何人杀死反猪可以摸3张牌 
		}
		else if(p[who].shen_fen==2&&p[lai_yuan].shen_fen==1){
			p[lai_yuan].shou_pai.clear();//清除手牌 
			p[lai_yuan].zhuang_bei=0;//弃去装备 
			hui_he_over=1;//回合直接结束 
		}//主猪误杀忠猪弃牌惩罚 
	}
	return 0;
}
void nan_wan(int who,char need){//由于南蛮入侵与万箭齐发性质及其类似,所以合并为一个函数 
	int who1=p[who].nxt;//need为响应所需的牌,为杀/K或闪/D 
	while(who1!=who){
		bool xiang_ying=0;//响应成功 
		if(p[who1].tiao)
			if(wu_xie(who1)){
				who1=p[who1].nxt;
				xiang_ying=1;
				continue;
			}
		auto it=p[who1].shou_pai.begin();
		while(it!=p[who1].shou_pai.end()){
			if(*it==need){
				p[who1].shou_pai.erase(it);
				who1=p[who1].nxt;
				xiang_ying=1;
				break;
			}
			it++;
		}
		if(!xiang_ying){
			p[who1].health--;
			if(p[who1].shen_fen==1)
				lei_fan[who]=1;
			if(p[who1].health<1)
				if(bin_si(who1,who))
					return;
			who1=p[who1].nxt;
		}
	}
	return;
}
void jue_dou1(int who,int who1,int cnt1){//决斗1 双方不遗余力地弃置杀,这时候已经杀红了眼了 
	int cnt2=0;//who发起者 who1承受者 cnt1发起者杀的数量 cnt2承受者杀的数量 
	auto it2=p[who1].shou_pai.begin();
	while(it2!=p[who1].shou_pai.end()){
		if(*it2=='K')
			cnt2++;
		it2++;
	}//承受者杀数量 
	if(cnt1>=cnt2){//发起者胜利 
		cnt1-=cnt2;//发起者剩余的杀 
		auto it3=p[who1].shou_pai.begin();
		while(it3!=p[who1].shou_pai.end()){
			if(*it3=='K')
				p[who1].shou_pai.erase(it3);
			else
				it3++;
		}//清空失败者的杀 
		it3=p[who].shou_pai.begin();
		while(it3!=p[who].shou_pai.end()){
			if(*it3=='K'){
				if(cnt1==0)
					p[who].shou_pai.erase(it3);
				else cnt1--,it3++;
			}
			else it3++;
		}//减成功者的杀 
		p[who1].health--;
		if(p[who1].health<1)//判濒死 
			if(bin_si(who1,who))
				return;
	}
	else{//发起者失败 
		cnt2-=cnt1;//胜利者剩余杀 
		cnt2--;//额外多减1 
		auto it3=p[who].shou_pai.begin();
		while(it3!=p[who].shou_pai.end()){
			if(*it3=='K')
				p[who].shou_pai.erase(it3);
			else
				it3++;
		}//清空发起者的杀 
		it3=p[who1].shou_pai.begin();
		while(it3!=p[who1].shou_pai.end()){
			if(*it3=='K'){
				if(cnt2==0)
					p[who1].shou_pai.erase(it3);
				else cnt2--,it3++;
			}
			else it3++;
		}//减胜利者的杀 
		p[who].health--;
		if(p[who].health<1)
			bin_si(who,who1);
		if(p[who].dead)//发起者死了直接结束当前回合 
			hui_he_over=1;
	}
	return;
}
bool jue_dou(int who,auto itk){//决斗 who发起者 itk将打出的决斗这张牌的地址 
	bool used=0;//是否使用 
	//穿itk和used的目的是因为不知道 
	int cnt1=0;//发起者杀的数量
	auto it1=p[who].shou_pai.begin();
	while(it1!=p[who].shou_pai.end()){
		if(*it1=='K')
			cnt1++;//统计发起者杀的数量 
		it1++;
	}
	if(p[who].shen_fen==1){//主猪发起 
		int who1=p[who].nxt;//忠猪无论如何都不会响应主猪 
		while(who1!=who){//找第一个跳反的 
			if(p[who1].tiao==2||(lei_fan[who1]&&p[who1].shen_fen==3)){//对反猪和类反反猪 
				used=1;
				p[who].shou_pai.erase(itk);//使用 
				if(wu_xie(who1))
					return used;
				jue_dou1(who,who1,cnt1);
				return used;
			}
			else if(lei_fan[who1]&&p[who1].shen_fen==2){//对类反忠猪 
				used=1;
				p[who].shou_pai.erase(itk);
				bool xiang_ying=0;
				if(!wu_xie(who1)){
					p[who1].health--;
					if(p[who1].health<1){
						bin_si(who1,who);
					}
					return used;
				}
				if(!xiang_ying){//未响应 
					p[who1].health--;
					if(p[who1].health<1){
						bin_si(who1,who);
					}
					return used;
				}
			}
			else who1=p[who1].nxt;
		}
	}
	else if(p[who].shen_fen==2){//忠猪发起 
		int who1=p[who].nxt;
		while(who1!=who){
			if(p[who1].tiao==2){//目标跳反 
				p[who].tiao=1;
				lei_fan[who]=0;//跳忠 
				used=1;
				p[who].shou_pai.erase(itk);
				if(wu_xie(who1))
					return used;
				jue_dou1(who,who1,cnt1);
				return used;
			}
			who1=p[who1].nxt;
		}
	}
	else{//反猪发起 
		p[who].tiao=2;//反猪只能对主猪决斗 
		lei_fan[who]=0;
		used=1;
		p[who].shou_pai.erase(itk);
		if(wu_xie(1))
			return used;
		jue_dou1(who,1,cnt1);
		return used;
	}
	return 0;
}
bool shan(int who){//闪 who承受者 
	auto it=p[who].shou_pai.begin();
	while(it!=p[who].shou_pai.end()){
		if(*it=='D'){
			p[who].shou_pai.erase(it);
			return 0;
		}
		it++;
	}
	return 1;//0 闪避成功,1 闪避失败
}
bool sha(int who,auto it){//杀 
	bool used=0;
	int who1=p[who].nxt;
	if(p[who].shen_fen==1){//主猪 
		if(p[who1].tiao==2||lei_fan[who1]){//跳反或类反 
			used=1;
			p[who].shou_pai.erase(it);
			if(shan(who1)){
				p[who1].health--;
				if(p[who1].health<1){
					if(bin_si(who1,who))
						return used;
				}
			}
		}
	}
	else if(p[who].shen_fen==2){//忠猪 
		if(p[who1].tiao==2){//跳反 
			p[who].tiao=1;
			lei_fan[who]=0;
			used=1;
			p[who].shou_pai.erase(it);
			if(shan(who1)){
				p[who1].health--;
				if(p[who1].health<1){
					if(bin_si(who1,who))
						return used;
				}
			}
		}
	}
	else{//反猪 
		if(p[who1].tiao==1){//跳忠 
			p[who].tiao=2;
			lei_fan[who]=0;
			used=1;
			p[who].shou_pai.erase(it);
			if(shan(who1)){
				p[who1].health--;
				lei_fan[who]=0;
				if(p[who1].health<1){
					if(bin_si(who1,who))
						return used;
				}
			}
		}
	}
	return used;
}
void chu_pai(int who){//出牌 who出牌者 
	bool able_sha=1;//能否使用杀 
	//除了桃以外,出牌后都应从左到右重新扫 
	hui_he_over=0;
	auto itk=p[who].shou_pai.begin();
	while(p[who].shou_pai.size()&&itk!=p[who].shou_pai.end()&&!hui_he_over){
		switch(*itk){
			case 'Z':{//猪哥连弩 
				p[who].shou_pai.erase(itk);
				p[who].zhuang_bei=1;
				itk=p[who].shou_pai.begin();
				able_sha=1;
				break;
			}
			case 'K':{//杀 
				if(able_sha&&sha(who,itk)){
					able_sha=0;
					itk=p[who].shou_pai.begin();
				}
				else
					itk++;
				if(p[who].zhuang_bei)//无限出杀 
					able_sha=1;
				break;
			}
			case 'F':{//决斗 
				if(jue_dou(who,itk))
					itk=p[who].shou_pai.begin();
				else
					itk++;
				break;
			}
			case 'W':{//万箭 
				p[who].shou_pai.erase(itk);
				nan_wan(who,'D');
				if(p[who].shou_pai.size())
					itk=p[who].shou_pai.begin();
				break;
			}
			case 'N':{//南猪 
				p[who].shou_pai.erase(itk);
				nan_wan(who,'K');
				if(p[who].shou_pai.size())
					itk=p[who].shou_pai.begin();
				break;
			}
			case 'P':{//桃 
				if(p[who].health<4){
					p[who].health++;
					p[who].shou_pai.erase(itk);
				}
				else
					itk++;
				break;
			}
			default:{
				itk++;
				break;
			}
		}
	}
	return;
}
int main(){
	read();
	while(1){
		if(now==1)
			cnt++;
		mo_pai(now,2);
		chu_pai(now);
		now=p[now].nxt;
	}
	return 0;
}

后话

这道题对大家来说应该也算是个挑战。欢迎大家一起来赤石。
这篇文章写得很仓促,也是自己一时兴起和大家的撺掇下写了,不当之处还请大家批评指正,你们要是因为我给你们准备屎吃就骂我的话,我就会……会哭的()。

posted @ 2025-02-13 19:44  headless_piston  阅读(21)  评论(4)    收藏  举报