【前缀和】子串分值和

目录

1、题目分析

2、代码展示

3、优化

3、继续优化

参考文献


题目描述

对于一个字符串 S,我们定义 S 的分值 f(S)为 S 中出现的不同的字符个数。例如 f(“aba”) = 2,f(“abc”) = 3, f(“aaa”) = 1,f(“aba”)=2,f(“abc”)=3,f(“aaa”)=1。

现在给定一个字符串 S [0...n − 1](长度为 n),请你计算对于所有 S 的非空子串 S [i...j](0 ≤ i ≤ j < n),f(S[i...j]) 的和是多少。

输入描述

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

其中,1≤ n ≤10^5

输出描述

输出一个整数表示答案。

输入输出样例

示例 1

输入

ababc

输出

28

运行限制

  • 最大运行时间:1s
  • 最大运行内存: 128M

题目来源:子串分值和


1、题目分析

  • 用dp [ i ] 表示以第 i 个数组元素结尾的非空子串内不同元素的和
  • 推理: 以题目给出的输入为例 ababc
    • dp[ 0 ] = 1
    • dp[ 1 ] = 2 + 1 = 3
    • dp[ 2 ] = 2 + 2 + 1 = 5
    • dp[ 3 ] = 2 + 2 + 2 + 1 = 7
    • dp[ 4 ] = 3 + 3 + 3 + 2 + 1 = 12
    • dp = 1 + 3 + 5 +7 + 12 = 28
  • 所以我们求出以每个位置为 结尾的非空子串的不同元素的和,然后把他们相加。
  • 怎么去求每个dp[ i ] 呢?
    • 首先以每个c[ i ] 为尾部,计算前面出现过的字符的种类和个数
    • 然后从前到后,没经过一个数组元素,便在记录数组里面减去它的类别的个数

2、代码展示

import java.util.Scanner;

public class _子串分值和 {

	//计算以s[predix]为尾部的所有非空子串的不同字符的个数
	//s是输入的字符序列,predix是尾部下标,length是s数组长度
	static void maxSubSum(char[] s,int predix,int length,int dp[]) {
		//存储出现的每个数组元素的个数
		int character[] = new int[27];
		for(int i=0;i<=predix;i++) {
			character[s[i]-'a'] ++;
		}
		
		//从第一个元素开始,计算以s[predix]为尾部的所有子串
		for(int i=0;i<=predix;i++) {
			int count=0;
			if(i>0) character[s[i-1]-'a']--;
			for(int j=0;j<26;j++) {
				//计算s[i]-s[predix]这个非空子串的不同字符的个数
				if(character[j]!=0)
					count++;
			}
			//把结果记录到dp数组
			dp[predix]+=count;
		}
		
		
		
	}

	public static void main(String[] args) {
		
		System.out.println("Please input data:");
		Scanner scanner = new Scanner(System.in);
		
		String str = scanner.next();
		int length = str.length();
		
		int NUM;
		NUM = length+5;
		int dp[] = new int[NUM];
		char s[] = str.toCharArray();
		
		int result = 0;
		for(int i=0;i<length;i++) {
			maxSubSum(s, i, length,dp);
			result+=dp[i];
		}
		
		System.out.println(result);
	}

}

但是这段代码只能通过50%,其它部分超时。所以需要优化。

3、优化

import java.util.Scanner;

public class _子串分值和 {

	//计算以s[predix]为尾部的所有非空子串的不同字符的个数
	//s是输入的字符序列,predix是尾部下标,length是s数组长度
	static void maxSubSum(char[] s,int predix,int length,int dp[]) {
		//存储出现的每个数组元素的个数
		int character[] = new int[27];
		int count=0;
		for(int i=0;i<=predix;i++) {
			character[s[i]-'a'] ++;
			if(character[s[i]-'a'] == 1)
				count++;
		}
		
		dp[predix] = count;
		//从第一个元素开始,计算以s[predix]为尾部的所有子串
		for(int i=1;i<=predix;i++) {
			character[s[i-1]-'a']--;
			if(character[s[i-1]-'a']==0)
				count--;
			//把结果记录到dp数组
			dp[predix]+=count;
		}
		
		
		
	}

