写在前面
此题网上存在大量题解,但本蒟蒻太菜了,看了不下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 }