传送门:https://www.luogu.com.cn/problem/P7147
写在前面
大家好我是泥萌可爱的出题人liuzhangfeiabc~
(别骂了我知道错了嘤嘤嘤)
说起来为什么会让我这个大一萌新来出这种题呢……喂这么毒瘤的题除了你还有谁会感兴趣
大家听我掩饰,哦不,解释,事情是这样的……最初他们组好题之后发现按往年惯例缺一道模拟,然后就找到了我
(为什么要找我啊?我也不想啊qwq)
为什么找你你自己心里还不清楚
总之接了这个活之后,按往年惯例一般大模拟会以一个游戏为背景(比如P5380 [THUPC2019]鸭棋),然后我就自然而然地想到去年整个机房被麻将支配的恐惧……
但是吧鉴于我的麻将水平也就那样,雀魂只有一个刚打上雀士1就弃了的号,也就勉强能分清日麻规则和国标规则的程度,于是造题的时候就很随心所欲(比如我题面里用的术语简直是乱七八糟大杂烩),到最后控制不住脑洞了又加了点特殊牌。当然最后出出来的结果是受到了出题组的一致好(tu)评(cao)(逃
行吧我们来看题解,当然作为大模拟我是不是只要写一个“本题按照题意模拟即可”就可以溜了……喂喂喂大家别走啊,喂!
接下来我们一个模块一个模块地看吧:
各种定义
#define gc getchar()
#define pc putchar
#define pii pair<int,int>
#define fi first
#define se second
#define mp make_pair
int pai[200],head = 1,fx = 1;
struct player{
int a[20],sum;
bool pass;
}p[4];
结构体player
定义一名玩家,其中a
数组表示他现有的牌,sum
为手牌数,pass
记录他有没有被pass。
pai
表示牌堆,head
表示牌堆顶,fx
表示游戏进行的顺序。
输入输出处理
我的处理方法是把输入进来的每一张牌变成1~37的数字进行存储,输出的时候再还原成牌的名称。
输入处理就是一堆case:
int i,j,l;
string st;
for(i = 1;i <= 148;++i){
cin>>st;
if(st[0] >= '1' && st[0] <= '9'){
l = st[0] - '0';
switch(st[1]){
case 'M' : pai[i] = l;break;
case 'P' : pai[i] = l + 9;break;
case 'S' : pai[i] = l + 18;break;
}
}
else{
switch(st[0]){
case 'E' : pai[i] = 28;break;
case 'S' : pai[i] = 29;break;
case 'W' : pai[i] = 30;break;
case 'N' : pai[i] = 31;break;
case 'B' : pai[i] = 32;break;
case 'F' : pai[i] = 33;break;
case 'Z' : pai[i] = 34;break;
case 'D' : pai[i] = 35;break;
case 'R' : pai[i] = 36;break;
case 'P' : pai[i] = 37;break;
}
}
}
输出的时候,先写了一个输出某特定编号牌的函数,也没有什么难度:
inline void outpai(int paii){//输出paii这一张牌
if(paii <= 27) cout<<(paii - 1) % 9 + 1<<("MPS"[(paii - 1) / 9]);
else if(paii <= 34) cout<<("ESWNBFZ"[paii - 28]);
else if(paii == 35) cout<<"DOUBLE";
else if(paii == 36) cout<<"REVERSE";
else cout<<"PASS";
}
有了这个之后来实现题目要求的几种输出就比较自然了:
inline void draw(){//输出平局
cout<<"DRAW"<<endl;
exit(0);
}
inline void win(int x){//输出x获胜
pc(x + 'A');pc(' ');cout<<"WIN"<<endl;
exit(0);
}
inline void hupai(int x){//输出x荣和
pc(x + 'A');cout<<" RON"<<endl;
win(x);
}
inline void zimo(int x){//输出x自摸
pc(x + 'A');cout<<" SELFDRAWN"<<endl;
win(x);
}
inline void in(int x,int paii){//输出x摸牌paii
pc(x + 'A');cout<<" IN ";
outpai(paii);pc('\n');
}
inline void out(int x,int paii,int fg = -1){//输出x出牌paii,特判pass
pc(x + 'A');cout<<" OUT ";
outpai(paii);
if(fg >= 0) pc(' '),pc(fg + 'A');
pc('\n');
}
inline void outchi(int x,int paii){//输出x吃,吃的第一张牌是paii
pc(x + 'A');cout<<" CHOW ";
outpai(paii);pc(' ');outpai(paii + 1);pc(' ');outpai(paii + 2);pc('\n');
}
inline void outpeng(int x,int paii){//输出x碰paii
pc(x + 'A');cout<<" PONG ";
outpai(paii);pc(' ');outpai(paii);pc(' ');outpai(paii);pc('\n');
}
进行回合、摸牌
int play(int x){//玩家x进行回合
if(p[x].pass){
p[x].pass = 0;
return nxt(x);
}
mopai(x);
return chupai(x);
}
这个框架很简单。一上来是判pass
,这个nxt
表示下家:
#define nxt(x) (((x) + fx + 4) % 4)
摸牌也相对简单:
inline void mopai(int x){//玩家x摸牌
if(head > 148) draw();
p[x].a[++p[x].sum] = pai[head++];
in(x,p[x].a[p[x].sum]);
sort(p[x].a + 1,p[x].a + p[x].sum + 1);
}
摸完牌最好整理一下手牌。
注意别忘了开局要摸牌:
for(i = 1;i <= 13;++i){
for(j = 0;j < 4;++j) mopai(j);//开局摸牌
}
出牌
int chupai(int x){//出牌
if(p[x].a[p[x].sum] == 37){//pass
--p[x].sum;p[nxt(x)].pass = 1;
out(x,37,nxt(x));
return nxt(x);
}
if(p[x].a[p[x].sum] == 36){//reverse
--p[x].sum;
fx *= -1;
out(x,36);
return nxt(x);
}
if(p[x].a[p[x].sum] == 35){//double
--p[x].sum;
out(x,35);
mopai(x);
return chupai(x);
}
int chu = check(x);
if(chu == -1) zimo(x);
chup(x,chu);
checkhu(x,chu);
int px = checkpeng(chu);
if(px >= 0) return chupai(px);
if(checkchi(nxt(x),chu)) return chupai(nxt(x));
return nxt(x);
}
先判断有没有特殊牌可出,有的话就出,没有就通过check
函数确定下一张出什么/有没有自摸。
确定了出的牌是chu
之后,用chup
函数进行出牌。
再用checkhu
、checkpeng
、checkchi
判断有无和牌、碰、吃。
注意吃碰之后不会摸牌,因此直接进入对应玩家的chupai
函数而不是play
函数。
chup
函数不难实现:
inline void chup(int x,int chu){//玩家x出牌chu
int chid;
for(chid = 1;chid <= p[x].sum;++chid) if(chu == p[x].a[chid]) break;
swap(p[x].a[chid],p[x].a[p[x].sum]);
--p[x].sum;
sort(p[x].a + 1,p[x].a + p[x].sum + 1);
out(x,chu);
}
出完牌也最好整理一下手牌。
接下来我们把重点放在其他几个函数上:
计算和牌距离和下一张出的牌
这可能是整道题里最难的部分,因为其余地方只需要无脑模拟就完事了,最多是写起来复杂一点。
但是这一块就需要有一些技巧。
如果你做过P5279 [ZJOI2019]麻将或者P5301 [GXOI/GZOI2019]宝牌一大堆的话,你应该能反应上来这需要一个dp:
dp[i][j][k][l][g]表示:目前考虑了前i种牌,已经凑齐了j个顺子和刻子、k个对子,计划凑成从第i-1种牌开始的顺子l个,从第i种牌开始的顺子g个,至少要往现在的手牌里添加多少张牌。
同时记录一个zy[i][j][k][l][g]表示在上述情况下,手里最后一张没有用上的手牌是什么。
转移:枚举i+1这种牌要用多少张(至少l+g张,至多4张)。假设要用x张,而手牌里有y张,需要讨论:
1、x<y:这说明手牌没用完,dp数组的值不会增加,zy数组的值变为i+1。
2、x=y:这说明手牌恰好用完,dp数组的值不会增加,zy数组的值不变。
3、x>y:这说明手牌不够,还差了x-y张是需要额外添加的,dp数组的值增加x-y,zy数组的值不变。
对于每个x,需要拿出l+g张用于之前未完成的顺子,剩余的部分可以用于开启新的顺子/凑一个对子/凑一个刻子,这样我们就可以通过讨论计算出下一步转移到的状态是什么。
这一部分的代码如下:
int sl[40],dp[40][5][2][3][3],zy[40][5][2][3][3];
#define zhuanyi(a,b,x,y) if((x) < (a) || ((x) == (a) && (y) > (b))) (a) = (x),(b) = (y)
pii work(int x){//计算和牌距离和下一张出的牌(最后一张没用的牌),x表示要凑出的顺子和刻子数目
memset(dp,0x3f,sizeof(dp));memset(zy,-1,sizeof(zy));
dp[0][0][0][0][0] = 0;
register int i,j,k,l,g,q,nx,ny,r;
for(i = 0;i < 34;++i){
for(j = 0;j <= x;++j){
for(k = 0;k <= 1;++k){
for(l = 0;l <= 2 && l + j <= x;++l){
for(g = 0;g <= 2 && g + l + j <= x;++g) if(dp[i][j][k][l][g] < 15){
for(q = l + g;q <= 4;++q){
nx = dp[i][j][k][l][g] + max(0,q - sl[i + 1]);
ny = (sl[i + 1] > q ? i + 1 : zy[i][j][k][l][g]);
r = q - l - g;
if(r >= 3 && j + l + 1 <= x) zhuanyi(dp[i + 1][j + l + 1][k][g][r - 3],zy[i + 1][j + l + 1][k][g][r - 3],nx,ny);
if(r >= 2 && !k) zhuanyi(dp[i + 1][j + l][k + 1][g][r - 2],zy[i + 1][j + l][k + 1][g][r - 2],nx,ny);
if(r <= 2) zhuanyi(dp[i + 1][j + l][k][g][r],zy[i + 1][j + l][k][g][r],nx,ny);
}
if(i % 9 == 0 || i >= 27) break;
}
if(i % 9 == 0 || i >= 27) break;
}
}
}
}
return mp(dp[34][x][1][0][0],zy[34][x][1][0][0]);
}
int check(int x){//返回应该出哪一张牌,如果已经和了返回-1(因此也可以用于检验是否已经和牌)
memset(sl,0,sizeof(sl));
for(int i = 1;i <= p[x].sum;++i) if(p[x].a[i] <= 34) ++sl[p[x].a[i]];
pii as = work(4 - (14 - p[x].sum) / 3);
return as.se;
}
其中check
函数会对玩家x
的手牌进行分类整理后扔给work
函数。
和牌
inline bool ts(int x){//能和牌的前提是没有特殊牌
for(int i = 1;i <= p[x].sum;++i) if(p[x].a[i] > 34) return 1;
return 0;
}
void checkhu(int x,int chu){//判断能否和牌
for(int i = nxt(x);i != x;i = nxt(i)) if(!ts(i)){
p[i].a[++p[i].sum] = chu;
if(check(i) == -1) hupai(i);
--p[i].sum;
}
}
这里在一开始写的时候遇到了一个bug:如果手牌里有特殊牌,work
函数有可能在和牌距离不为0的时候返回-1,因此提前判了一下。
碰
bool chkpeng(int x,int chu){//碰
memset(sl,0,sizeof(sl));
for(int i = 1;i <= p[x].sum;++i) ++sl[p[x].a[i]];
int lst = work(4 - (14 - p[x].sum) / 3).fi;
if(sl[chu] >= 2){
sl[chu] -= 2;
if(lst > work(3 - (14 - p[x].sum) / 3).fi){
outpeng(x,chu);
p[x].sum = 0;
for(int i = 1;i <= 37;++i){
while(sl[i]) p[x].a[++p[x].sum] = i,--sl[i];
}
return 1;
}
}
return 0;
}
int checkpeng(int chu){//对每一家判断能不能碰
for(int x = 0;x < 4;++x) if(chkpeng(x,chu)) return x;
return -1;
}
所有玩家都能碰,所以要枚举不同的玩家,这里可以这么写是因为自己肯定不会碰自己刚出的牌。
具体实现就是先计算一下和牌距离,然后把手里相应的牌去掉再计算和牌距离,如果减小了就碰,碰完也最好整理一下手牌。
吃
bool checkchi(int x,int chu){//吃
if(chu > 27) return 0;
memset(sl,0,sizeof(sl));
for(int i = 1;i <= p[x].sum;++i) ++sl[p[x].a[i]];
int lst = work(4 - (14 - p[x].sum) / 3).fi;
if(chu % 9 != 8 && chu % 9 != 0 && sl[chu + 1] && sl[chu + 2]){//x x+1 x+2
--sl[chu + 1];--sl[chu + 2];
if(lst > work(3 - (14 - p[x].sum) / 3).fi){
outchi(x,chu);
p[x].sum = 0;
for(int i = 1;i <= 37;++i){
while(sl[i]) p[x].a[++p[x].sum] = i,--sl[i];
}
return 1;
}
++sl[chu + 1];++sl[chu + 2];
}
if(chu % 9 != 1 && chu % 9 != 0 && sl[chu - 1] && sl[chu + 1]){//x-1 x x+1
--sl[chu - 1];--sl[chu + 1];
if(lst > work(3 - (14 - p[x].sum) / 3).fi){
outchi(x,chu - 1);
p[x].sum = 0;
for(int i = 1;i <= 37;++i){
while(sl[i]) p[x].a[++p[x].sum] = i,--sl[i];
}
return 1;
}
++sl[chu - 1];++sl[chu + 1];
}
if(chu % 9 != 1 && chu % 9 != 2 && sl[chu - 2] && sl[chu - 1]){//x-2 x-1 x
--sl[chu - 2];--sl[chu - 1];
if(lst > work(3 - (14 - p[x].sum) / 3).fi){
outchi(x,chu - 2);
p[x].sum = 0;
for(int i = 1;i <= 37;++i){
while(sl[i]) p[x].a[++p[x].sum] = i,--sl[i];
}
return 1;
}
++sl[chu - 2];++sl[chu - 1];
}
return 0;
}
吃与碰的区别在于只有下家能吃,但是有3种可能的吃法,都判断一下就好。其他细节跟碰类似。
大功告成!来看一下完整代码
#include<bits/stdc++.h>
#define gc getchar()
#define pc putchar
#define pii pair<int,int>
#define fi first
#define se second
#define mp make_pair
using namespace std;
int pai[200],head = 1,fx = 1;
struct player{
int a[20],sum;
bool pass;
}p[4];
inline void outpai(int paii){//输出paii这一张牌
if(paii <= 27) cout<<(paii - 1) % 9 + 1<<("MPS"[(paii - 1) / 9]);
else if(paii <= 34) cout<<("ESWNBFZ"[paii - 28]);
else if(paii == 35) cout<<"DOUBLE";
else if(paii == 36) cout<<"REVERSE";
else cout<<"PASS";
}
inline void draw(){//输出平局
cout<<"DRAW"<<endl;
exit(0);
}
inline void win(int x){//输出x获胜
pc(x + 'A');pc(' ');cout<<"WIN"<<endl;
exit(0);
}
inline void hupai(int x){//输出x荣和
pc(x + 'A');cout<<" RON"<<endl;
win(x);
}
inline void zimo(int x){//输出x自摸
pc(x + 'A');cout<<" SELFDRAWN"<<endl;
win(x);
}
inline void in(int x,int paii){//输出x摸牌paii
pc(x + 'A');cout<<" IN ";
outpai(paii);pc('\n');
}
inline void out(int x,int paii,int fg = -1){//输出x出牌paii,特判pass
pc(x + 'A');cout<<" OUT ";
outpai(paii);
if(fg >= 0) pc(' '),pc(fg + 'A');
pc('\n');
}
inline void outchi(int x,int paii){//输出x吃,吃的第一张牌是paii
pc(x + 'A');cout<<" CHOW ";
outpai(paii);pc(' ');outpai(paii + 1);pc(' ');outpai(paii + 2);pc('\n');
}
inline void outpeng(int x,int paii){//输出x碰paii
pc(x + 'A');cout<<" PONG ";
outpai(paii);pc(' ');outpai(paii);pc(' ');outpai(paii);pc('\n');
}
inline void mopai(int x){//玩家x摸牌
if(head > 148) draw();
p[x].a[++p[x].sum] = pai[head++];
in(x,p[x].a[p[x].sum]);
sort(p[x].a + 1,p[x].a + p[x].sum + 1);
}
inline void chup(int x,int chu){//玩家x出牌chu
int chid;
for(chid = 1;chid <= p[x].sum;++chid) if(chu == p[x].a[chid]) break;
swap(p[x].a[chid],p[x].a[p[x].sum]);
--p[x].sum;
sort(p[x].a + 1,p[x].a + p[x].sum + 1);
out(x,chu);
}
int sl[40],dp[40][5][2][3][3],zy[40][5][2][3][3];
#define zhuanyi(a,b,x,y) if((x) < (a) || ((x) == (a) && (y) > (b))) (a) = (x),(b) = (y)
pii work(int x){//计算和牌距离和下一张出的牌(最后一张没用的牌),x表示要凑出的顺子和刻子数目
memset(dp,0x3f,sizeof(dp));memset(zy,-1,sizeof(zy));
dp[0][0][0][0][0] = 0;
register int i,j,k,l,g,q,nx,ny,r;
for(i = 0;i < 34;++i){
for(j = 0;j <= x;++j){
for(k = 0;k <= 1;++k){
for(l = 0;l <= 2 && l + j <= x;++l){
for(g = 0;g <= 2 && g + l + j <= x;++g) if(dp[i][j][k][l][g] < 15){
for(q = l + g;q <= 4;++q){
nx = dp[i][j][k][l][g] + max(0,q - sl[i + 1]);
ny = (sl[i + 1] > q ? i + 1 : zy[i][j][k][l][g]);
r = q - l - g;
if(r >= 3 && j + l + 1 <= x) zhuanyi(dp[i + 1][j + l + 1][k][g][r - 3],zy[i + 1][j + l + 1][k][g][r - 3],nx,ny);
if(r >= 2 && !k) zhuanyi(dp[i + 1][j + l][k + 1][g][r - 2],zy[i + 1][j + l][k + 1][g][r - 2],nx,ny);
if(r <= 2) zhuanyi(dp[i + 1][j + l][k][g][r],zy[i + 1][j + l][k][g][r],nx,ny);
}
if(i % 9 == 0 || i >= 27) break;
}
if(i % 9 == 0 || i >= 27) break;
}
}
}
}
return mp(dp[34][x][1][0][0],zy[34][x][1][0][0]);
}
int check(int x){//返回应该出哪一张牌,如果已经和了返回-1(因此也可以用于检验是否已经和牌)
memset(sl,0,sizeof(sl));
for(int i = 1;i <= p[x].sum;++i) if(p[x].a[i] <= 34) ++sl[p[x].a[i]];
pii as = work(4 - (14 - p[x].sum) / 3);
return as.se;
}
inline bool ts(int x){//能和牌的前提是没有特殊牌
for(int i = 1;i <= p[x].sum;++i) if(p[x].a[i] > 34) return 1;
return 0;
}
#define nxt(x) (((x) + fx + 4) % 4)
void checkhu(int x,int chu){//判断能否和牌
for(int i = nxt(x);i != x;i = nxt(i)) if(!ts(i)){
p[i].a[++p[i].sum] = chu;
if(check(i) == -1) hupai(i);
--p[i].sum;
}
}
bool chkpeng(int x,int chu){//碰
memset(sl,0,sizeof(sl));
for(int i = 1;i <= p[x].sum;++i) ++sl[p[x].a[i]];
int lst = work(4 - (14 - p[x].sum) / 3).fi;
if(sl[chu] >= 2){
sl[chu] -= 2;
if(lst > work(3 - (14 - p[x].sum) / 3).fi){
outpeng(x,chu);
p[x].sum = 0;
for(int i = 1;i <= 37;++i){
while(sl[i]) p[x].a[++p[x].sum] = i,--sl[i];
}
return 1;
}
}
return 0;
}
int checkpeng(int chu){//对每一家判断能不能碰
for(int x = 0;x < 4;++x) if(chkpeng(x,chu)) return x;
return -1;
}
bool checkchi(int x,int chu){//吃
if(chu > 27) return 0;
memset(sl,0,sizeof(sl));
for(int i = 1;i <= p[x].sum;++i) ++sl[p[x].a[i]];
int lst = work(4 - (14 - p[x].sum) / 3).fi;
if(chu % 9 != 8 && chu % 9 != 0 && sl[chu + 1] && sl[chu + 2]){//x x+1 x+2
--sl[chu + 1];--sl[chu + 2];
if(lst > work(3 - (14 - p[x].sum) / 3).fi){
outchi(x,chu);
p[x].sum = 0;
for(int i = 1;i <= 37;++i){
while(sl[i]) p[x].a[++p[x].sum] = i,--sl[i];
}
return 1;
}
++sl[chu + 1];++sl[chu + 2];
}
if(chu % 9 != 1 && chu % 9 != 0 && sl[chu - 1] && sl[chu + 1]){//x-1 x x+1
--sl[chu - 1];--sl[chu + 1];
if(lst > work(3 - (14 - p[x].sum) / 3).fi){
outchi(x,chu - 1);
p[x].sum = 0;
for(int i = 1;i <= 37;++i){
while(sl[i]) p[x].a[++p[x].sum] = i,--sl[i];
}
return 1;
}
++sl[chu - 1];++sl[chu + 1];
}
if(chu % 9 != 1 && chu % 9 != 2 && sl[chu - 2] && sl[chu - 1]){//x-2 x-1 x
--sl[chu - 2];--sl[chu - 1];
if(lst > work(3 - (14 - p[x].sum) / 3).fi){
outchi(x,chu - 2);
p[x].sum = 0;
for(int i = 1;i <= 37;++i){
while(sl[i]) p[x].a[++p[x].sum] = i,--sl[i];
}
return 1;
}
++sl[chu - 2];++sl[chu - 1];
}
return 0;
}
int chupai(int x){//出牌
if(p[x].a[p[x].sum] == 37){//pass
--p[x].sum;p[nxt(x)].pass = 1;
out(x,37,nxt(x));
return nxt(x);
}
if(p[x].a[p[x].sum] == 36){//reverse
--p[x].sum;
fx *= -1;
out(x,36);
return nxt(x);
}
if(p[x].a[p[x].sum] == 35){//double
--p[x].sum;
out(x,35);
mopai(x);
return chupai(x);
}
int chu = check(x);
if(chu == -1) zimo(x);
chup(x,chu);
checkhu(x,chu);
int px = checkpeng(chu);
if(px >= 0) return chupai(px);
if(checkchi(nxt(x),chu)) return chupai(nxt(x));
return nxt(x);
}
int play(int x){//玩家x进行回合
if(p[x].pass){
p[x].pass = 0;
return nxt(x);
}
mopai(x);
return chupai(x);
}
int main(){
int i,j,l;
string st;
for(i = 1;i <= 148;++i){
cin>>st;
if(st[0] >= '1' && st[0] <= '9'){
l = st[0] - '0';
switch(st[1]){
case 'M' : pai[i] = l;break;
case 'P' : pai[i] = l + 9;break;
case 'S' : pai[i] = l + 18;break;
}
}
else{
switch(st[0]){
case 'E' : pai[i] = 28;break;
case 'S' : pai[i] = 29;break;
case 'W' : pai[i] = 30;break;
case 'N' : pai[i] = 31;break;
case 'B' : pai[i] = 32;break;
case 'F' : pai[i] = 33;break;
case 'Z' : pai[i] = 34;break;
case 'D' : pai[i] = 35;break;
case 'R' : pai[i] = 36;break;
case 'P' : pai[i] = 37;break;
}
}
}
for(i = 1;i <= 13;++i){
for(j = 0;j < 4;++j) mopai(j);//开局摸牌
}
int nw = 0;
while(1) nw = play(nw);
return 0;
}
后记
泥萌怎么能说我这题比猪国杀还毒瘤呢qwq
这题std明明只写了3个小时,代码只有6个k,验题人甚至只写了4个k
再说了场上可是有足足两车人过了这题啊qaq