子串分值

81 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

题目描述

对于一个字符串 S,我们定义 S 的分值 f(S) 为 S 中恰好出现一次的字符个数。例如 f(aba) = 1,f(abc) = 3, f(aaa) = 0。

现在给定一个字符串 S0⋯n−1​(长度为 n1≤n≤10^5),请你计算对于所有 S 的非空子串Si⋯j​(0≤i≤j<n),f(Si⋯j)的和是多少。

输入描述

输入一行包含一个由小写字母组成的字符串 S。

输出描述

输出一个整数表示答案。

输入输出样例

示例

输入

ababc

输出

21

解题思路

我们先来看示例中的21是怎么算出来的。

以第一个字符开头的字串有a,ab,aba,abab,ababc加起来f(s)=1+2+1+0+1=5

以第二个字符开头的字串有b,ba,bab,babc,f(s)=1+2+1+2=6

以第三个字符开头的字串有a,ab,abc,f(s)=1+2+3=6

以第四个字符开头的字串有b,bc,f(s)=1+2=3

以第五个字符开头的字串有c,f(s)=1

总的=5+6+6+3+1=21

本题如果用这种模拟的方式去找字符串的子串再去求每个字串的f(s),当n很大的时候会执行非常长的时间。

我们换一种思路,去看每一个字符的贡献度

第一个字符a在哪些字串贡献了 a,ab, 2

第二个字符b, aba,ab,b,ba,4

第三个字符a, ba,bab,babc,a,ab,abc, 6

第四个字符b, ab,abc,b,bc,4

第五个字符c, ababc,babc,abc,bc,c 5

从中我们可以发现字符的贡献度在于其左右两边出现的位置,等于当前位置减去左边的位置*右边出现的位置-当前的位置。以示例中第三个字符a为例

image.png 找到贡献度的方法后,就能求出全部字符的贡献度

public class Main {
   public static void main(String[] args) {
      Scanner scan = new Scanner(System.in);
      String s = scan.nextLine();
      int n = s.length();
      // 找到每一个字符前面一个和后面一个相同字符的下标
      int[][] arr = new int[2][n];
      // 默认左边下标为-1,右边为n
      Arrays.fill(arr[0],-1);
      Arrays.fill(arr[1],n);
      // 记录26个字符最新的位置
      int[] location = new int[26];
      Arrays.fill(location,-1);
      // 找左右字符位置
      for (int i = 0; i < n; i++) {
         char c = s.charAt(i);
         if (location[c - 'a'] != -1) {
            arr[1][location[c - 'a']] = i; // 设置上一个数的右边值
         }
         arr[0][i] = location[c - 'a']; // 设置当前数的左边值
         location[c - 'a'] = i;
      }
      int ans = 0;
      // (当前位置 - 左边位置) * (右边位置 - 当前位置)
      for (int i = 0; i < n; i++) {
         ans += (i - arr[0][i]) * (arr[1][i] - i);
      }
      System.out.println(ans);
      scan.close();
   }
}

image.png