SDOI2017 BZOJ 4820 硬币游戏 解题报告
写在前面
此题网上存在大量题解,但本蒟蒻太菜了,看了不下10篇均未看懂,只好自己冷静分析了。本文将严格详细地论述算法(避免一切意会和玄学),因此可能会比其它题解更加理论化一些,希望能对像我一样看了其它题解还云里雾里的人有帮助。最后,为了追求极致,以下将字符串长度\(m\)加强到了10000(原题是300),并给出了一个时间复杂度到达极限的做法。
以下数学推导较多,如有错误之处欢迎批评指正!
题目描述
给定\(n\)个串,每个串仅包含字符T和F,长度均为\(m\)且互不相同。现在有一个字符串发生器,每次以等概率产生一个字符T或F,若某一时刻形成的字符串中出现了第\(i\)个串,则玩家\(i\)获胜。问每个玩家获胜的概率。
数据范围:\(n \le 300, m \le 10000\)
详细题解
一些定义
1、我们定义一个字符串\(T\)的\(q\)值为\(2^{-|T|}\),可以理解为每个字母以\(1/2\)的概率生成(但并不是严格意义的概率);
2、定义\(res(T)\)表示对于字符串\(T\),哪个玩家获胜(当\(T\)的末尾匹配\(S_i\),其它位置没有匹配任何给定串时,\(res(T)=i\);若整个串都没有匹配任何给定串,\(res(T)=0\);其他情况\(res(T)=-1\));
3、定义玩家\(i\)获胜的概率为\(P(i)\);
4、定义字符串\(T_1\)和\(T_2\)的\(link\)为所有长度\(l\)的集合,使得\(T_1\)长为\(l\)的后缀与\(T_2\)长为\(l\)的前缀相同。
方程的建立
我们的最终目标是求出\(P(i)\)。下面我们建立关于未知数\(P(i)\)的方程组,并用高斯消元求解。
引理1:\(\displaystyle P(i)=\sum\limits_{res(T) = i} {q(T)} \),且\(\displaystyle \sum\limits_{i = 1}^n {P(i)} = 1\)。
根据游戏规则可以得出。
引理2:对任意\(i\),以下式子成立:
\[\sum\limits_{res(T) = 0} {q(T{S_i})} = \sum\limits_{j = 1}^n {P(j)\sum\limits_{l \in link({S_j},{S_i})} {{2^{l - m}}} } \]
证明:任取字符串\(T_j\)满足\(res(T_j)=j\),以及\(l \in link({S_j},{S_i})\),必然对应唯一一种方式,使得将\(T_j\)结尾添加\(m-l\)个字符后,长为\(m\)的后缀为\(S_i\)。若将\(T_j\)结尾添加\(m-l\)个字符后的字符串记做\(TS_i\),则必有\(res(T) = 0\)。因此对等式右边每一个\(q(T_j)2^{l - m}\)必然对应且唯一对应等式左边一个\(q(TS_i)\)。另一方面,对于任意满足\(res(T) = 0\)的字符串\(TS_i\),取它的最短的一个\(res\)不为0的前缀(记作\(T_j,res(T_j)=j\)),那么唯一对应了等式右边的\((T_j,l)\)对;而所有长度大于\(T_j\)长度的前缀必然\(res\)值都是-1,而所有小于\(T_j\)长度的前缀res值都为0,这就意味着等式左边\(q(TS_i)\)必然对应且唯一对应右边一个\(q(T_j)2^{l - m}\)。从而对应关系是一一对应,等式左边等于右边。证毕。
引理3:对任意\(i\),\(\displaystyle \sum\limits_{res(T) = 0} {q(T{S_i})}\)的值均相同。
证明:对于每个\(i\),该值均为\(\displaystyle \sum\limits_{res(T) = 0} {q(T)\times 2^{-m}}\),故都相同。
根据引理2和引理3,我们便得到了\(n\)个关于\(P(i)\)的方程与一个附加未知数\(\displaystyle \sum\limits_{res(T) = 0} {q(T{S_i})}\)。再由引理1的第2式,便可列\(n+1\)个方程解\(n+1\)个未知数了。
\(link\)的计算
现在只剩一个问题,对任意\(i,j\)计算\(\displaystyle \sum\limits_{l \in link({S_j},{S_i})} {{2^{l - m}}}\)。
我们对所有给定的串\(S_i\)建立AC自动机。对于每个串\(S_i\)和\(S_j\),我们希望求出所有的\(link({S_i},{S_j})\)集合元素。这等价于\(S_i\)对应的AC自动机中的结束状态开始沿fail指针向上走所能到达的所有状态(这一集合设为\(F(S_i)\))中,有哪些状态在Trie树上是\(S_j\)对应结束状态的祖先。这样转化问题后,可以得到如下做法:
对于每个\(S_i\),先求出\(F(S_i)\);对于每个结点\(N \in F(S_i)\),将Trie树上\(N\)的子树中所有结点均加上一个值\(2^{l - m}\),最后对每个\(S_j\),查询Trie树对应结束状态的值即可。树上子树增加单点查询显然可以按树的dfs序用树状数组维护。
事实上,还可以进一步优化。注意到子树修改完后才询问,因此可以使用差分来修改区间,修改完后再前缀和回答询问。但是注意到Trie树的结点很多(可达\(nm\)量级),不能每次对所有结点前缀和。然而幸运的是,我们的查询仅限于Trie树结束状态的结点,因此只需要对这些结点按dfs序生成一个长度为\(n\)的区间,修改时差分,询问时前缀和即可。
时间复杂度分析
最后分析时间复杂度。首先建立AC自动机时间复杂度\(O(nm)\)。求出Trie树结束状态的结点dfs序,以及每个树上结点对应的dfs序区间时间复杂度也是\(O(nm)\)。对每个\(S_i\),求出\(F(S_i)\)然后差分修改的时间复杂度为\(O(m)\),因为\(F(S_i)\)集合大小一定不超过\(m\);最后前缀和并查询的时间复杂度为\(O(n)\)。全部求出后高斯消元的时间复杂度为\(O(n^3)\)。故总时间复杂度为\(O(n(m+n^2))\)。
总结
花了好几个小时才把理论推导理清楚,这题实在是太神了!同时对自己毒瘤的把数据范围改到\(m=10000\)表示成就感++(2333)!
AC代码
已略去高斯消元模板和AC自动机模板。
#define LETTER 2
inline int convert(char ch){ return ch == 'T' ? : ; }
int stEnd[], dfsEnd[], cnt2;
double a[], link[][];
int l[], r[];
char s[];
struct Trie{
int num, fail, match, depth;
int next[LETTER];
}pool[];
void insert(char *s, int id)
{
int cur = ;
for (int i = ; s[i]; i++){
int &pos = trie[cur].next[convert(s[i])];
if (!pos){
pos = ++cnt;
memset(&trie[cnt], , sizeof(Trie));
trie[cnt].depth = i + ;
}
cur = pos;
}
trie[cur].num = id;
}
void dfs(int i)
{
if (trie[i].num){
dfsEnd[trie[i].num] = ++cnt2;
l[i] = r[i] = cnt2;
}
else{ l[i] = << ; r[i] = ; }
for (int j = ; j<LETTER; j++){
int id = trie[i].next[j];
if (id){
dfs(id);
l[i] = min(l[i], l[id]);
r[i] = max(r[i], r[id]);
}
}
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
init();
for (int i = ; i <= n; i++){
scanf("%s", s);
insert(s, i);
stEnd[i] = cnt;
}
dfs();
makeFail();
for (int i = ; i <= n; i++){
memset(a, , sizeof(a));
for (int st = stEnd[i]; st; st = trie[st].fail){
double t = pow(, trie[st].depth - m);
a[l[st]] += t; a[r[st] + ] -= t;
}
for (int j = ; j <= n; j++)
a[j] += a[j - ];
for (int j = ; j <= n; j++)
link[i][j] = a[dfsEnd[j]];
}
Matrix mt(n + , n + );
for (int i = ; i <= n + ; i++)
mt.a[][i] = ;
for (int i = ; i <= n; i++){
mt.a[i][] = ;
for (int j = ; j <= n; j++)
mt.a[i][j] = link[j][i];
}
mt.gauss();
for (int i = ; i <= n; i++)
printf("%.10lf\n", mt.a[i][n + ]);
}
SDOI2017 BZOJ 4820 硬币游戏 解题报告的更多相关文章
- 「洛谷P1080」「NOIP2012提高组」国王游戏 解题报告
P1080 国王游戏 题目描述 恰逢 \(H\)国国庆,国王邀请\(n\)位大臣来玩一个有奖游戏.首先,他让每个大臣在左.右手上面分别写下一个整数,国王自己也在左.右手上各写一个整数.然后,让这 \( ...
- BZOJ 4619 Swap Space 解题报告
今天是因为David Lee正好讲这个题的类似题,我才做了一下. 本题是world final 2016的一道水…… 题目地址如下 http://www.lydsy.com/JudgeOnline/p ...
- NOIP2008 普及组T3 传球游戏 解题报告-S.B.S.
题目描述 上体育课的时候,小蛮的老师经常带着同学们一起做游戏.这次,老师带着同学们一起做传球游戏. 游戏规则是这样的:n个同学站成一个圆圈,其中的一个同学手里拿着一个球,当老师吹哨子时开始传球,每个同 ...
- 洛谷 P4705 玩游戏 解题报告
P4705 玩游戏 题意:给长为\(n\)的\(\{a_i\}\)和长为\(m\)的\(\{b_i\}\),设 \[ f(x)=\sum_{k\ge 0}\sum_{i=1}^n\sum_{j=1}^ ...
- [HEOI2016/TJOI2016]游戏 解题报告
[HEOI2016/TJOI2016]游戏 看起来就是个二分图匹配啊 最大化匹配是在最大化边数,那么一条边就代表选中一个坐标内的点 但是每一行不一定只会有一个匹配 于是把点拆开,按照'#'划分一下就好 ...
- BZOJ 2839: 集合计数 解题报告
BZOJ 2839: 集合计数 Description 一个有\(N\)个元素的集合有\(2^N\)个不同子集(包含空集),现在要在这\(2^N\)个集合中取出若干集合(至少一个),使得 它们的交集的 ...
- 洛谷 P1129 [ZJOI2007]矩阵游戏 解题报告
P1129 [ZJOI2007]矩阵游戏 题目描述 小\(Q\)是一个非常聪明的孩子,除了国际象棋,他还很喜欢玩一个电脑益智游戏――矩阵游戏.矩阵游戏在一个\(N*N\)黑白方阵进行(如同国际象棋一般 ...
- 洛谷 P2059 [JLOI2013]卡牌游戏 解题报告
P2059 [JLOI2013]卡牌游戏 题意 有\(n\)个人玩约瑟夫游戏,有\(m\)张卡,每张卡上有一个正整数,每次庄家有放回的抽一张卡,干掉从庄家起顺时针的第\(k\)个人(计算庄家),干掉的 ...
- 洛谷 P1450.硬币购物 解题报告
P1450.硬币购物 题目描述 硬币购物一共有\(4\)种硬币.面值分别为\(c1,c2,c3,c4\).某人去商店买东西,去了\(tot\)次.每次带\(d_i\)枚\(c_i\)硬币,买\(s_i ...
随机推荐
- DB2许可证文件
与 DB2® 数据库产品相关联的许可证文件有两种类型: 基本许可证密钥和 完整许可证密钥.这些许可证密钥以纯文本格式存储,通常称为 许可证文件或 许可证权利证书. "基本"许可证未 ...
- Oracle删除重复行
Oracle删除重复行 分类: ORACLE2010-12-12 17:10 423人阅读 评论(0) 收藏 举报 oracletabledeleteintegerinsert.net 查询及删除重复 ...
- linux下面调试C、C++
(1)写好makefile文件(支持debug) objects = Main.o Satellite.o TimeSystem.o SRPPara:$(objects) g++ -g -o SRP ...
- Java并发-容器
同步容器类:同步容器类包括Vector和Hashtable.这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法进行同步,使得每次只有一个线程可以访问容器的状态.JDK1.2之后,提供了 ...
- Maven的使用
在项目中,我们通常会为项目添加多种多样的依赖包(jar包),去网上下载,然后引入到项目中,很是麻烦. 但是用maven后,这一切都将变得简单起来.由于我的MyEclipse已经集成了maven插件,这 ...
- 基于Python的数据分析:数据库索引效率探究
索引在数据库中是一个很特殊的存在,它的目的就是为了提高数据查询得效率.同样,它也有弊端,更新一个带索引的表的时间比更新一个没有带索引的时间更长.有得有失.我希望做一些研究测试,搞清楚索引对于我们使用数 ...
- 记录一次坑爹的Python脚本抢购低价手机经历!
无意间浏览到魅族官网,说魅族3限量100台.30号中午12点抢购.正好我爪机目前处于报废状态,就来一试手气了.11点多种,习惯性的看了下网页脚本,发现了检测是否到抢购时间,并返回抢购消息的ajax.于 ...
- 【转】java中PriorityQueue优先级队列使用方法
优先级队列是不同于先进先出队列的另一种队列.每次从队列中取出的是具有最高优先权的元素. PriorityQueue是从JDK1.5开始提供的新的数据结构接口. 如果不提供Comparator的话,优先 ...
- Python进阶开发之网络编程,socket实现在线聊天机器人
系列文章 √第一章 元类编程,已完成 ; √第二章 网络编程,已完成 ; 本文目录 什么是socket?创建socket客户端创建socket服务端socket工作流程图解socket公共函数汇总实战 ...
- 洛谷 P1053 解题报告
P1053 篝火晚会 题目描述 佳佳刚进高中,在军训的时候,由于佳佳吃苦耐劳,很快得到了教官的赏识,成为了"小教官".在军训结束的那天晚上,佳佳被命令组织同学们进行篝火晚会.一共有 ...