P8422 题解
yanhao40340 · · 题解
第一道紫题题解!虽然是道模拟。
题意
大体题意就是一个消消乐的游戏过程,多看几遍题面即可。
分析
消消乐的游戏步骤:交换棋子
其中有效操作每满
最后要统计终局奖分。
因为这是一道模拟题,且
交换棋子
交换棋子是这些步骤里最简单的一个。
首先需要判断这两个交换的位置是否相邻。
其次判断这两个位置是否存在棋子。
最后判断交换后盘面能否实现消除。
时间复杂度
bool change(int xa,int ya,int xb,int yb){ //交换棋子,返回值表示是否交换成功
if (abs(xa-xb)+abs(ya-yb)==1) //判断是否相邻
if (a[xa][ya].first&&a[xb][yb].first){ //判断位置是否有棋子
swap(a[xa][ya],a[xb][yb]);
bool res=check(1); //判断能否消除
if (!res){swap(a[xa][ya],a[xb][yb]);return false;} //不能消除则重置盘面
return true;
}
return false;
}
判断是否可消
主要思路是对于每一个棋子向右和向下扫描,判断是否有 dels 中。
时间复杂度
bool check(bool flg){ //判断是否可消,flg 参数表示是否是第一轮消除,返回值表示是否可以消除
dels.clear();
bool can=false; //记录是否消除
for (int i=1;i<=n;++i)
for (int j=1;j<=m;++j){
if (a[i][j].first){ //如果有棋子
int tot=0,tot2=0;
for (int k=0;i+k<=n;++k){ //向下扫描
if (a[i+k][j].first==a[i][j].first) ++tot;
else break;
}
if (tot>=3){ //如果有 3 个及以上的同色棋子
can=true;
if (flg) col[now%5].insert(a[i][j].first); //如果是第一轮消除,那么将消除的颜色加入到这一次的主颜色中
for (int k=0;k<tot;++k)
dels.insert(make_pair(i+k,j)); //把要消除的棋子加入 dels
}
for (int k=0;j+k<=m;++k){ //向右扫描
if (a[i][j+k].first==a[i][j].first) ++tot2;
else break;
}
if (tot2>=3){
can=true;
if (flg) col[now%5].insert(a[i][j].first);
for (int k=0;k<tot2;++k)
dels.insert(make_pair(i,j+k));
}
}
}
return can;
}
统计组合奖分
把所有要消除的点打上标记(代码中的 mp 数组),然后求出所有四连通块即可。
依次对还没有访问过的点进行宽搜,宽搜时访问的点打上已访问标记(代码中的 vis 数组)。
时间复杂度
bitset<maxn> vis[maxn],mp[maxn];
int dir[][2]={{0,1},{0,-1},{1,0},{-1,0}};
void calcsquares(){ //统计组合奖分
queue<pii> q;delsquares.clear();
for (int i=1;i<=n;++i) vis[i].reset(),mp[i].reset();
for (pii it:dels) mp[it.first].set(it.second); //标记所有要消除的点
for (pii it:dels){
if (!vis[it.first][it.second]){ //如果该点还没有访问过,即不在已经搜索的任一四连通块内
q.push(it);int tot=0; //tot 记录连通块大小
vis[it.first].set(it.second);
while (!q.empty()){ //宽搜板子
pii fr=q.front();q.pop();++tot;
int x=fr.first,y=fr.second;
for (int i=0;i<4;++i){
int dx=dir[i][0]+x,dy=dir[i][1]+y;
if (1<=dx&&dx<=n&&1<=dy&&dy<=m){
if (mp[dx].test(dy)&&a[dx][dy].first==a[x][y].first&&!vis[dx].test(dy)){
vis[dx].set(dy);
q.push(make_pair(dx,dy));
}
}
}
}
delsquares.push_back(tot); //记录该四连通块的大小
}
}
for (int it:delsquares)
ans+=(it-3)*(it-3)*50; //计算分数
return;
}
特殊效果
对于每一个功能都按题意模拟实现即可。把所有功能加入到队列中,如果触发了新的功能则将新功能也加入到队列中。
也是使用宽搜(也许叫手写栈的深搜?)实现。
时间复杂度约为
void feature(){ //特殊效果
queue<tuple<int,int,int> > q;
for (pii it:dels)
if (a[it.first][it.second].second){ //如果有特殊效果
q.push(make_tuple(it.first,it.second,a[it.first][it.second].second)); //加入队列
a[it.first][it.second].second=0; //取消效果,否则会被多次加入
}
while (!q.empty()){ //宽搜板子
auto fr=q.front();q.pop();
int x=get<0>(fr),y=get<1>(fr),op=get<2>(fr); //x 和 y 表示坐标,op 表示功能的类型
switch (op){
case 1: //消一行
for (int i=1;i<=m;++i)
if (a[x][i].first){
if (a[x][i].second)
q.push(make_tuple(x,i,a[x][i].second)),a[x][i].second=0;
dels.insert(make_pair(x,i));
}
break;
case 2: //消一列
for (int i=1;i<=n;++i)
if (a[i][y].first){
if (a[i][y].second)
q.push(make_tuple(i,y,a[i][y].second)),a[i][y].second=0;
dels.insert(make_pair(i,y));
}
break;
case 3: //消一行 + 一列
for (int i=1;i<=m;++i)
if (a[x][i].first){
if (a[x][i].second)
q.push(make_tuple(x,i,a[x][i].second)),a[x][i].second=0;
dels.insert(make_pair(x,i));
}
for (int i=1;i<=n;++i)
if (a[i][y].first){
if (a[i][y].second)
q.push(make_tuple(i,y,a[i][y].second)),a[i][y].second=0;
dels.insert(make_pair(i,y));
}
break;
case 4: //消 3*3 区域
for (int i=max(1ll,x-1);i<=min(n,x+1);++i)
for (int j=max(1ll,y-1);j<=min(m,y+1);++j)
if (a[i][j].first){
if (a[i][j].second)
q.push(make_tuple(i,j,a[i][j].second)),a[i][j].second=0;
dels.insert(make_pair(i,j));
}
break;
case 5: //消 5*5 区域
for (int i=max(1ll,x-2);i<=min(n,x+2);++i)
for (int j=max(1ll,y-2);j<=min(m,y+2);++j)
if (a[i][j].first){
if (a[i][j].second)
q.push(make_tuple(i,j,a[i][j].second)),a[i][j].second=0;
dels.insert(make_pair(i,j));
}
break;
case 6: //消同色棋子
for (int i=1;i<=n;++i)
for (int j=1;j<=m;++j)
if (a[i][j].first==a[x][y].first){
if (a[i][j].second)
q.push(make_tuple(i,j,a[i][j].second)),a[i][j].second=0;
dels.insert(make_pair(i,j));
}
break;
default:
break;
}
}
}
统计消除奖分
按题中所给公式模拟即可,没什么好说的。
时间复杂度
void calcchesses(int turns){ //统计消除奖分
for (pii it:dels){
int x=it.first,y=it.second;
ans+=a[x][y].first*turns; //公式
}
return;
}
消除棋子
现在所有要消除的棋子都在 dels 里面,所以直接遍历 dels 逐个进行消除即可。
时间复杂度
void remove(){ //消除棋子
for (pii it:dels){
int x=it.first,y=it.second;
a[x][y].first=0;
}
return;
}
重力下落
对于每一列分开考虑,如果这一个位置有棋子,那么它下落的格数就等于这一列它下面被消除的棋子个数。
所以对每一列做后缀和,统计被消除棋子的个数,即可从
时间复杂度
int zeros[maxn]; //后缀和,表示被消除棋子的个数
void down(){ //重力下落
for (int i=1;i<=m;++i){
for (int j=n;j;--j){ //后缀和
if (!a[j][i].first) zeros[j]=zeros[j+1]+1;
else zeros[j]=zeros[j+1];
}
for (int j=n;j;--j)
if (a[j][i].first) //如果有棋子
if (j+zeros[j]<=n){
pii tmp=a[j][i];
a[j][i]=make_pair(0,0);
a[j+zeros[j]][i]=tmp; //交换
}
}
return;
}
枚举主颜色
显然,对于每一次操作,最少有一个主颜色,最多有两个主颜色。
那么我们就对有两个主颜色的操作进行枚举,因为最多只有
枚举可以使用
时间复杂度
int dfscolors(){ //枚举主颜色,返回值表示这五次操作的最大牌型奖分
vector<int> dbcols; //有 2 个主颜色的操作编号
for (int i=0;i<5;++i)
if (col[i].size()==2) dbcols.push_back(i);
int len=dbcols.size(),range=1<<len,num=0;
vector<int> nowcol;
int maxx=0;
for (int i=0;i<range;++i){ //二进制数枚举
num=0;
for (int j=0;(1<<j)<=i;++j)
if (i&(1<<j)) num|=(1<<dbcols[j]); //计算当前状态对应的数
nowcol.clear();
for (int j=0,k=1;j<5;++j,k<<=1){ //依次取主颜色
if (num&k) nowcol.push_back(*col[j].rbegin());
else nowcol.push_back(*col[j].begin());
}
int res=calccolors(nowcol); //计算
maxx=max(res,maxx);
}
return maxx;
}
统计当前牌型奖分
主要思想就是两次使用映射计算出每种牌型有几种。
cnt 表示一个数出现的次数;
ccnt 表示一种牌型(
时间复杂度
int calccolors(vector<int> cols){ //统计当前牌型奖分,返回值表示当前牌型得分
int cnt[maxk]={0},ccnt[6]={0};
for (int it:cols) ++cnt[it];
for (int i=1;i<=kinds;++i) ++ccnt[cnt[i]];
if (ccnt[1]==5){ //高牌:5 种不同颜色
int maxx=*max_element(cols.begin(),cols.end()); //取最大颜色编号
return 50+maxx;
}
if (ccnt[1]==3&&ccnt[2]==1){ //一对:1 对+ 3 种不同颜色
for (int i=kinds;i;--i)
if (cnt[i]==2) return 100+(i<<1);
}
if (ccnt[1]==1&&ccnt[2]==2){ //两对:2 对+ 1 种不同颜色
int res=200,flg=1;
for (int i=kinds;i;--i)
if (cnt[i]==2) res+=(i<<flg),flg=0;
return res;
}
if (ccnt[3]==1&&ccnt[1]==2){ //三条:3 种相同颜色+ 2 种不同颜色
for (int i=kinds;i;--i)
if (cnt[i]==3) return 300+i*3;
}
if (ccnt[3]==1&&ccnt[2]==1){ //葫芦:3 个相同颜色 + 另外 2 个相同颜色
int res=500;
for (int i=kinds;i;--i)
if (cnt[i]==3) res+=i*3;
else if (cnt[i]==2) res+=i;
return res;
}
if (ccnt[4]==1&&ccnt[1]==1){ //四条:4 个相同颜色 + 1 个其他颜色
for (int i=kinds;i;--i)
if (cnt[i]==4) return 750+i*5;
}
if (ccnt[5]==1){ //五条:5 个颜色全部相同
for (int i=kinds;i;--i)
if (cnt[i]==5) return 1000+i*10;
}
return 0;
}
主函数和剩下的几种得分
这些得分比较简单,我就都放在了主函数里计算。
bool alluse=true; //标记操作是否全部有效
while (q--){
int xa=read(),ya=read(),xb=read(),yb=read();
bool flg=change(xa,ya,xb,yb);
if (flg){ //如果操作有效
calcsquares();
feature();
calcchesses(1);
remove();down();
int turns=1;
while (check(0)){
++turns;
calcsquares();
feature();
calcchesses(turns);
remove();down();
}
ans+=(turns-1)*(turns-1)*80; //统计连锁奖分
++now; //计入一次有效操作
}
else alluse=false; //否则无法得到全部操作有效的终局得分
if (now==5){ //如果有效操作满 5 个
ans+=dfscolors();
for (int i=0;i<5;++i) col[i].clear();
now=0;
}
}
//以下是终局奖分
if (alluse) ans+=1000;
if (delall()) ans+=10000; //delall 函数表示是否全部删除,该函数实现简单,就不放代码了
后记
AC 记录。
这道大模拟属于比较好做的,没有什么坑点,主要在于码量。
另外熟练运用 STL 在一些不卡时间的模拟题中可以减少很多不必要的错误和码量。(仅个人观点)
完结散花!!!