[THUPC 2021] 群星连接-封装神力

[THUPC 2021] 群星连结 题解——封装神力

推荐理由:我过了

题目描述

(太长了放不下)

前言

我在代码注释里加了很多私货,欢迎猜测成分(

一些其他的话

由于我比较孤陋寡闻,对大模拟的认知也一直停留在猪国杀斗地主。直到最近集训,我的朋友给我推荐了这道题,告诉我他调了两个月,写了 20KB 代码才过。遂前往观之。

看模拟题第一步自然是认真读题和看题解,我发现这道题似乎特别模块化,似乎只要代码足够好,就能多次复用。众所周知,我非常喜欢封装,于是爽开!

请叫我封装战神

另外,如果需要题解中提到的一些东西(如数据生成器/分析器),可以翻到最后看参考资料。

正式分析前言

仅是记录过程,不保证足够优秀

接下来是按照时间顺序给出思考过程,不保证按照代码逻辑,有些后加的模块会删掉,因此不保证编译过,仅供参考。想要正常代码对拍或仔细分析什么的直接看最后的完整代码吧……

此外,部分没有坑点/过于冗长的部分被删除,详情见完整代码。这里只给出函数名,毕竟核心目的是逻辑推导,也就是“对函数封装的建议”,自然只负责给出模块的功能就行了。函数内部实现就交给你啦~

通读题目

首先,第一步:认真读题。

注意到题目非常复杂,一定要小心再小心,如果发现了什么坑可以考虑拿纸笔或注释记录一下。此外,由于代码较长,debug 周期也很长,建议写下注释防止忘掉自己干了啥。

然后,一定一定,不要急着写代码。最起码,简单过一遍题,思考一下每个模块都应该干什么,再开始动手。

接下来,我们打开题解,根据题解经验,我们得到了重要的代码构建提示:

玩家开二维方便处理!

这个我一定要提前说,否则你可能就得双倍代码长度了……

这一点如果用代码复用角度来考虑也是相对显然的,毕竟两个玩家决策完全相同,猛猛封装是对的。

好的,接下来开始简单地过一遍题:

基础逻辑

每个角色都有一堆基础参数

这启示我们结构体存角色,无疑是合理的。而且,结构体内部也能封装一堆东西,方便我们操作。

另外,实现上,如果把角色 id 和所属玩家一块封进结构体里,会让后面的传参方便很多。

死亡判定

死亡判定这个东西看着还是有点复杂,而且还很大的影响了后续游戏和输出,因此考虑封装一个模块。

void death_deter(PC &p)//死亡判定

参数溢出、优先目标

无疑是简单的,只要用角色的结构体封装相关函数即可。不过要注意,角色死亡时会更换敌方优先目标,这个可以考虑封装到死亡判定中(或在死亡判定中调用)。

(普通攻击、天赋和技能太杂了,先跳过)

生命恢复和能量恢复

这两个只受参数溢出限制,都不用封装函数。记得注意什么情况下会恢复即可。

生命值扣除

基础逻辑中较为复杂的一个,而且触发频繁,考虑封装函数。

int HPdec_deter(PC &p,int com,int real,bool op=1)
//生命值扣除判定,参数分别是角色、普通伤害和真实伤害。猜猜 op 是什么?

到此为止,基础逻辑基本结束,进入操作逻辑部分。

操作逻辑

普通攻击

一种普通的攻击方式,封装即可。

int BasicAtk(PC &p,bool op)
//参数为发动攻击的角色,猜猜 op 是什么?

天赋

五个天赋很杂,分别在生命值扣除、普攻、己方回合结束和大回合结束时触发,难以整体封装,所以考虑穿插到各个函数中。

技能

技能本身还好,只是一个超级分讨。我们可以开一堆 if 来做。但是,我们发现了一个遗漏的基础逻辑——增益。

我们针对攻击力、防御力、能量回复的调整(增益/减益),分别开三个时间线数组以维护。就像上文说的,要开第二维记录这是哪一方的调整;然后对这个东西进行封装。

int Atkadj[M][2],Defadj[M][2],MPadj[M][2];//下标1 截止时间,下标2 玩家,存储 数值
int MPrec[2];//由于每回合都要恢复,所以直接开一个累计数组较为方便。
void MPadd(int pl,int num,int t)//MP恢复增加,参数:角色,数值,持续时间
void Atkadd(int pl,int num,int t)//攻击调整,参数:角色,数值,持续时间
void Defadd(int pl,int num,int t)//防御调整,参数:角色,数值,持续时间

此外,如果你封装的比较好,你会发现,调整结束等价于新的无限时间的调整,因此该函数可以复用。

最后,给你一个提示,可以考虑单独拎出来技能 10 处理,毕竟它比较特殊——只会被用一次。

攻击逻辑(索敌逻辑)

如果能用技能,这部分较为方便。但是,如果是普攻,计算伤害无疑是麻烦的,和之前的代码有很大的相似但有所不同。尤其是在做大模拟时,我们要时刻怀疑写过的代码是否有问题,如果一段代码多次出现,一旦存在错误很容易出连锁反应。

不难想到,我们给普攻和扣血逻辑添加一个标识符,标识是否为真实操作,这样就可以复用了。

整体逻辑

回合逻辑

欢迎来到最后一部分——整体逻辑。

这里是比较简单的,直接暴力模拟即可。不过,虽然可以直接扔主函数,但是依然可以封装函数,传参为当前行动方,让代码难度降低。

我们可以开一个全局变量存储当前回合,这样无疑是方便的。

int Round;//当前回合
void Attack(int pl)//将己方攻击和己方回合结束压缩为一个函数。
void Roundend()//回合结束

到此为止,你基本上可以在较好的封装下独立写出代码了。若有其他需求可以看代码

那些年我们踩过的坑

  • 技能三写成当前生命值而非最大生命值了
  • 修改了一下单体回血那个技能的逻辑,原本默认是角色 1,但是因为角色 1 不一定存活,就改成了默认 -1 加特判。但是死亡角色 HP 恒定为 0,应该不用改的。
  • pc 的两个下标写反了

代码

一般题会删头尾来着,但是这题比较特殊,所以给出了完整代码。

考虑到可能能启发你调试,保留了调试信息。反正不是因为我懒╮(╯▽╰)╭

另外,尽管我认为注释已经足够详细,但是在这里我仍然给出详细目录:

基础逻辑部分

  • line 31&32,是否存活的 bool 数组和存活人数
  • line 34~74 角色结构体、skill 10 特殊处理和当前回合计数
  • line 81~95 增益调整函数
  • line 97~115 游戏结束判定
  • line 117~136 角色扣血/死亡判定

操作逻辑部分

  • line 139~147 普攻函数
  • line 149~231 技能函数
  • line 134~255 索敌函数

整体逻辑(总函数)部分

  • line 259~286 己方回合和己方回合结束部分(跑两次)
  • line 288~303 回合结束部分
  • line 306~329 主函数

碎碎念

  • line 342~364

AC Code

//I debug, therefore I am.//Code by FarrisL
#include<bits/stdc++.h>
using namespace std;
bool Mst;
#define LL long long
#define For(a,b,c) for(int a=(b);(a)<=(c);(a)++)
#define Rep(a,b,c) for(int a=(b);(a)>=(c);(a)--)
#define FE(a,b) for(int a=hd[b];(a);a=e[a].nx)
const int N=20+3,M=6e5+10,K=20+1,Mod=1e9+9;
const int INF=0x3f3f3f3f;
// const LL INF=0x3f3f3f3f3f3f3f3f,all=1ll;
// #define debug
int n;

template <typename T> void read(T &a){
	char c=getchar();T w=1,f=0;while(c<'0'||c>'9'){if(c=='-') w=-w;c=getchar();}
	while(c>='0'&&c<='9'){f=f*10+c-'0',c=getchar();}a=f*w;
}
template <typename T> void tomin(T &a,const T &b){if(b<a) a=b;}
template <typename T> void tomax(T &a,const T &b){if(a<b) a=b;}
void modadd(int &a,int b,const int &mod=Mod){a+=b;a<0?a+=Mod:0;a>=Mod?a-=Mod:0;}
void modsub(int &a,int b,const int &mod=Mod){a-=b;a<0?a+=Mod:0;a>=Mod?a-=Mod:0;}
void modmul(int &a,int b,const int &mod=Mod){a=1ll*a*b%mod;}
int ksm(int a,int b,int mod=Mod){int res=1;while(b){if(b&1) modmul(res,a,mod);modmul(a,a,mod);b>>=1;}return res;}
int inv(int x){return ksm(x,Mod-2,Mod);}
mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());inline LL rd(LL l,LL r){return rng()%(r-l+1)+l;}

