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自动机模板。
1 #define LETTER 2 2 inline int convert(char ch){ return ch == 'T' ? 0 : 1; } 3 int stEnd[305], dfsEnd[305], cnt2; 4 double a[305], link[305][305]; 5 int l[3000005], r[3000005]; 6 char s[10005]; 7 struct Trie{ 8 int num, fail, match, depth; 9 int next[LETTER]; 10 }pool[3000001]; 11 void insert(char *s, int id) 12 { 13 int cur = 0; 14 for (int i = 0; s[i]; i++){ 15 int &pos = trie[cur].next[convert(s[i])]; 16 if (!pos){ 17 pos = ++cnt; 18 memset(&trie[cnt], 0, sizeof(Trie)); 19 trie[cnt].depth = i + 1; 20 } 21 cur = pos; 22 } 23 trie[cur].num = id; 24 } 25 void dfs(int i) 26 { 27 if (trie[i].num){ 28 dfsEnd[trie[i].num] = ++cnt2; 29 l[i] = r[i] = cnt2; 30 } 31 else{ l[i] = 1 << 30; r[i] = 0; } 32 for (int j = 0; j<LETTER; j++){ 33 int id = trie[i].next[j]; 34 if (id){ 35 dfs(id); 36 l[i] = min(l[i], l[id]); 37 r[i] = max(r[i], r[id]); 38 } 39 } 40 } 41 int main() 42 { 43 int n, m; 44 scanf("%d%d", &n, &m); 45 init(); 46 for (int i = 1; i <= n; i++){ 47 scanf("%s", s); 48 insert(s, i); 49 stEnd[i] = cnt; 50 } 51 dfs(0); 52 makeFail(); 53 for (int i = 1; i <= n; i++){ 54 memset(a, 0, sizeof(a)); 55 for (int st = stEnd[i]; st; st = trie[st].fail){ 56 double t = pow(2, trie[st].depth - m); 57 a[l[st]] += t; a[r[st] + 1] -= t; 58 } 59 for (int j = 2; j <= n; j++) 60 a[j] += a[j - 1]; 61 for (int j = 1; j <= n; j++) 62 link[i][j] = a[dfsEnd[j]]; 63 } 64 Matrix mt(n + 1, n + 1); 65 for (int i = 1; i <= n + 1; i++) 66 mt.a[0][i] = 1; 67 for (int i = 1; i <= n; i++){ 68 mt.a[i][0] = 1; 69 for (int j = 1; j <= n; j++) 70 mt.a[i][j] = link[j][i]; 71 } 72 mt.gauss(); 73 for (int i = 1; i <= n; i++) 74 printf("%.10lf\n", mt.a[i][n + 1]); 75 }