https://www.luogu.org/problemnew/show/P5279

以下为个人笔记,建议别看:

首先考虑如何判一个牌型是否含有胡的子集。先将牌型表示为一个数组num,其中num[i]表示牌i出现了几张。

先判七对子(略)。

然后做一个dp。(后面的算法不支持"在最后(i接近n时)进行特判的dp",如果"在开始(i为1,2,3时)进行特判"也可能难以实现,因此可能需要改进一下dp。)

ans[i][j][k][l]表示考虑前i种花色的牌,是否预留了对子(j为1有,j为0无),顺子(i-1,i,i+1)取k个,顺子(i,i+1,i+2)取l个,把剩余的第1~i种的牌都尽量组成刻子,最多能得到多少个面子(这些顺子自身的贡献要等取到最大的那个数时再算,可以避免一些边界处理,比如不会出现(1,0,-1),(n+1,n,n-1)之类的顺子)。由于3个相同的顺子等同于3个刻子,只需要考虑0<=k<=2,0<=l<=2即可。当且仅当ans[n][1][0..2][0..2]的最大值>=4时牌可以胡。(转移略)

可以得到这样一个暴力

 #include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define fi first
#define se second
#define pb push_back
typedef long long ll;
typedef unsigned long long ull; inline void setmax(int &a,int b)
{
if(a<b) a=b;
} int num[],n;
inline char judge()
{
int t1=,i,k,l,tt,ed;
for(i=;i<=n;++i)
if(num[i]>=)
++t1;
if(t1>=) return ;
static int ok[][][][];
memset(ok,,sizeof(ok));//(i-1,i,i+1)->k,(i,i+1,i+2)->l
ok[][][][]=;
for(i=;i<n;++i)
{
for(k=;k<=;++k)
{
for(l=;l<=;++l)
{
ed=min(,num[i+]-k-l);
for(tt=;tt<=ed;++tt)
{
setmax(ok[i+][][l][tt],ok[i][][k][l]+k+(num[i+]-k-l-tt)/);
setmax(ok[i+][][l][tt],ok[i][][k][l]+k+(num[i+]-k-l-tt)/);
}
ed=min(,num[i+]-k-l-);
for(tt=;tt<=ed;++tt)
{
setmax(ok[i+][][l][tt],ok[i][][k][l]+k+(num[i+]-k-l-tt-)/);
}
}
}
}
int ans=;
for(k=;k<=;++k)
for(l=;l<=;++l)
setmax(ans,ok[n][][k][l]);
return ans>=;
}
const int md=;
#define addto(a,b) ((a)+=(b),((a)>=md)&&((a)-=md))
int fac[],ifac[];
int ans;
/*
ull ttttt[555];
void out()
{
for(int i=1;i<=200;++i)
printf("%llu ",ttttt[i]);
puts("");
int t;
scanf("%d",&t);
}
*/
void dfs(int p,int ddd)
{
if(judge())
{
addto(ans,ull(p)*ddd%md*fac[*n--p]%md);
/*
ttttt[p]+=ddd;++ttttt[0];
if(ttttt[0]%10000==0)
{
out();
}
*/
//if(p<10)
//printf("%d\n",p);
return;
}
for(int i=;i<=n;++i)
if(num[i]<)
{
++num[i];
dfs(p+,ull(ddd)*(-num[i])%md);
--num[i];
}
}
int poww(int a,int b)
{
int ans=;
for(;b;b>>=,a=ull(a)*a%md)
if(b&)
ans=ull(ans)*a%md;
return ans;
}
int main()
{
/*
int i,w,t;
scanf("%d%d",&n,&t);
for(i=1;i<=t;++i)
{
scanf("%d",&w);
++num[w];
}
printf("%d\n",int(judge()));
*/
int i,w,t;
fac[]=;
for(i=;i<=;++i)
fac[i]=ull(fac[i-])*i%md;
ifac[]=poww(fac[],md-);
for(i=;i>=;--i)
ifac[i-]=ull(ifac[i])*i%md;
scanf("%d",&n);
for(i=;i<=;++i)
{
scanf("%d%d",&w,&t);
++num[w];
}
dfs(,);
printf("%llu\n",ull(ans)*ifac[*n-]%md);
//out();
return ;
}

可以根据这个dp建成一个类似自动机的东西。自动机上的状态(点)可以当做一个三维数组再加上一个数字,三维就是就是ans的后三维,数组的元素就是在那三维的条件下最多凑出的面子数,加上的数字是"有多少种数字的牌可以凑出对子"(为了把七对子的统计放进自动机)。转移边就是根据dp的转移来连。对于起始状态,显然额外数字为0,设数组为a,则数组中只有a[0][0][0]=0,其余全为-inf。可以用和开头一样的方法判断一个状态是否是胡牌状态。为了方便,可以把所有胡牌的状态合并成一个状态,它的所有转移边都指向自身。

