最初的思路:
两个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";
由代码可知,本代码的空间复杂度为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 }
这样可以进一步的缩短代码的运行时间。
但是,在跑笔试用例时,仍然只有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 }
分析:
类似于前缀和,如果我们从字符串的第一个字符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();