每日一题:子串分值

来源:2020蓝桥杯省赛大学A、B组

P8715子串分值 - 洛谷

子串分值 - 蓝桥云课

题解

这道题要求计算字符串所有非空子串的“分值和”,其中分值定义为子串中恰好出现一次的字符个数。直接枚举所有子串(共约 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;
}