zoukankan      html  css  js  c++  java
  • 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)=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 }
  • 相关阅读:
    1.线程是什么?
    eclipse新手知识点1
    《图解Java多线程设计模式》UML用什么软件画?
    Microsoft SQL Server 2008完整+差异备份及还原实战
    xcopy
    导入EXCEL到数据库表中的步骤:
    js判断一个值是空的最快方法是不是if(!value){alert("这个变量的值是null");}
    Pytorch1.6.0 Nvidia Jetson TX2
    python所有的print就会自动打印到log.txt
    python import numpy right but import caffe wrong
  • 原文地址:https://www.cnblogs.com/zbh2047/p/9074750.html
Copyright © 2011-2022 走看看