洛谷P5279 [ZJOI2019]麻将
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]麻将的更多相关文章
- 洛谷 P5279 - [ZJOI2019]麻将(dp 套 dp)
洛谷题面传送门 一道 dp 套 dp 的 immortal tea 首先考虑如何判断一套牌是否已经胡牌了,考虑 \(dp\).我们考虑将所有牌按权值大小从大到小排成一列,那我们设 \(dp_ ...
- 洛谷P5279 [ZJOI2019]麻将(乱搞+概率期望)
题面 传送门 题解 看着题解里一堆巨巨熟练地用着专业用语本萌新表示啥都看不懂啊--顺便\(orz\)余奶奶 我们先考虑给你一堆牌,如何判断能否胡牌 我们按花色大小排序,设\(dp_{0/1,i,j,k ...
- 题解 洛谷 P5279 【[ZJOI2019]麻将】
这题非常的神啊...蒟蒻来写一篇题解. Solution 首先考虑如何判定一副牌是否是 "胡" 的. 不要想着统计个几个值 \(O(1)\) 算,可以考虑复杂度大一点的. 首先先把 ...
- Luogu P5279 [ZJOI2019]麻将
ZJOI2019神题,间接送我退役的神题233 考场上由于T2写挂去写爆搜的时候已经没多少时间了,所以就写挂了233 这里不多废话直接开始讲正解吧,我们把算法分成两部分 1.建一个"胡牌自动 ...
- 【题解】Luogu P5279 [ZJOI2019]麻将
原题传送门 希望这题不会让你对麻将的热爱消失殆尽 我们珂以统计每种牌出现的次数,不需要统计是第几张牌 判一副牌能不能和,类似这道题 对于这题: 设\(f[i][j][k][0/1]\)表示前\(i\) ...
- 洛谷P5280 [ZJOI2019]线段树 [线段树,DP]
传送门 无限Orz \(\color{black}S\color{red}{ooke}\)-- 思路 显然我们不能按照题意来每次复制一遍,而多半是在一棵线段树上瞎搞. 然后我们可以从\(modify\ ...
- 洛谷P5280 [ZJOI2019]线段树(线段树)
题面 传送门 题解 考场上就这么一道会做的其它连暴力都没打--活该爆炸-- 首先我们得看出问题的本质:有\(m\)个操作,总共\(2^m\)种情况分别对应每个操作是否执行,求这\(2^m\)棵线段树上 ...
- 洛谷P5280 [ZJOI2019]线段树
https://www.luogu.org/problemnew/show/P5280 省选的时候后一半时间开这题,想了接近两个小时的各种假做法,之后想的做法已经接近正解了,但是有一些细节问题理不 ...
- 洛谷 P5280 - [ZJOI2019]线段树(线段树+dp,神仙题)
题面传送门 神仙 ZJOI,不会做啊不会做/kk Sooke:"这八成是考场上最可做的题",由此可见 ZJOI 之毒瘤. 首先有一个非常显然的转化,就是题目中的"将线段树 ...
随机推荐
- 洛谷P2895 [USACO08FEB]流星雨Meteor Shower
题目描述 Bessie hears that an extraordinary meteor shower is coming; reports say that these meteors will ...
- C/C++面试题总结(2)
C++部分: 1.static(静态)变量有什么作用? 2.virtual关键字用法 3.const有哪些作用 或<王道程序员求职宝典>P95 4.new/delete与malloc/fr ...
- oracle单实例12.2.0.1安装
说明:本文描述oracle linux 6.8 安装 oracle 12.2.0.1 0. 查看操作系统版本 [root@12c01 ~]# cat /etc/os-release NAME=&quo ...
- VS2008中宽字节和普通字节的使用
由于麻烦,所以并没有使用宽字节,留待以后.
- redis多机集群部署文档
redis多机集群部署文档(centos6.2) (要让集群正常工作至少需要3个主节点,在这里我们要创建6个redis节点,其中三个为主节点,三个为从节点,对应的redis节点的ip和端口对应关系如下 ...
- nginx中给目录增加密码保护实现程序
一款nginx中给目录增加密码保护实现程序,可以有效的保护一些目录不被访问,有需要的朋友可参考一下. 了防止一些可能出现存在漏洞的后台脚本暴露,使用验证的方式保护这些文件所在的目录 使用apache的 ...
- Linux编程里getopt_long_only函数用法详解
在程序中难免需要使用命令行选项,可以选择自己解析命令行选项,但是有现成的,何必再造轮子.下面介绍使用getopt_long_only和getopt_long(两者用法差不多)解析命令行选项. 程序中主 ...
- 给JZ2440开发板重新分区
转自:http://mp.weixin.qq.com/s?__biz=MzAxNTAyOTczMw==&mid=2649328035&idx=1&sn=7d3935cc05d3 ...
- VisualGDB系列5:使用VS来开发Linux程序
根据VisualGDB官网(https://visualgdb.com)的帮助文档大致翻译而成.主要是作为个人学习记录.有错误的地方,Robin欢迎大家指正. 本文演示如何使用VS来构建和调试Linu ...
- cisco 2901 配置拨号上网
1.输入en,然后输入密码确认后按conf t2.Router(config)# vpdn enable interface dialer 1 // 进入拨号器13.Router(c ...