输入格式:
输入在2行中先后给出字符串A和B。两字符串的长度都不超过10^4^,并且保证每个字符串都是由可见的ASCII码和空白字符组成,最后以换行符结束。
输出格式:
在一行中打印出A-B的结果字符串。
输入样例:
I love GPLT! It's a fun game!
aeiou
输出样例:
I lv GPLT! It's fn gm!
本题可以用三种方法解决,其中一种是暴力算法,还有一种是暴力算法的改进,最后一种是最简明的版本。
先来讲暴力算法:
暴力算法,思想简单直接,用两个for循环嵌套,外循环变量的上限是B串长,内循环变量的上限是A串长,如果A中字符等于B中字符,使用replace函数将其替代为空串,即完成一次删除,而此删除操作将产生O(N)的复杂度,总复杂度为O(N^3),实在无法忍受,提交后最后一个测试点会不过,因为超过100ms了。代码1如下:
#include<iostream> #include<string> using namespace std; int main() { string A,B; getline(cin,A); getline(cin,B); for(int i=0;i<B.length();i++) for(int j=0;j<A.length();j++) if(A[j]==B[i]) { A.replace(j,1,""); j--;//处理连续两个字符相同的情况,j会++所以需要--,从原来的位置出发 } cout<<A; return 0; }
暴力算法的改进:引入索引
为了处理连续两个字符相同的情况,引入索引变量来标记已经删除一次的位置,下一次位置再从此位置开始判定,当输入规模很大时,效率会很高,具体我们之后再讲。这里的寻找相同字符操作不是像暴力算法那样,而是使用了find函数,代码2如下:
#include<iostream> #include<string> using namespace std; int main() { string A,B; int t,index=0; getline(cin,A); getline(cin,B); for(int i=0;i<B.length();i++) { index=0; while((t=A.find(B[i],index))!=string::npos) { A.replace(t,1,""); index=t; } } cout<<A; return 0; }
标识思想的妙用,一维数组简明解决:
此算法是已知的解决此题的最简明的方法,连字符串函数都没用到,定义一个一维数组,数组下标就是字符的ASC码值,下标对应的元素只有0或1,0代表没有B的字符,1代表有B的字符,标识思想在这里发挥了极大的作用,使得算法复杂度为O(N),相对暴力算法,连降了两个数量级,代码3如下:
#include<iostream> #include<string> using namespace std; int main() { string A,B; int book[256]={0}; getline(cin,A); getline(cin,B); for(int i=0;i<B.length();i++) book[B[i]]=1; for(int i=0;i<A.length();i++) if(!book[A[i]]) cout<<A[i]; return 0; }
你可能会奇怪我为什么没有讲代码2的复杂度,当然不是因为容易看出,而是你看出了代码2的复杂度为O(N^3)也没有用。因为代码2能过,而且不是卡着耗时上限100ms过!最后一个测试点的耗时是:7ms!你可能觉得为什么同是O(N^3)复杂度,实际耗时差距怎么这么大。先不说这个,你可能还想知道代码3的复杂度,它的复杂度天生就是O(N),代码也非常简明,堪称完美算法,但是最后一个测试点的耗时也不过是5ms,在合理范围内,然而它和代码2只差2ms,明显是同一个数量级的。
为什么?效率会这么高?你肯定想到了,没错,这就是索引的威力!当输入规模相当大,也即字符串相当长时,如果待找字符只在末尾的一小段区间内,如果像代码1那样每次都从头找起,那么效率是非常低下的。而索引恰恰能避免这一情况,因为它就在原来被删除的位置上,如果待删除字符全部集中在字符串末尾的很小一段区间内,那么加上删除操作,时间复杂度将接近O(1)!可是即便如此,一开始的那次查找也会花费O(N)的时间,总体复杂度怎么会降至O(N)呢?
本人认为,应该是有两方面的因素。一方面是代码3的复杂度不是纯粹的O(N),而是O(2N),具体而言,如果B的长度为LB,A的长度为LA,则复杂度为O(LB+LA)。另一方面是如果输入规模大,根据实际测试,应该是A的长度作为较大的那个输入规模,也就是A的长度要远远大于B的长度,所以代码2的效率没有想象的那么低。如果你到现在还没明白的话,请看代码2外循环变量的上限是什么?LB!也就是说如果LA远大于LB,代码2的效率是绝不亚于代码3的。容易得出,代码2的复杂度实际将接近于O(LB*LA+LB*1*1),当LA足够大,LB足够接近1的时候,代码3将接近于O(LA+1)=O(LA),而代码2的复杂度将非常接近于O(1*LA+1*1*1)=O(LA)!这才是代码2高效的真正关键!而暴力算法呢?就算LA足够大,LB足够接近1,其复杂度也接近于O(1*LA*LA)=O(LA^2)!我们是无法忍受其低效的。
综上,即便是O(N^3)的复杂度,还是有可以大大提高效率的地方,归功于索引,用好索引,将给解题带来极大的帮助!