每日一题:子串分值
来源:2020蓝桥杯省赛大学A、B组
题解
这道题要求计算字符串所有非空子串的“分值和”,其中分值定义为子串中恰好出现一次的字符个数。直接枚举所有子串(共约 n2/2个)会超时,因为字符串长度最大可达 1e5。我们需要一种更高效的方法,通常是考虑每个字符对总答案的贡献。
核心思路
对于每个字符位置 k,计算有多少个子串包含该位置,并且在该子串中,字符 S[k]只出现一次。那么所有位置上的贡献之和就是答案。
如何计算单个位置的贡献
设字符串长度为 n,位置索引从 0开始。对于位置 k的字符 c:
- 找到左边第一个与 c相同字符的位置,记为 L(如果没有,则 L=−1)。
- 找到右边第一个与 c相同字符的位置,记为 R(如果没有,则 R=n)。
要使包含位置 k的子串中字符 c只出现一次,子串的左端点必须大于 L且不超过 k,右端点必须大于等于 k且小于 R。因此,左端点的选择有 (k−L)种,右端点的选择有 (R−k)种。于是,位置 k的贡献为:
\[(k-L) * (R-k)
\]
对所有位置求和即可。
计算左右边界
可以通过两次遍历得到每个位置的 L和 R:
- 从左到右遍历:用数组
last[26]记录每个字符最近一次出现的位置。对于位置 i,字符 c=S[i],left[i] = last[c](若未出现过则为 −1),然后更新last[c] = i。 - 从右到左遍历:类似地,用
next[26]记录每个字符最近一次出现的位置(从右往左看)。对于位置 i,right[i] = next[c](若未出现过则为 n),然后更新next[c] = i。
vector<int> left(s.size()), right(s.size());
vector<int> last(26, -1), next(26, s.size());
for (int i = 0; i < s.size(); ++i)
{
left[i] = last[s[i] - 'a'];
last[s[i] - 'a'] = i;
}
for (int i = s.size() - 1; i >= 0; --i)
{
right[i] = next[s[i] - 'a'];
next[s[i] - 'a'] = i;
}
样例解释
对于样例ababc,以中间的a为例
索引 01234
样例 ababc
---- //包含中间a且唯一包含的最大子串
-- //子串左边可选择的范围
--- //子串右边可选择的范围
//这个位置上a贡献度为2*3
索引 0 1 2 3 4
样例 a b a b c
left -1 -1 0 1 -1
right 2 3 5 5 5
完整代码
//代码来源byboyou,反对直接复制粘贴题解抄袭的行为
#include <bits/stdc++.h>
#define int long long
using namespace std;
string s;
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> s;
vector<int> left(s.size()), right(s.size());
vector<int> last(26, -1), next(26, s.size());
for (int i = 0; i < s.size(); ++i)
{
left[i] = last[s[i] - 'a'];
last[s[i] - 'a'] = i;
}
for (int i = s.size() - 1; i >= 0; --i)
{
right[i] = next[s[i] - 'a'];
next[s[i] - 'a'] = i;
}
int ans = 0;
for (int i = 0; i < s.size(); i++)
{
ans += (i - left[i]) * (right[i] - i);
}
cout << ans << '\n';
return 0;
}
浙公网安备 33010602011771号