//0 是 Alice,1 是 Bob。
char player[2][10]={"Alice","Bob"};

bool il[N][2];
int lfnum[2];

struct PC{
	int id,pl;//角色 id 和玩家
	int HP,MP,atk,def;
	int hp,mp,datk,ddef,A,D;//可变信息,需更新
	int p[K],now,target;//目标排列与当前目标
	int talent,tx,ty;//天赋相关
	int skill,sx,sy,sz;//技能相关
	void update(){//更新信息。该函数会被滥用,但是复杂度均摊 O(1)。
		A=max(1,atk+datk);
		D=max(0,def+ddef);
		while(!il[p[now]][pl^1]&&now<=n) now++;
		target=p[now];
	}
	void hpadd(int x){hp+=x;tomin(hp,HP);tomax(hp,0);}
	void hpsub(int x){hp-=x;tomin(hp,HP);tomax(hp,0);}
	void mpadd(int x){mp+=x;tomin(mp,MP);tomax(mp,0);}
	void atkadd(int x){datk+=x;update();}
	void defadd(int x){ddef+=x;update();}
	bool islife(){return il[id][pl];}//是否活着
	void input(int d,int player){
		id=d;pl=player;
		read(HP),read(MP),read(atk),read(def);
		hp=HP,mp=0;
		For(i,1,n) read(p[i]);
		read(talent),read(tx),read(ty);
		read(skill),read(sx),read(sy),read(sz);
		update();
	}
	void output(){
		printf("%s %d/%d:\n",player[pl],id,n);
		printf("  HP: %d/%d, MP: %d/%d\n",hp,HP,mp,MP);
		printf("  atk: %d(%d), def: %d(%d)\n",atk,datk,def,ddef);
		printf("  A: %d, D: %d\n",A,D);
		printf("  Talent: %d, x=%d, y=%d\n",talent,tx,ty);
		printf("  Skill: %d, x=%d, y=%d, z=%d\n",skill,sx,sy,sz);
	}
};

