仅含 1 的子串数
给你一个二进制字符串 s(仅由 ‘0’ 和 ‘1’ 组成的字符串)。
返回所有字符都为 1 的子字符串的数目。
由于答案可能很大,请你将它对 10^9 + 7 取模后返回。
提示:
- s[i] == ‘0’ 或 s[i] == ‘1’
- 1 <= s.length <= 10^5
题解分析
看到题目求子串,脑海里最先出来的是不是要用到KMP算法去求解。阅读题干后能够理解题目是想让我们去完成求所有的子串数量,这样的子串有一定要求:
- 首先子串只能包含“1”
- 子串是可以重复的
我们根据这两个条件进行深入分析,由于子串只能包含“1”,所以当子串中出现0时,我们就不再考虑这种情况。也就是说所有的子串一定是出现在某一段连续的“1”之中,基于这些连续的子串,我们再去求他们的子串。总结来说题目是想让我们求所有规定子串的子串数之和(我们将其称之为“孙串”),并且允许有重复。
编码思路
首先我们基于过程式的编程来思路来看待这道题,我们要完成解决问题的话需要进行三步操作。
- 先将字符串进行分割得到连续的“1”子串
- 对于每一个连续的子串再求其孙串的数量
- 将每一组子串下合法的孙串累加起来再求模就得到了正确结果
我们根据这三个步骤依次来设计对应的函数实现
1.先将字符串进行分割得到连续的“1”子串
这里我考虑到的是使用go语言中内置的字符串分割函数,以“0”为分割就能得到一段段合格的子串,但是实践发现以strs := strings.Split(str, "0")
分割后会产生空串,它不能很好的解决多个连续“0”出现的情况。所以使用如下分割函数进行实现:
func splitSrc(s string) []string {
str := strings.FieldsFunc(s, func(c rune) bool {
return c == '0'
})
return str
}
2.对于每一个连续的子串再求其孙串的数量
这里我先是对每一个长度的子串都进行了观察,子串的长度和孙串的数量之间有什么对应关系,我们可以容易的发现如下规律:
- “1” length:1 - > sum:“1x1”
- “11” length:2 - > sum:“2x1+1x1”
- “111” length:3 - > sum:“3x1+2x1+1x1”
- “1111” length:4 - > sum:“4x1+3x1+2x1+1x1”
- “11111” length:5 - > sum:“5x1+4x1+3x1+2x1+1x1”
基于上述观察容易写出函数:
func complexNum(num int) int {
sum := 0
for i := 1; i <= num; i++ {
sum += i
}
return sum
}
3.对于每一个连续的子串再求其孙串的数量
在这里我考虑到另一个问题:分割完成之后的子串可能存在长度相等的情况,因为孙串可以重复,那我们就不用再去执行一遍冗余的操作来求孙串的数量。所以对于新出现的子串长度我们求孙串数量,并将此结果存放至map的键值对当中。当出现重复的子串长度时我们直接去map中读取孙串的数量。将所有求完后的数累积便能得到结果。
const mod = 1000000007
func numSub(s string) int {
//添加计数器
sum := 0
//添加辅助map
tempMap := make(map[int]int)
//对字符串进行拆分
var str = splitSrc(s)
//遍历每一个连续含1的字符串
for _, i := range str {
length := len(i)
v, ok := tempMap[length]
if ok {
sum = (sum + v) % mod
} else {
var tempNum = complexNum(length)
tempMap[length] = tempNum
sum = (sum + tempNum) % mod
}
}
return sum
}
提交结果
官方优化
func numSub(s string) int {
cnt := 0
ans := 0
for _, d := range s {
if d == '0' {
ans += (1 + cnt) * cnt / 2
cnt = 0
} else {
cnt++
}
}
ans += (1 + cnt) * cnt / 2
return ans % (1e9+7)
}
在阅读了官方的代码实例后,发现自己找对了该题解的思路,但是代码的质量真的是很low很low。接下来我将分别讲讲从时间和空间上的优化。
- 时间上的优化
- 明明自己已经发现了不同长度字符串的子串数量规律,但我还是用了最笨的累积计算,这不就是高中便学过的等差数列求和,利用公式便可一步获得结果,经过改动尝试。时间消耗直接减少了4ms。
- 空间上的优化
可以看到官方代码的执行空间仅仅用到了3.7MB,而我的却达到了6.2MB。我认为是因为我的切片数组和map增大了代码的开销,将分割完后的子串存入了一个切片,当子串非常多时,这样的确会浪费一定的空间,官方示例中直接以O(n)的复杂度动态的切割完了字符串并动态的去求解孙串数量。此外由于等差数列的公式存在,知道子串长度求孙串数量相当便捷,时间复杂度仅为O(1),没有必要额外开辟空间记录。当s.length < 10^5 的情况下,开辟map最大可能会有10^5条记录,内存浪费肯定会巨大。
心得总结
虽然我的字符串切割算法在本体当中没有很好的时间复杂度和空间复杂度的展现,但在某些情况下还是有必要去使用的。
str := strings.FieldsFunc(s, func(c rune) bool {
return c == '0'
})
其次,在题目解决期间一定要考虑不必要的空间浪费问题。