d(s)为字符串s的字符种类个数,求d(s) 的值,以及s的子串中,字符种类为1,2,...,d(s) 的分别有多少个。
CF上的标签是"dp" 和 “two pointers"。
第二次遇到的双指针的题,上一次遇到是CF的514D。这个的双指针比514D的要稍微麻烦一些,实际上是”三指针“。
双指针主要用于,需要遍历某个区间的所有子区间 的时候,直接枚举左右边界的话时间复杂度是O(n^2),用双指针的话,是O(n)。
对于这道题而言,对 k =1,2,..,d(s) 分别计算 t(k)。
首先枚举区间左端点L,R1为保证d ( [L,R] )=k 的最小R值,R2为保证d ( [L,R] ) > k 的最小R值,这样的话,以L为左端点的区间中,字符种数为k的就有R2-R1种。
双指针能简化运算就在于,L+1所对应的R1和R2 一定大于等于原先L对应的R1和R2,所以,计算新的R1和R2的时候,只需要从原来的R1,R2往后加就行了,
这样就变成了L,R1,R2这三个指针的单调递增,于是总时间复杂度也就变成O(n)的了。
有一个例外情况,当[L,R1]的字符种数小于k的时候,若R1< n 则R1++,所以,若出现 [L,R1]的字符种数小于k 这种情况时,一定是R1=n,R2=n+1;
这时,由于答案不再是R2-R1种,而是R2-R1-1种,所以其实表达式应该是 ANS+=R2-R1- (R1==n);
当然,(R1==n)等价于 ( [L,R1]的字符种数小于k) 在我的代码中即为(M1.m<k)
#include <iostream> #include <cstdio> #include <cmath> #include <cstring> #define maxn 300007 using namespace std; struct Mark{//存储状态的结构体 int cnt[26]; int m; Mark(){memset(cnt,0,sizeof(cnt));m=0;} }; string str; int n; long long d[27]; int getK(Mark m){ int ANS=0; for(int i=0;i<26;++i) ANS+=(m.cnt[i]>0); return ANS; } void T(int k){//求 T(k) int R1=1,R2=1;//[L,[R1..R2)]为以L开始的区间 Mark M1,M2;//区间[L,R1]和 [L,R2] 的状态 M1.cnt[str[0]-'a']++;M1.m=1;M2=M1; for(int L=1;L <=n;++L){//枚举左端点 //更新 M1,M2 使之对应区间 [L,R1]和[L,R2] (这里的L是新的L。而M1,M2对应的是旧L的区间) if(L>1){ M1.cnt[str[L-2]-'a']--;if(!M1.cnt[str[L-2]-'a']) M1.m--; M2.cnt[str[L-2]-'a']--;if(!M2.cnt[str[L-2]-'a']) M2.m--; } //计算新的R1,R2 while(M1.m < k&&R1 < n){if(!M1.cnt[str[R1]-'a']) M1.m++;M1.cnt[str[R1++]-'a']++;} while(M2.m <=k&&R2 <=n){if(!M2.cnt[str[R2]-'a']) M2.m++;M2.cnt[str[R2++]-'a']++;} //累加计算结果 d[k]+=R2-R1-(M1.m <k);//printf("d[%d]+=%d (L=%d R1=%d R2=%d)\n",k,R2-R1,L,R1,R2); } } int main(void) { while(cin>>str){ Mark M; n=str.length(); for(int i=0;i<n;++i) M.cnt[str[i]-'a']++; memset(d,0,sizeof(d)); int D=getK(M); for(int i=1;i<=D;++i){T(i);} printf("%d\n",D); for(int i=1;i<=D;++i){ cout<<d[i]<<endl; } } return 0; }