zoukankan      html  css  js  c++  java
  • 偶串——状态记忆

    最初的思路:

      两个for循环遍历字符串T所有的子串,并一个接一个的对子串进行是否是偶串的判断。最坏时间复杂度为O(n^3) (即当子串就是T时)。

      显然这种思路不是最优的。

    第一步优化方案:

      对于字符串T=t1,t2,t3,...,tn。当其子串的开始位相同时,相邻子串之间的差异只是在增加了最后一个字符的改变。即

      SUB(i,j)=ti,ti+1,ti+2,...,tj.

      SUB(i,j+1)=ti,ti+1,ti+2,...,tj,tj+1.

      SUB(i,j)与SUB(i,j+1)的差异仅仅是SUB(i,j+1)比前者多了一个字符tj+1。

      所以我们可以用变量保持住SUB(i,j)中又哪些字符是奇数个,然后加入字符tj+1,直接判断SUB(i,j)是否是偶串。这样算法的时间复杂度降低至O(n^2)。

      代码如下:

     1 <?php
     2 function getInput(){
     3     $str = trim(fgets(STDIN));
     4     return $str;
     5 }
     6 
     7 function pushChar($status,$char){
     8     if(0 == count($status))
     9         $status[$char] = 1;
    10     else{
    11         if(array_key_exists($char, $status))
    12             unset($status[$char]);
    13         else{
    14             $status[$char] = 1;
    15         }
    16     }
    17     return $status;
    18 }
    19 
    20 function main(){
    21     $str = getInput();
    22     $str_len = strlen($str);
    23 
    24     $num = 0;
    25     for($i=0;$i<$str_len;$i++){
    26         $single[$i][0] = array();
    27         for($j=1;$j<=$str_len-$i;$j++){
    28             //j为长度
    29             $char = $str[$i+$j-1];
    30             $single[$i][$j] = pushChar($single[$i][$j-1],$char);
    31             if(0 == count($single[$i][$j])){
    32                 $num++;
    33             }
    34         }
    35     }
    36     echo $num."
    ";
    37 }
    38 
    39 $startTime = microtime(true);
    40 main();
    41 $endTime = microtime(true);
    42 $time = $endTime - $startTime;
    43 echo '占用最大内存:'.memory_get_peak_usage()."
    ";
    View Code

       由代码可知,本代码的空间复杂度为O(n^2).(即O(n*(n+1)/2) )。这在输入的字符串特别长时会报内存用尽的错误

    报此错误时,输入的字符串长度为10000.这个时候由于消耗的内存超出了php.ini中设置的memory_limit=128M的上限而报错。

    关于此错误的解释,可以参考文章PHP Fatal error——内存用尽

    因为我们只需要统计偶串的总数。因此在第二个for循环中并没有必要维护一个数组,只需要把上一个状态记录下来就可以了。

    进一步优化:

    我们在上一方案中,判断一个子串是否是偶串是通过数组实现的。当有新字符时,判断数组中是否存在key为新字符的元素,存在则unsset该元素。不存在,则设置该key,并设值为1.最后,通过count数组的元素个数,来判断子串时候为偶串。

    实际上,上述操作就是模拟了异或操作,我们可以直接使用移位操作、异或操作来实现对子串是否为偶串的判断。

    1 function pushChar($status,$char){
    2     $tmp = ord($char) - ord('a');
    3     $n = 1<<$tmp;
    4     $status ^= $n;
    5     return $status;
    6 }
    View Code

    这样可以进一步的缩短代码的运行时间。

    但是,在跑笔试用例时,仍然只有80%的通过率。异常情况出现在当输入的字符串长度为50000时,代码耗时10分钟才执行完毕。需要继续对代码优化。

    终极优化:

    下面是官方给出的终极版

     1 #include <iostream>
     2 #include <cstdio>
     3 #include <fstream>
     4 #include <algorithm>
     5 #include <cmath>
     6 #include <deque>
     7 #include <vector>
     8 #include <queue>
     9 #include <string>
    10 #include <cstring>
    11 #include <map>
    12 #include <stack>
    13 #include <set>
    14 
    15 #define maxn 100009
    16 using namespace std;
    17 char s[maxn];
    18 map<int,int>mp;
    19 int n;
    20 int main(){
    21     scanf("%s",s);
    22     n = strlen(s);
    23     mp[0] = 1;
    24     int cur = 0;
    25     long long ans = 0;
    26     for(int i = 0; i < n; i++){
    27         int x = s[i] - 'a';
    28         cur ^= (1 << x);
    29         ans += mp[cur];
    30         mp[cur]++;
    31     }
    32     cout << ans << endl;
    33     return 0;
    34 }
    View Code

     分析:

    类似于前缀和,如果我们从字符串的第一个字符S[0]开始逐个遍历,并且将读到的字符S[i]存储在一个‘011100...100’的26位二进制数cur[i]中(0表示该位对应的字符在子串中的个数为偶数,1表个数为基数),用来表示当读取到当前字符时,子串S[0:i]中各个字符的状态。那么最多有2^26个可能的状态。

    可能会存在cur[i] == cur[j]  (j>i>0) 的情况,这种情况就表明S[i+1:j]是偶串,

    因为 cur[i+1] = cur[i] ^ ( 1 << (S[i+1] - 'a') )

      cur[j] = cur[i] ^ (S[i+1:j]的cur值)

    只有当 S[i+1:j]的cur值等于0时,cur[j] 才等于 cur[i]。

    而当出现 cur[i] == cur[j] == cur[m] 时,就可以得到 S[i+1:j]、S[j+1:m]都是偶串  ==>进一步,可以知道S[i+1:m] 也是偶串。

    设cur[i] == cur[j] == cur[m] == k ,并且mp[k]表示cur值为k的字串的数目,在遍历到字符S[i],mp[k]=0。在遍历了S[i]之后,mp[k]++。

    则当cur值为k的子串数目一共有mp[k]个时,就表示在这些子串中最小偶串(指该偶串内部不再包含偶串)的数目为mp[k]-1,且这些最小偶串依次相邻。所以由这些最小偶串一共可以组成(mp[k]-1+1)(mp[k]-1)/2个不同的偶串。即(mp[k]*(mp[k]-1)/2个偶串。

    而前一次检测到cur值等于k时,其mp[k] = a;在下一次检查到cur值等于k时,mp[k] = a+1;

    这相邻两次所增加的偶串的个数就= (a+1)a/2 - a(a-1)/2 = a.

    因此,在遍历整个字符串时,只需要在每次访问一个新的字符S[i]时,保存S[0:i]子串的cur值,以及该cur值出现的次数,每访问一个新的字符,则将对应cur值已经出现的次数加至表示偶串总数的sum中(当cur值第一次出现时对应的次数为0),这样遍历完整个字符串后就可以得到字符串中包含的偶串的总数。

    特别的是,当访问字符串的字符时,出现cur等于0时,表示从起始到该字符为一个偶串,因此在遍历开始之前就需要给mp[0]赋初值为1,这样才能在第一次出现cur==0时,在偶串的总记录中加1。

    总结:

    在这种终极算法中,思想类似于前缀和int型变量保存子串[a-z]字符的个数的奇、偶情况,每增加一个字符就是在前一个cur的基础上进行异或运算,通过cur值相等来寻找最小偶串,并进一步根据相邻相等cur时,增加的偶串的数目等于该cur值已经出现的次数(推到见上面)的特点,迭代的计算总的偶串和。

    补充:这种算法的时间复杂度降低至了O(n), 空间复杂度最差为O(2^26) (即cur值最多可能有2^26种情况,需要2^26个mp变量来保存该cur值出现的次数,上面的代码中用int存储cur值,那么在最坏的情况下,会消耗 (2^26)*4byte 大约256M的内存空间)。

    php实现:

     1 <?php
     2 function getInput(){
     3     $str = trim(fgets(STDIN));
     4     return $str;
     5 }
     6 
     7 function main(){
     8     $str = getInput();
     9     $str_len = strlen($str);
    10 
    11     $num = 0;
    12     $cur = 0;
    13     $mp = array();
    14     $mp[0] = 1;
    15     for($i=0;$i<$str_len;$i++){
    16            $x = ord($str[$i]) - ord('a');
    17            $cur ^= (1<<$x);
    18            if(!isset($mp[$cur]))
    19                 $mp[$cur] = 0;
    20            $num += $mp[$cur];
    21            $mp[$cur]++;
    22         }
    23     echo $num;
    24 }
    25 main();
    View Code
  • 相关阅读:
    Django之路由系统(urls.py)
    Django之配置文件(settings.py)
    初始Django
    CSS基础
    HTML
    mysql基础知识拾遗
    python的进程
    python 队列
    python的线程
    2017年10月21日 CSS常用样式&鼠标样式 以及 jQuery鼠标事件& jQuery图片轮播& jQuery图片自动轮播代码
  • 原文地址:https://www.cnblogs.com/jade640/p/6624889.html
Copyright © 2011-2022 走看看