最初的思路:

  两个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."\n";
37 }
38 
39 $startTime = microtime(true);
40 main();
41 $endTime = microtime(true);
42 $time = $endTime - $startTime;
43 echo '占用最大内存:'.memory_get_peak_usage()."\n";
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

 

posted on 2017-03-27 00:27  jade640  阅读(549)  评论(0编辑  收藏  举报