爆搜一下,可以发现这个自动机的状态并不是很多(不知道为什么)。爆搜的方法就是搞一个bfs,队列中一开始只有初始状态,每次从队列首部取出一个状态,枚举下一个数牌数量是0/1/2/3/4进行转移,得到它的后继状态。如果后继状态胡了:直接向某一个钦点的结束状态连边连边。如果后继状态没有胡:如果没有遍历过后继状态就建立后继状态对应的点并连边,然后将后继状态加入队列,否则直接向后继状态连边。判断后继状态是否遍历过可以强行搞一个map之类的。为了复杂度对,需要让数组中各个值对4取min,额外数字对7取min(这一步的确是有必要的,因为可能a[0]里面有很大的数字,但是a[1]里面都很小,虽然有很大数字,仍然不能胡牌,导致有无限个状态)

如何根据这个自动机计算答案?(以下“能胡”指存在一个子集胡牌)

最终的答案=所有方案胡牌巡数的平均值=所有方案胡牌巡数总和/方案数

此处的一种方案:给剩余的未摸进来的牌每张一个(a,b)的编号,表示数字为a的第b张牌;对于这些(a,b)对的任意一个排列就是一种方案。

先算所有方案胡牌巡数总和。把每个方案拆开,拆成没胡牌前每一巡1的贡献,胡牌那一巡1的贡献,两者分开考虑。对于两个部分,都将所有方案一起考虑。对于第一部分,每一巡分开考虑,相当于每一巡的贡献是这一巡有多少方案不能胡。对于第二部分,由于所有牌摸进来必定能胡,贡献就是方案数。

胡牌巡数总和除以方案数,得到答案=$\frac{\sum_{i=1}^{4n-13}额外摸i张牌不能胡的方案数}{总方案数}+1$

怎么算这个东西?首先,总方案数等于$(4n-13)!$

dp一下,ans[i][j][k]表示考虑前i种牌,额外摸了j张,当前在自动机上状态是k的方案数(考虑最终答案,额外摸i张牌不能胡的方案,相当于先从所有(a,b)对中选出i个,让它们作为前i张摸上来的牌,如果它们不能胡,则产生贡献i!(4n-13-i)!;因此此处的一种方案定义为从前i种牌产生的所有(a,b)对中选出j个,最后统计答案时ans[n][j][k]只有当k!=T时才产生贡献,对答案的贡献要乘上j!(4n-13-j)!)(转移略)

 #include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<map>