PC pc[N][2];//双方角色,千万不要写反下标!!!给一个调试技巧:如果码风优良的话变量格式相对统一,可以使用ctrl+f的查找看有没有(可能的)错误。
int Round;//当前回合
pair <int,int> skill10={-1,-1};//发动方 & 截止时间。

//-----debug-----//
void puts_all(){
	For(pl,0,1) For(x,1,n) pc[x][pl].output();
}

//-----基础&判定函数-----//
int Atkadj[M][2],Defadj[M][2],MPadj[M][2];//下标1 截止时间,下标2 玩家,存储 数值
int MPrec[2];
void MPadd(int pl,int num,int t){//MP恢复增加,参数:角色,数值,持续时间
	if(t!=-1) MPadj[Round+t-1][pl]+=num;
	MPrec[pl]+=num;
}
void Atkadd(int pl,int num,int t){//攻击调整,参数:角色,数值,持续时间
	if(t!=-1) Atkadj[Round+t-1][pl]+=num;
	For(i,1,n) if(pc[i][pl].islife()) pc[i][pl].atkadd(num);//错误三,下标写反了……
}
void Defadd(int pl,int num,int t){//防御调整,参数:角色,数值,持续时间
	if(t!=-1) Defadj[Round+t-1][pl]+=num;
	For(i,1,n) if(pc[i][pl].islife()) pc[i][pl].defadd(num);
}