	public static void main(String[] args) {
		
		System.out.println("Please input data:");
		Scanner scanner = new Scanner(System.in);
		
		String str = scanner.next();
		int length = str.length();
		
		int NUM;
		NUM = length+5;
		int dp[] = new int[NUM];
		char s[] = str.toCharArray();
		
		int result = 0;
		for(int i=0;i<length;i++) {
			maxSubSum(s, i, length,dp);
			result+=dp[i];
		}
		
		System.out.println(result);
	}

}

但是还是只能通过60%,剩余40%属于超时。

3、继续优化

  • dp[ i ] :表示以 c[ i ] 元素结尾的子串的非空不同字符的个数之和
  • dp[ i ] 和 dp[ i-1 ] 的比较
    • dp[ i ] 与 dp[ i-1 ] 相比,dp[ i ] 表示以 c[ i ] 元素结尾的子串的非空不同字符的个数之和,dp[ i-1 ] 表示以 c[ i-1 ] 元素结尾的子串的非空不同字符的个数之和。dp[ i ]多一个元素。多出来的这个元素,对于前面有c[ i ] 这个元素的子串而言没有什么作用,因为多个 c[ i ] 只会被计算1次。但是对于那些没有 c[ i ] 元素的子串而言,他们就少计算了一个字符。
    • 最后一个 c[ i ] 的位置 减去 倒数第二个 c[ i ] 的位置就是只有一个c[ i ] 且在末尾的最长子串。

 比如dp[ 3 ] = dp[ 2 ] + 3 -1

        对于下标从 0 到 0 的元素来说( 以它们为起点 ),以 下标 2 结尾和下标 3 结尾并没有什么不同,因为 c[ 3 ] = b,而 c[ 0 ] = b ,前面已经有了,不会参与不同符号的计数中去。 

        对于下标从 1 到 3 的元素来说( 以它们为起点 ),与dp[ 2 ]相比,少了一个符号c[ 3 ],而这个符号在1 - 3 之间是没有出现过的,所以多一个子串,就要 +1, 为什么只 +1 呢?这是因为除c[ 3 ]  以外的不同单词的个数在 dp[ 2 ] 中已经计算过了。

状态转移方程:dp[ n ] = dp [ n-1 ] + n - pos[ c[ i-1 ] ]

代码:

import java.util.Scanner;

public class _子串分值和2 {

	public static void main(String[] args) {
		System.out.println("Please input data:");
		Scanner inputScanner = new Scanner(System.in);
		String string = inputScanner.next();
		
		int dp[] = new int[string.length()+5];
		int pos[] = new int[200];
        //如果不是定义为long类型,那只能通过60%
		long ans=0L;
		
		char[] c = string.toCharArray();
		
		for(int i=1;i<=string.length();i++) {
			dp[i] = dp[i-1] + i - pos[c[i-1]];
			pos[c[i-1]]=i;
			ans+=dp[i];
		}
		
		System.out.println(ans);

	}
}

如果不是定义为long类型,那只能通过60%

4、方法二

        我们可以从当前这个字符在哪些子串中第一次出现(因为只有第一次出现才记录分数),子串的个数之和也就是这个位置的字符产生的贡献值。

那么dp[ i ] = (i - pos[ c[ i ] ])*(length - i)

public class _子串分值和3 {

	public static void main(String[] args) {
        //记录最后一次字符串出现的位置
		int pos[] = new int[27];
		
		Scanner inputScanner = new Scanner(System.in);
		char [] c = inputScanner.next().toCharArray();
		
		//这个地方如果不是long类型,只能通过80%
        //String类型是length()方法,char数组是length属性
		long length = c.length;

		//这个地方也要用long类型
		long  res = 0;
		for(int i=0;i<length;i++) {
			res += (long)((i-pos[c[i]-'a'])*(length-i));
			pos[c[i]-'a']=i;
		}
		System.out.println(res);

	}

}

注意:

  • length必须使用 long 类型,这是由数据集决定的。所以以后代码有少部分答案错误,代码没有明显的逻辑错误,那试试看是不是数据类型表示的范围不够。
  • res 同理,也必须使用 long 类型, int 类型不足以表示答案。

参考文献

[1] 子串分值和

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值