using namespace std;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
typedef long long ll;
typedef unsigned long long ull;
const int md=;
#define addto(a,b) ((a)+=(b),((a)>=md)&&((a)-=md))
inline void setmax(int &a,int b)
{
if(a<b && b>=) a=b;
}
inline void setmin(int &a,int b)
{
if(a>b) a=b;
}
struct st
{
int a[][][],b;
};
inline bool operator<(const st &a,const st &b)
{
/*
int t=memcmp(a.a,b.a,sizeof(a.a));
if(!t) return a.b<b.b;
else return t<0;
*/
for(int i=;i<=;++i)
for(int j=;j<=;++j)
for(int k=;k<=;++k)
if(a.a[i][j][k]!=b.a[i][j][k])
return a.a[i][j][k]<b.a[i][j][k];
return a.b<b.b;
}
inline bool judge(const st &a)
{
if(a.b>=) return ;
int j,k;
for(j=;j<;++j)
for(k=;k<;++k)
if(a.a[][j][k]>=)
return ;
return ;
}
inline void nxt_state(const st &a,st &b,int x)
{
b.b=min(,a.b+(x>=));
memset(b.a,,sizeof(b.a));
int i,j,k;
for(i=;i<=;++i)
for(j=;j<=;++j)
{
for(k=;k<=min(,x-i-j);++k)
{
setmax(b.a[][j][k],a.a[][i][j]+i+(x-i-j-k)/);
setmax(b.a[][j][k],a.a[][i][j]+i+(x-i-j-k)/);
}
for(k=;k<=min(,x-i-j-);++k)
{
setmax(b.a[][j][k],a.a[][i][j]+i+(x-i-j-k-)/);
}
}
for(i=;i<=;++i)
for(j=;j<=;++j)
{
setmin(b.a[][j][k],);
setmin(b.a[][j][k],);
} }
map<st,int> ma;
int trans[][];
/*
struct E
{
int to,nxt;
}e[200011];
int f1[10011],ne;
inline void me(int x,int y)
{
e[++ne].to=y;e[ne].nxt=f1[x];f1[x]=ne;
}
*/
int mem,S,T;st ta[];
/*
void out()
{
printf("%d %d\n",S,T);
for(int i=1;i<=mem;++i)
{
printf("id%d\n",i);
for(int j=0;j<=1;++j)
{
for(int k=0;k<=2;++k)
{
for(int l=0;l<=2;++l)
{
printf("%d ",ta[i].a[j][k][l]);
}
puts("");
}
puts("/////////////////////");
}
for(int j=0;j<=4;++j)
printf("%d ",trans[i][j]);
puts("");
printf("%d\n---------------------\n",ta[i].b);
}
}
*/
void init()
{
st t1,t2;int t,i;
T=++mem;
S=++mem;
memset(t1.a,,sizeof(t1.a));
t1.b=;
t1.a[][][]=;
ma[t1]=S;ta[S]=t1;
for(t=S;t<=mem;++t)
{
t1=ta[t];
for(i=;i<=;++i)
{
nxt_state(t1,t2,i);
if(judge(t2))
trans[t][i]=T;
else if(!ma.count(t2))
{
ma[t2]=++mem;
ta[mem]=t2;
trans[t][i]=mem;
}
else
trans[t][i]=ma[t2];
}
}
for(i=;i<=;++i)
trans[T][i]=T;
} int n1[],n,ans;
int an1[][][];
int fac[],ifac[];
int C(int n,int m) {return ull(fac[n])*ifac[m]%md*ifac[n-m]%md;}
int CC[][];
int main()
{
int i,t1,t2,j,k,l;
fac[]=;
for(i=;i<=;++i)
fac[i]=ull(fac[i-])*i%md;
//printf("1t%d\n",fac[10000]);
ifac[]=;
for(i=;i>=;--i)
ifac[i-]=ull(ifac[i])*i%md;
//printf("2t%d\n",ifac[1]);
init();
for(i=;i<=;++i)
for(j=;j<=i;++j)
CC[i][j]=C(i,j);
/*
printf("1t%d\n",mem);
for(i=21;i<=25;++i)
{
for(int j=0;j<=4;++j)
printf("%d ",trans[i][j]);
puts("");
for(int j=0;j<=2;++j)
{
for(int k=0;k<=2;++k)
printf("%d ",ta[i].a[0][j][k]);
puts("");
}
puts("");
for(int j=0;j<=2;++j)
{
for(int k=0;k<=2;++k)
printf("%d ",ta[i].a[1][j][k]);
puts("");
}
puts("");
}
return 0;
*/
//printf("1t%d\n",mem);
scanf("%d",&n);
for(i=;i<=;++i)
{
scanf("%d%d",&t1,&t2);
++n1[t1];
}
an1[][][S]=;
for(i=;i<n;++i)
{
for(j=;j<=*n-;++j)
{
for(k=;k<=mem;++k)
{
for(l=;l<=-n1[i+];++l)
{
addto(an1[i+][j+l][trans[k][l+n1[i+]]],ull(an1[i][j][k])*CC[-n1[i+]][l]%md);
//预处理C(a,b)减小常数
}
}
}
}
for(j=;j<=*n-;++j)
{
for(k=;k<=mem;++k)
if(k!=T)
{
addto(ans,ull(an1[n][j][k])*fac[j]%md*fac[*n--j]%md);
}
}
printf("%llu\n",(ull(ans)*ifac[*n-]+)%md);
return ;
}