void gameover(){//游戏结束
	if(!lfnum[0]&&!lfnum[1]){//如果平局,输出任意卖萌表情(-ω-)
		puts("(>.< );");
	}
	else{
		int winner=(lfnum[0]==0);
		printf("%d\n",Round);
		printf("%s\n",winner?"Bob":"Alice");
		For(i,1,n){
			int w=max(pc[i][winner].hp,0);//理论上不会有这种情况,但是万一呢
			printf("%d ",w);
		}
		puts("");
	}
	exit(0);//程序结束
}
void over_deter(){//游戏是否结束相关判断
	if(!lfnum[0]||!lfnum[1]) gameover();
}

void death_deter(PC &p){//死亡判定
	if(p.hp>0) return ;
	int pl=p.pl,x=p.id;
	p.hp=0,il[x][pl]=0,lfnum[pl]--;
	For(i,1,n) pc[i][pl^1].update();//更新敌方攻击列表
	over_deter();
}
int HPdec_deter(PC &p,int com,int real,bool op=1){//生命值扣除判定,op 决定是否真实操作,返回造成伤害
	p.update();
	int D=p.D;
	int damage=max(com-D,0);
	if(p.talent==1) damage+=real-real/2;//天赋 1,「血肉皮囊」,真伤减半
	else damage+=real;
	if(op){
		p.hpsub(damage);
		p.mpadd(1);
		death_deter(p);
	}
	return damage;
}

//-----操作函数-----//
int BasicAtk(PC &p,bool op){//普攻函数,op 决定是否真实操作,返回造成伤害
	p.update();
	int pl=p.pl,y=p.target,A=p.A;
	int damage;
	if(p.talent==2) damage=HPdec_deter(pc[y][pl^1],A,p.tx,op);//天赋 2,「星河力量投射」,普攻附加 x 点真伤
	else if(p.talent==4) damage=HPdec_deter(pc[y][pl^1],0,A,op);//天赋 4,「超凡入圣」,造成真伤但不造成白伤
	else damage=HPdec_deter(pc[y][pl^1],A,0,op);
	return damage;
}

