题目描述
对于一个字符串 S,我们定义 S 的分值 f(S) 为 S 中恰好出现一次的字符个数。例如 f(aba)=1,f(abc)=3,f(aba)=1,f(abc)=3,f(aaa)=0。
现在给定一个字符串S0⋯n−1(长度为 n,1≤n≤10^5,请你计算对于所有 S 的非空子串 Si⋯j(0≤i≤j<n),f(Si⋯j) 的和是多少。
输入描述
输入一行包含一个由小写字母组成的字符串 �S。
输出描述
输出一个整数表示答案。
输入输出样例
示例
输入
ababc
输出
21
运行限制
- 最大运行时间:1s
- 最大运行内存: 256M
思路一:最简单粗暴的想法:使用两个for循环进行暴力枚举。可以该时间复杂度为O(n^2),在数据量级为10^5时,“暴力”显得不可行。
思路二:利用每个字母的贡献值
根据题目一个字符串中一旦出现两个一样的字符时,其贡献值就变为0,即无效贡献字符串。
也就是说,一个字母的有效贡献字符串长度被离其最近的前后两个同字母限定。
以ababa为例子。
0 | 1 | 2 | 3 | 4 |
a | b | a | b | a |
对于(2,a)来说,它有限贡献字符串被(0,a)与(4,a)框定,也就是bab的含a子串。
而这些子串的个数公式为(2-0)*(4-2),抽象化后(i-pre[i])*(next[i]-i);含义就是以a中心将bab分为含中心的两个区域,(ba],[ab)。两个区域的个数之积即为所求。
位置编号 | 字母元素 | 字串列举 | 贡献值 | 公式计算 |
0 | a | a ab | 2 | (0-(-1))*(2-0) |
1 | b | b ab ba aba | 4 | (1-(-1))*(3-1) |
2 | a | a ba ab abc bab babc | 6 | (2-0)*(5-2) |
3 | b | b ab bc abc | 4 | (3-1)*(5-3) |
4 | c | c bc abc babc ababc | 5 | (4-(-1))*(5-4) |
合计 | 21 | 21 |
因此代码将分为以下几个部分:
1.每个编号字母的前后最近同字母位置的初始化。
2.标记每个字母的前后最近字母位置
3.开始计算累和。
#include<bits/stdc++.h>
using namespace std;
int forth[100003];
int latter[100003];
int main()
{
int sum=0,target;//target就是基准
char a[100003];
int mark[27];
int lmark[27];
gets(a);//输入那些字符串
int length=strlen(a);//计算长度
for(int i=0;i<26;i++){
mark[i]=-1;//初始化为-1
lmark[i]=length;//初始化为字符串长度
}
for(int i=0;i<length;i++){
target=a[i]-'a';//将字符转换为字符编号
forth[i]=mark[target];//该字母的前面(离该字母最近的)又出现了该字母的位置编号给予它
mark[target]=i;//更新字符的位置
}
for(int i=length-1;i>=0;i--){
target=a[i]-'a';//将字符转换为字符编号
latter[i]=lmark[target];//该字母的后面(离该字母最近的)又出现该字母的位置编号给予它
lmark[target]=i;//更新字符的位置
}
for(int i=0;i<length;i++){
sum=sum+((i-forth[i])*(latter[i]-i));
}
cout<<sum;
return 0;
}
该思路以及代码来自(1条消息) 子串分值(解决sum+=(i-pre[i])*(next[i]-i)的真谛与疑惑)_张立龙666的博客-CSDN博客
思路二将时间复杂度降到O(3*n)~O(4*n)也就是O(n)级别。但是空间存储稍微有一点大。于是我们来看看下面这个思路
思路三:dp+分治思想
以ababc而言,面对它我们可以认为是f(S(不含尾字母的子串))之和+f(S(含为字母字串))之和。
也就是从ababc(级别5)-->abab(级别4)-->aba(级别3)-->ab(级别2)-->a(级别1)。
我们可以列出以下表格
字符串 | 增加字符串 | 增加贡献值(d[i]) |
a | a | 1 |
ab | b,ab | 3 |
aba | a,ba,aba | 4 |
abab | b,ab,bab,abab | 4 |
ababc | c,bc,abc,babc,ababc | 9 |
合计 | 21 |
从上表我们可以发现:d[i] = d[i-1] + i - pre[i]*2 ;(i从1开始)
然而我们很自然的想到似乎还不对,例如下表列出ababb的情况;
字符串 | 增加字符串 | 增加贡献值 |
a | a | 1 |
ab | b,ab | 3 |
aba | a,ba,aba | 4 |
abab | b,ab,bab,abab | 4 |
ababb | b,bb,abb,babb,ababb | 3 |
合计 | 15 |
我们马上发现当前面的字符一旦出现多个同样的字母,那么字母带来的效果不再是单单的一个“-1”,还可能保持不变。
其实很容易理解,一个字符串里面有两个相同字母==>分值-1,而两个以上==>分值+0;
也就有下式:
d[i] = d[i-1] + i - pre[i] - ( pre[i] - pre[pre[i]] );
式子注解:(i - pre[i])这是真正+1的部分,碰见第一个相同字符前;
( pre[i] - pre[pre[i]] )这是-1的部分,在碰见前前一个相同字母前;
代码如下:(因为下标从0开始,所以上面描述的所有i均-1,也就是现在的范围是[-1,n-1])
#include<stdio.h>
#include<string.h>
int d=0;
int len=0;
char a[100001];
int atozlen[26][2];
unsigned int sum=0;
int main()
{
int i,x,y;
int eq1;
int eq2;
gets(a);
len=strlen(a);
for(x=0;x<26;x++)
for(y=0;y<2;y++)
atozlen[x][y]=-1;
for(i=0;i<len;i++)
{
eq1=atozlen[a[i]-'a'][0];
eq2=atozlen[a[i]-'a'][1];
if(atozlen[a[i]-'a'][0]==-1)
atozlen[a[i]-'a'][0]=i;
else
{
atozlen[a[i]-'a'][1]=atozlen[a[i]-'a'][0];
atozlen[a[i]-'a'][0]=i;
}
d+=i-eq1-(eq1-eq2);
sum=sum+d;
}
printf("%u",sum);
return 0;
}