[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。
*/
参考资料
数据生成器 警告,可能会生成不会结束的数据(推测是回血数值太高了)
数据分析器 输出端为 std::cerr,因此可以用来跑对拍。警告但是有一个笔误,详情可见讨论区二楼(或者叫三楼?)。
注:洛谷题解区中大部分题解都有看过,全列出来太累了,在这里感谢所有人!
以及感谢所有在讨论区中警示后人的所有人!

浙公网安备 33010602011771号