void use_skill(PC &p){
	p.update();
	int pl=p.pl,sk=p.skill;
	int A=p.A,sx=p.sx,sy=p.sy,sz=p.sz;
	p.mp=0;
	if(sk==1){//对敌方所有角色造成 x 伤害,然后全员减 mp/10 能量;
		For(i,1,n){
			PC &y=pc[i][pl^1];
			if(!y.islife()) continue;
			HPdec_deter(y,sx,0);
		}
		For(i,1,n){
			PC &y=pc[i][pl^1];
			if(!y.islife()) continue;
			y.mpadd(-(y.mp/10));
		}
	}else if(sk==2){//全员 A 点真实伤害;
		For(i,1,n){
			PC &y=pc[i][pl^1];
			if(!y.islife()) continue;
			HPdec_deter(y,0,A);
		}
	}else if(sk==3){//全员(函数)伤害;
		For(i,1,n){
			PC &y=pc[i][pl^1];
			if(!y.islife()) continue;
			HPdec_deter(y,min(y.HP/10,sx*A),0);//这里写错过,记得是最大生命值 	
		}
	}else if(sk==4){//全员每回合恢复 sy 能量,持续 sx;
		MPadd(pl,sy,sx);
	}else if(sk==5){//单体永久防御减益加 A 点真伤;
		PC &y=pc[p.target][pl^1];
		y.defadd(-sx);
		HPdec_deter(y,0,A);
	}else if(sk==6){//单体真伤加敌方全员攻击减益;
		PC &y=pc[p.target][pl^1];
		HPdec_deter(y,0,A);
		Atkadd(pl^1,-sy,sx);
	}else if(sk==7){//单体回血加全员攻击增幅;
		int x=-1;
		For(i,1,n){
			PC &p=pc[i][pl];
			if(!p.islife()) continue;
			if(x==-1||p.hp<pc[x][pl].hp) x=i;
		}
		if(x==-1) (cerr<<"in skill 7,x=-1,it's impossible!"),exit(-1);
		pc[x][pl].hpadd(sz);
		Atkadd(pl,sy,sx);
	}else if(sk==8){//全员 A 点伤害加全员防御减益;
		For(i,1,n){
			PC &y=pc[i][pl^1];
			if(!y.islife()) continue;
			HPdec_deter(y,A,0);
		}
		Defadd(pl^1,-sy,sx);
	}else if(sk==9){//全员回血加防御增幅
		For(i,1,n){
			PC &p=pc[i][pl];
			if(!p.islife()) continue;
			p.hpadd(sz);
		}
		Defadd(pl,sy,sx);
	}else if(sk==10){//全力超全开!轮回之终末!
		For(i,1,n){
			PC &p=pc[i][pl];
			if(!p.islife()) continue;
			p.atk*=2,p.def*=2,tomax(p.hp,p.HP/2),tomax(p.mp,p.MP/2);
			p.update();
		}
		MPadd(pl,1,sx);
		skill10={pl,Round+sx-1};
		For(i,1,n){//清空双方的技能 10
			PC &p=pc[i][pl];
			if(!p.islife()) continue;
			if(p.skill==10) p.skill=0;
		}
		For(i,1,n){
			PC &y=pc[i][pl^1];
			if(!y.islife()) continue;
			if(y.skill==10) y.skill=0;
		}
	}
}

//-----预处理(?)函数-----//
pair <int,int> find_target(int pl){//寻找目标,返回角色 id 和攻击方式,为 -1 时为普攻。
	For(i,1,n) pc[i][pl].update();
	int now=-1,x=0;
	For(i,1,n){
		PC &p=pc[i][pl];
		if(!p.islife()) continue;
		if(p.mp<p.MP) continue;
		if(p.skill>=now) now=p.skill,x=p.id;
	}
	if(now!=-1) return {x,now};
	int mxhp=-1,mxdm=-1;
	For(i,1,n){
		PC &p=pc[i][pl];
		if(!p.islife()) continue;
		int y=p.target;
		int damage=BasicAtk(p,0);
		auto choose=[&](){mxhp=pc[y][pl^1].hp,mxdm=damage,x=i;};
		if(pc[y][pl^1].hp>mxhp) choose();
		else if(pc[y][pl^1].hp==mxhp) 
			if(damage>=mxdm) choose();
	}
	return {x,-1};
}

//-----总函数-----//
void Attack(int pl){//将己方攻击和己方回合结束压缩为一个函数。
	//---攻击阶段---//
	auto pi=find_target(pl);
	PC &p=pc[pi.first][pl];
	#ifdef debug
		printf("  %s start\n",player[pl]);
		if(~pi.second) printf("    %s#%d will activate skill %d\n",player[pl],p.id,p.talent);
		else printf("    Normal damage, %s#%d to %s#%d\n",player[pl],p.id,player[pl^1],p.target);
	#endif
	if(~pi.second) use_skill(p);
	else BasicAtk(p,1);
	p.mpadd(1);
	if(p.talent==5){//天赋 5
		if(~pi.second) p.mpadd(p.ty);
		else p.hpadd(p.tx);
	}
	//---回合结束---//
	For(i,1,n){
		PC &p=pc[i][pl];
		if(!p.islife()) continue;
		p.mpadd(1);//正常加 1 能量
		p.mpadd(MPrec[pl]);//全体额外恢复能量
		if(p.talent==3) p.hpadd(p.tx),p.mpadd(p.ty);//天赋 3
		#ifdef debug
			printf("    %s#%d's mp: %d\n",player[pl],i,p.mp);
		#endif
	}
}

