题目描述
对于一个字符串 S,我们定义 S 的分值 f(S) 为 S 中恰好出现一次的字符个数。例如 f(aba)=1,f(abc)=3,f(aaa)=0。
现在给定一个字符串 S0⋯n−1(长度为 n,1≤n≤105),请你计算对于所有 S 的非空子串 Si⋯j(0≤i≤j<n),f(Si⋯j) 的和是多少。
输入描述
输入一行包含一个由小写字母组成的字符串 S。
输出描述
输出一个整数表示答案。
输入输出样例
示例
输入
ababc
输出
21
运行限制
- 最大运行时间:1s
- 最大运行内存: 256M
解答
注意:下面说的子串规定,位置编号为i的元素只能在子串中出现一次,也就是元素本身。
sum+=(i-pre[i])*(next[i]-i)是解决此题的关键。
1、首先是计算每一个字母只出现过一次,能够存在于多少个子串。那么(i-pre[i])*(next[i]-i)就是计算字母编号为i的字母在只出现一次的情况下,能包含于多少个子串。
2、pre[i]记录了编号为i的字母,该字母的前面(离该字母最近的)又出现了该字母的位置编号,若前面没有出现过pre[i]初始值为-1(为什么初始值为-1可观察下面的表格第5列)。
3、next[i]记录了编号为i的字母,该字母的后面(离该字母最近的)又出现了该字母的位置编号,若后面没有出现过next[i]初始值为字符串的长度(为什么初始值为字符串长度可观察下面的表格第5列)。
下面我们以题目中的ababc为例,来一一列举每一个字母能够存在于多少个子串中
位置编号 | 字母元素 | 子串列举 | 子串数量(贡献度) | 用(i-pre[i])*(next[i]-i)得子串数量 |
---|---|---|---|---|
0 | a | a ab | 2 | (0-(-1))*(2-0) |
1 | b | b ab aba ba | 4 | (1-(-1))*(3-1) |
2 | a | a ba bab ab babc abc | 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 |
sum+=(i-pre[i])*(next[i]-i)的真谛
疑问
1、为什么贡献度加起来就是答案呢?为啥上表中不同的位置编号居然有相同的子串且都在各自的位置编号又算了一遍?
答、(1)计算每一个编号的元素子串数量实质上就是以该元素为准,其每一个子串贡献度只能为1,因为我只考虑该元素,不考虑其它元素,不考虑其它元素是因为其它的位置编号还要考虑,不然你就多算了。以ababc来说,我只考虑位置编号为0的元素a,那么就只算a的子串,只考虑a的话每个子串的分值就是1(现在不看b也,不看c,那是当位置编号元素为b或c的时候才计算)也就是a的贡献度为1,那么有几个子串贡献度就为几。
(2)不是又算了一遍,比如位置编号为0的a元素中有个子串是“ba”我只是单看字母元素a其分值度1,那为啥不是2呢?那是因为我算编号为1的子串“ba”的时候我也要单看字母元素b算其分值度为1,说白了就是它们的贡献度。这两个贡献度加起来就是真正的“ba”分值度为2嘛,如果题问ba的分值度是多少,那么答案就是2。那么我们时间复杂度为O(n)的算法就是将分值度剖开计算单个元素在其子串中的分值度(也就是贡献度)然后都加起来就是答案(子串数量=贡献度)。虽然每个编号可能会出现相同的子串(还是以子串ba为例),但是它们的基准不同,一个是以a为基准,一个是以b为基准,这样的话避免了重复.
2、为啥例如编号2中元素a的子串"babc"贡献度为1?为啥不是2?
答、不是说了嘛,你说的是它的分值是2,不是贡献度,为啥我们计算所有编号子串数量就是答案,就是因为每个子串贡献度都为1,那不正好是子串数量嘛。我上面说的意思是“babc”的分值为1,那是因为我只单看元素a且每个子串就出现一次a,那不就是一次嘛,怎么可能是2,你看看那个表里面哪个基准元素出现两次的?(子串规定就只出现一次)
3、(i-pre[i])*(next[i]-i) 怎么来的?
答、(i-pre[i])*(next[i]-i)实际上是根据计算每个元素的子串数量得出来的规律或实际总结出来的,看表就能看出规律,比如位置编号2,它的子串有6种为例,在串中“ababc”,以编号2元素a为基准包括a到左侧的b有2种可能(位置编号2减去与其最近相同元素编号),包括a到右侧的c有3种可能(总长度减去位置编号2)。左侧*右侧就是所有情况(与高中数学的概率知识类似),举一个例子,桌子上有一个一元硬币一个五角硬币考虑正反的情况组合(一元硬币正反共两种可能,五角正反硬币有两种,那么2*2,在桌子上出现的情况有4种组合的可能),综上所述用sum计算总和就是答案。
以下代码forth就是pre,latter就是next
#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;
}