首页 > 技术文章 > SDOI2017 BZOJ 4820 硬币游戏 解题报告

zbh2047 原文

写在前面

此题网上存在大量题解,但本蒟蒻太菜了,看了不下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)=sumlimits_{res(T) = i} {q(T)} ),且(displaystyle sumlimits_{i = 1}^n {P(i)} = 1)。

根据游戏规则可以得出。

引理2:对任意(i),以下式子成立:

[sumlimits_{res(T) = 0} {q(T{S_i})} = sumlimits_{j = 1}^n {P(j)sumlimits_{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 sumlimits_{res(T) = 0} {q(T{S_i})})的值均相同。

证明:对于每个(i),该值均为(displaystyle sumlimits_{res(T) = 0} {q(T) imes 2^{-m}}),故都相同。

根据引理2和引理3,我们便得到了(n)个关于(P(i))的方程与一个附加未知数(displaystyle sumlimits_{res(T) = 0} {q(T{S_i})})。再由引理1的第2式,便可列(n+1)个方程解(n+1)个未知数了。

(link)的计算

现在只剩一个问题,对任意(i,j)计算(displaystyle sumlimits_{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
", mt.a[i][n + 1]);
75 }

推荐阅读