P7147 [THUPC2021 初赛] 麻将模拟器
传说中比猪国杀更牛逼的模拟?
话说这场初赛还打过,当时这个题都没怎么看(
主要是前几天打 PKUSC 的时候有个判断向听数的题,于是就想到来写这个了
判断向听的时候用一个 dp,设 \(f(i,a,b,p,q,h)\) 表示考虑到第 \(i\) 中牌,之前有 \(a\) 对 \((i-1,i)\) 的牌(需要补一个 \(i+1\) 组成顺子),有 \(b\) 个 \(i\) 牌(需要补一个 \(i+1,i+2\) 组成顺子),一共扔了 \(p\) 张牌,一共拿进来了 \(q\) 张牌,目前有/没有雀头 的情况,是否可以达成
转移就是枚举加入(或扔掉)了几张 \(i+1\),第 \(i+1\) 种牌是否要组成刻子,是否要组成去偷,然后所有的 \(b\) 变成 \(a\),除去雀头、刻子剩下的牌变成 \(b\)
一万、一索、一筒、字牌的转移和别的牌不太相同,只有 \(f(i,0,0,p,q,h)\) 的状态可以转移到他们
技能牌则是必须扔掉,这个可以用限制技能牌一共有 \(0\) 张,其他的一共有 \(4\) 张来实现,不用单独讨论
然后显然最后合理的状态是要满足 \(p=q\) 的,而这个 \(p\)(或 \(q\)) 就是胡牌距离(注意题面里说的胡牌距离是向听数加一)
由于技能牌的存在,且不考虑七对子,所以最大胡牌距离是 \(12\)(给你发 \(12\) 张技能牌)
因为一共有 \(37\) 种牌,所以最后找最小的 \(dis\) 满足 \(f(37,0,0,dis,dis,1)=1\)
因为还要找到最大的能打的牌,所以要再记录一个数组表示能达成此状态的前提下,扔掉的牌中最大是多少
这个 dp 的状态数是 \(38\times 5\times 5\times 13\times 13\times 2\),每个状态的转移最多有 \(12\)(相同的牌一共有 \(4\) 张,所以扔掉、添加一共有 \(4\) 种情况,再加上有雀头、有刻子、没有雀头或刻子三种情况)
看起来复杂度不低,但是显然完全跑不满;考虑一共要做多少次这样的 dp,配牌后还剩 \(96\) 张牌,这些要判断切什么牌一次、切完判断别人胡不胡这个四次,所以固定有 \(480\) 次,而一局种能副露的情况实际非常少,基本不用考虑
所以每次吃碰、判断是否荣和都只要暴力跑这个 dp 就行,并不需要什么其他的骚操作(
判断吃碰的时候要把拿出来副露的两张牌从手牌里删掉跑 dp,而不是把要被吃碰的那张牌加进去,否则可能会导致那张加进去的牌和别的牌(不是要副露的那两张)组成面子或雀头
另外副露了的牌就没用了,直接删了就行不用记录
然后他甚至不需要存牌山(
具体细节上用一个 cardNum
定义这种牌最多多少张,用 isBeginCard
定义这种牌是否是一万、一索、一筒、字牌、技能牌
然后吃的时候,如果你存所有牌的编号是连着的,不要不要出现拿 89M 去吃 1P,或者吃了字牌这种事(
所以下一步是不是要出个在向听数最小的情况下要求用进张数最广的打法的题,再下一步是不是要出个考虑役种的题,再下一步是不是要和三个交互库一块打麻将(
代码只有 5K 多点,甚至比某些数据结构题还短,感觉除了没怎么封装外还挺好看(
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<assert.h>
const int _nex[2][4]={{1,2,3,0},{3,0,1,2}};
const int *nex=_nex[0];
int order=0;
const char playerName[]={'A','B','C','D'};
const char *cardName[]={
"",
"1M","2M","3M","4M","5M","6M","7M","8M","9M",
"1P","2P","3P","4P","5P","6P","7P","8P","9P",
"1S","2S","3S","4S","5S","6S","7S","8S","9S",
"E","S","W","N","B","F","Z",
"DOUBLE","REVERSE","PASS"
};
const int isBeginCard[]={1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1};
const int cardNum[]={0,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,0,0,0};
#define CARD_TYPE_NUM 37
#define CARD_TYPE_NUM2 34
#define PLAYER_NUM 4
#define MAX_DIS 12
char input[12];
inline int strMatch(const char *s,const char *t){
for(int i=0,j=0;s[i]||t[j];i++,j++)if(s[i]!=t[j]) return 0;
return 1;
}
struct Player{
int dis,id;//max dis: 12
int t[38];
inline int getCard(){
if(scanf("%s",input)==EOF) return 0;
for(int i=1;i<=CARD_TYPE_NUM;i++)if(strMatch(input,cardName[i])){
t[i]++;printf("%c IN %s\n",playerName[id],cardName[i]);
break;
}
return 1;
}
inline void init(int _id){id=_id;dis=MAX_DIS;}
};
int f[38][5][5][14][14][2][2];
inline void trans(int *t,int i,int a,int b,int p,int q,int h,int dis){
if(!f[i][a][b][p][q][h][0]) return;
int *u=f[i][a][b][p][q][h],*_u;
for(int n,x=std::max(p-dis,-t[i+1]);x+q<=dis&&x+t[i+1]<=cardNum[i+1];x++){//x= num of cards that will be added
n=t[i+1]+x;
for(int _h=0;_h+h<2&&2*_h+a+b<=n;_h++)for(int o=0;2*_h+3*o+a+b<=n;o++){
if(x<0){
_u=f[i+1][b][n-2*_h-3*o-a-b][p-x][q][h|_h];
_u[1]=i+1;_u[0]=1;
}
else{
_u=f[i+1][b][n-2*_h-3*o-a-b][p][q+x][h|_h];
_u[1]=std::max(_u[1],u[1]);_u[0]=1;
}
}
}
}
inline int findCardDp(int *t,int &dis){
//f...[0]: can reach? f...[1]: the biggest card that be thrown
//f[i][a][b][p][q][h]: p= num of cards that be thrown q= num of cards that be added h= if thers is a head
//a= num of (i-1,i) b= num of (i)
std::memset(f,0,sizeof f);
f[0][0][0][0][0][0][0]=1;
for(int i=0;i<CARD_TYPE_NUM;i++)for(int p=0;p<=dis;p++)for(int q=0;q<=dis;q++)for(int h=0;h<2;h++){
if(isBeginCard[i+1]) trans(t,i,0,0,p,q,h,dis);
else for(int a=0;a<=4;a++)for(int b=0;a+b<=4;b++) trans(t,i,a,b,p,q,h,dis);
}
for(dis=0;dis<=MAX_DIS&&!f[CARD_TYPE_NUM][0][0][dis][dis][1][0];dis++);
if(dis>12) return 0;
return f[CARD_TYPE_NUM][0][0][dis][dis][1][1];
}
inline int findCard(Player &p,int &nextPlayer){
if(p.t[37]) return nextPlayer=nex[nex[p.id]],37;
else if(p.t[36]) return nex=_nex[order^=1],nextPlayer=nex[p.id],36;
else if(p.t[35]) return nextPlayer=p.id,35;
nextPlayer=nex[p.id];
return findCardDp(p.t,p.dis);
}
inline int ron(Player &p,int card){
if(p.dis!=1) return 0;
p.t[card]++;
int disBackup=p.dis;
findCardDp(p.t,p.dis);
if(!p.dis){
printf("%c RON\n",playerName[p.id]);
return 1;
}
p.t[card]--;p.dis=disBackup;
return 0;
}
inline int pon(Player &p,int card){
if(p.t[card]<2) return 0;
p.t[card]-=2;
int disBackup=p.dis;
findCardDp(p.t,p.dis);
if(p.dis>=disBackup){
p.dis=disBackup;p.t[card]+=2;
return 0;
}
printf("%c PONG %s %s %s\n",playerName[p.id],cardName[card],cardName[card],cardName[card]);
return 1;
}
inline int chi(Player &p,int a,int b,int c){
if(!p.t[a]||!p.t[b]) return 0;
p.t[a]--;p.t[b]--;
int disBackup=p.dis;
findCardDp(p.t,p.dis);
if(p.dis>=disBackup){
p.t[a]++;p.t[b]++;p.dis=disBackup;
return 0;
}
if(b>c) std::swap(b,c);
if(a>b) std::swap(a,b);
if(b>c) std::swap(b,c);
if(a>b) std::swap(a,b);
printf("%c CHOW %s %s %s\n",playerName[p.id],cardName[a],cardName[b],cardName[c]);
return 1;
}
inline int chi(Player &p,int card){
int a,b;
a=card+1;b=card+2;
if(!isBeginCard[a]&&!isBeginCard[b]&&chi(p,a,b,card)) return 1;
a=card-1;b=card+1;
if(!isBeginCard[card]&&!isBeginCard[b]&&chi(p,a,b,card)) return 1;
a=card-2;b=card-1;
if(!isBeginCard[card]&&!isBeginCard[b]&&chi(p,a,b,card)) return 1;
return 0;
}
Player p[4];
inline int ron(int gunner,int c){
for(int j=nex[gunner];j!=gunner;j=nex[j])
if(ron(p[j],c)) return 1;
return 0;
}
inline int pon(int &nextPlayer,int c){
for(int j=nextPlayer;nex[j]!=nextPlayer;j=nex[j])
if(pon(p[j],c)) return nextPlayer=j,1;
return 0;
}
inline int chi(int &nextPlayer,int c){
if(chi(p[nextPlayer],c)) return 1;
return 0;
}
int main(){
for(int i=0;i<PLAYER_NUM;i++) p[i].init(i);
for(int i=1;i<=13;i++)for(int j=0;j<PLAYER_NUM;j++) p[j].getCard();
int willGetCard=1;
for(int i=0,nextPlayer;;i=nextPlayer){
if(willGetCard&&!p[i].getCard()) break;
int c=findCard(p[i],nextPlayer);willGetCard=1;
if(!p[i].dis){
printf("%c SELFDRAWN\n",playerName[i]);
break;
}
printf("%c OUT %s",playerName[i],cardName[c]);
p[i].t[c]--;
if(c==37) printf(" %c\n",playerName[nex[i]]);
else puts("");
if(c>CARD_TYPE_NUM2) continue;
if(ron(i,c)) break;
if(pon(nextPlayer,c)||chi(nextPlayer,c)) willGetCard=0;
}
for(int i=0;i<PLAYER_NUM;i++)if(!p[i].dis) return printf("%c WIN\n",playerName[i]),0;
puts("DRAW");
return 0;
}