void Roundend(){
	if(skill10.second==Round){//技能 10 善后
		lfnum[skill10.first]=0;//因为不会再用到己方角色,所以直接改存活人数即可,不用改血量
		over_deter();
	}
	For(pl,0,1){//结束时间删增益,使用循环让代码复用
		Atkadd(pl,-Atkadj[Round][pl],-1);//直接复用调整函数
		Defadd(pl,-Defadj[Round][pl],-1);
		MPadd(pl,-MPadj[Round][pl],-1);
	}
	#ifdef debug
		printf("Round %d finish\n\n",Round);
		puts_all();
	#endif
	Round++;
}

int Test=1;
void mian()
{
	read(n);
	For(i,0,1){//因为 update 会调用相关信息,所以提前初始化
		For(j,1,n) il[j][i]=1;
		lfnum[i]=n;
	}
	For(i,0,1){
		For(j,1,n) pc[j][i].input(j,i);
	}
	#ifdef debug
		puts_all();
	#endif
	Round++;
	For(i,1,24000){
		#ifdef debug
			printf("Round:%d\n",Round);
		#endif
		Attack(0);
		Attack(1);
		Roundend();
	}
	puts("OvO");
}
bool Med;
signed main()
{
	cerr<<(&Med-&Mst)/1024.0/1024.0<<" MB\n";
	// freopen("P7610.in","r",stdin);
	// freopen("P7610.out","w",stdout);
	// init();
	// cin>>Test;
	while(Test--) mian();
	return 0;
}
/*
那些年我们踩过的坑:

那个输出函数的技能写成当前生命值而非最大生命值了

修改了一下单体回血那个技能的逻辑,原本默认是角色 1,但是因为角色 1 不一定存活,就改成了默认 -1 加特判。但是死亡角色 HP 恒定为 0,应该不用改的。

pc 的两个下标写反了

代码从 2026/7/27 14:00开始写,当日在16:30后暂停。那时应该还差mian函数、Attack函数和Roundend函数,其余大部分已完成。

2026/7/28 16:00左右开始二次动工,17:30左右写完整体框架,然后去玩了。

18:16时回来了一趟并发现过了样例,进行了第一次提交。19:20左右回来,并发现了错误一;提交后由 3pts 变为 4pts……

19:27,抱着怀疑的态度修改了错误二,分数没有任何变化。

19:45左右,通过 ctrl+f 大法,发现了 pc 的下标反了。错误三被发现,但是只改了后面,没改 if,只获得了 5pts。

从题解区和讨论区贺了一堆错误自查和数据生成器/解析器,开始二次调试。

在很唐的把错误三彻底改完后,由于生成器有问题(会输出回合大于23333,疑似无限进行),导致对拍非常受阻。在用手运行了 50+ 组数据后,决定交一发。

于是,20:46,AC。
*/

参考资料

核心参考题解 & py 交易

数据生成器 警告,可能会生成不会结束的数据(推测是回血数值太高了)

数据分析器 输出端为 std::cerr,因此可以用来跑对拍。警告但是有一个笔误,详情可见讨论区二楼(或者叫三楼?)。

注:洛谷题解区中大部分题解都有看过,全列出来太累了,在这里感谢所有人!

以及感谢所有在讨论区中警示后人的所有人!

posted @ 2026-07-29 16:30  FarrisL  阅读(5)  评论(0)    收藏  举报