洛谷P5279 [ZJOI2019]麻将的更多相关文章

  1. 洛谷 P5279 - [ZJOI2019]麻将(dp 套 dp)

    洛谷题面传送门 一道 dp 套 dp 的 immortal tea 首先考虑如何判断一套牌是否已经胡牌了,考虑 \(dp\)​​​​​.我们考虑将所有牌按权值大小从大到小排成一列,那我们设 \(dp_ ...

  2. 洛谷P5279 [ZJOI2019]麻将(乱搞+概率期望)

    题面 传送门 题解 看着题解里一堆巨巨熟练地用着专业用语本萌新表示啥都看不懂啊--顺便\(orz\)余奶奶 我们先考虑给你一堆牌,如何判断能否胡牌 我们按花色大小排序,设\(dp_{0/1,i,j,k ...

  3. 题解 洛谷 P5279 【[ZJOI2019]麻将】

    这题非常的神啊...蒟蒻来写一篇题解. Solution 首先考虑如何判定一副牌是否是 "胡" 的. 不要想着统计个几个值 \(O(1)\) 算,可以考虑复杂度大一点的. 首先先把 ...

  4. Luogu P5279 [ZJOI2019]麻将

    ZJOI2019神题,间接送我退役的神题233 考场上由于T2写挂去写爆搜的时候已经没多少时间了,所以就写挂了233 这里不多废话直接开始讲正解吧,我们把算法分成两部分 1.建一个"胡牌自动 ...

  5. 【题解】Luogu P5279 [ZJOI2019]麻将

    原题传送门 希望这题不会让你对麻将的热爱消失殆尽 我们珂以统计每种牌出现的次数,不需要统计是第几张牌 判一副牌能不能和,类似这道题 对于这题: 设\(f[i][j][k][0/1]\)表示前\(i\) ...

  6. 洛谷P5280 [ZJOI2019]线段树 [线段树,DP]

    传送门 无限Orz \(\color{black}S\color{red}{ooke}\)-- 思路 显然我们不能按照题意来每次复制一遍,而多半是在一棵线段树上瞎搞. 然后我们可以从\(modify\ ...

  7. 洛谷P5280 [ZJOI2019]线段树(线段树)

    题面 传送门 题解 考场上就这么一道会做的其它连暴力都没打--活该爆炸-- 首先我们得看出问题的本质:有\(m\)个操作,总共\(2^m\)种情况分别对应每个操作是否执行,求这\(2^m\)棵线段树上 ...

  8. 洛谷P5280 [ZJOI2019]线段树

      https://www.luogu.org/problemnew/show/P5280 省选的时候后一半时间开这题,想了接近两个小时的各种假做法,之后想的做法已经接近正解了,但是有一些细节问题理不 ...

  9. 洛谷 P5280 - [ZJOI2019]线段树(线段树+dp,神仙题)

    题面传送门 神仙 ZJOI,不会做啊不会做/kk Sooke:"这八成是考场上最可做的题",由此可见 ZJOI 之毒瘤. 首先有一个非常显然的转化,就是题目中的"将线段树 ...

随机推荐

  1. 白话算法(6) 散列表(Hash Table) 从理论到实用(下)

    [澈丹,我想要个钻戒.][小北,等等吧,等我再修行两年,你把我烧了,舍利子比钻戒值钱.] ——自扯自蛋 无论开发一个程序还是谈一场恋爱,都差不多要经历这么4个阶段: 1)从零开始.没有束缚的轻松感.似 ...

  2. bzoj 1927 星际竞速 —— 最小费用最大流

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=1927 首先注意到这是个DAG: 考虑每个点从哪里来,可以是瞬移来的,也可以是从某个点走过来的 ...

  3. jenkins pipline 用法收集

    1.下载多个项目 node { stage('clone'){ dir('test1'){ checkout([$class: 'GitSCM', branches: [[name: '*/maste ...

  4. Poj 1401 Factorial(计算N!尾数0的个数——质因数分解)

    一.Description The most important part of a GSM network is so called Base Transceiver Station (BTS). ...

  5. 如何在Windows平台使用VS搭建C++/Lua的开发环境

    转自:http://ju.outofmemory.cn/entry/95358 本文主要介绍如何在Windows平台利用VS搭建C++/Lua开发环境.这里的“C++/Lua开发环境”主要指的是C++ ...

  6. CentOS安装配置radius服务器

    1.安装 Yum install -y freeradius freeradius-mysql freeradius-utils 2.配置 1)修改 clients.conf # vi /usr/lo ...

  7. python fabric的安装与使用

    背景:fabric主要执行远程的shell命令,包括文件的上传.下载,以及提示用户输入等辅助其它功能. 测试系统:ubuntu16 要求:python //系统有自带,ubuntu16 通常自带pyt ...

  8. 使用LookAndFeel为界面更换皮肤

    ----------------siwuxie095                             在 Windows 系统中,默认的 Java 运行环境(JRE)会为当前的窗体程序 指定一 ...

  9. Umbraco中根据ID获取IPublishedContent

    Umbraco中根据ID来获取IPublishedContent 在Umbraco网站上的 https://our.umbraco.com/documentation/Reference/Templa ...

  10. cookie 、Session 和自定义分页

    cookie cookie的由来 大家都知道Http协议是无状态的. 无状态的意思 是每次请求都是独立的,它的执行情况和结果与前面的请求和之后的请求都无直接关系, 他不会受前面的请求响应情况直接影响, ...