1. 问题描述:
给你一个下标从 0 开始的二进制字符串 s ,它表示一条街沿途的建筑类型,其中:
s[i] = '0' 表示第 i 栋建筑是一栋办公楼,
s[i] = '1' 表示第 i 栋建筑是一间餐厅。
作为市政厅的官员,你需要随机选择 3 栋建筑。然而,为了确保多样性,选出来的 3 栋建筑相邻的两栋不能是同一类型。比方说,给你 s = "001101" ,我们不能选择第 1 ,3 和 5 栋建筑,因为得到的子序列是 "011" ,有相邻两栋建筑是同一类型,所以不合题意。请你返回可以选择 3 栋建筑的有效方案数 。
示例 1:
输入:s = "001101"
输出:6
解释:
以下下标集合是合法的:
- [0,2,4] ,从 "001101" 得到 "010"
- [0,3,4] ,从 "001101" 得到 "010"
- [1,2,4] ,从 "001101" 得到 "010"
- [1,3,4] ,从 "001101" 得到 "010"
- [2,4,5] ,从 "001101" 得到 "101"
- [3,4,5] ,从 "001101" 得到 "101"
没有别的合法选择,所以总共有 6 种方法。
示例 2:
输入:s = "11100"
输出:0
解释:没有任何符合题意的选择。
提示:
3 <= s.length <= 10 ^ 5
s[i] 要么是 '0' ,要么是 '1' 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/number-of-ways-to-select-buildings/
2. 思路分析:
① 分析题目可以知道本质上是求解s中子序列为"101"和"010"的数目,直接上感觉可以使用dp来解决,因为数据范围为10 ^ 5,所以需要将时间复杂度控制在O(n)或者是O(nlogn)以内,首先我们需要考虑先做出来然后再考虑优化,dp主要有两个关键点:状态表示和状态计算;对于子序列的题目我们一般考虑以当前位置i结尾的...,对于这道题目来说我们可以声明三维数组,其中f[i][j][k]表示以当前位置i结尾长度为j,并且当前位置i的字符是k,其中i=0~len(s),j = 0~3,k = 0~1;怎么样进行状态计算呢?对于当前位置i的字符如果为0那么尝试添加到0~i-1并且以1结尾的子序列的后面,如果为1那么尝试添加到0~i~1并且以0结尾的子序列的后面,更新当前对应长度为1,2,3对应的状态值即可,这种状态表示虽然比较好理解但是由于是两层循环所以时间复杂度为O(n ^ 2),所以肯定会超时,我们需要想更优的做法使得时间复杂度控制在O(n)左右才可以通过,对于s中的每一个字符我们看能否匹配当前t中的字符(t为子序列"010"和"101"),基于这个想法我们想到可以定义一个二维数组,其中f[i][j]表示以i结尾的字符串s与以j结尾的字符串t,相同子序列的方案数目,怎么样进行状态计算呢?一般是找最后一个不同点,当s[i] != t[j]的时候那么相同子序列的方案数目只能是f[i][j] = f[i - 1][j],当s[i] = t[j]时,f[i][j]由两部分组成,第一部分为f[i - 1][j - 1]表示前i - 1个字符匹配到状态j - 1,第二部分为前i - 1个字符已经匹配到状态j了,对于当前的字符s[i]或者t[j]可以选择匹配也可以选择不匹配所以是两部分结果相加,也即f[i][j] = f[i - 1][j - 1] + f[i - 1][j],因为涉及到减1操作所以下标从1开始递推比较方便,在初始化的时候由于s中的任何字符与空串t都是匹配的,所以将所有的f[i][0]设置为1,然后枚举所有的位置递推即可,最终f[len(s)[len(t)]]就是答案;与115题是一样的;
② 除了上面dp的思路之外,对于这道题目其实还有一个比较取巧的方法,由于计算的是"010"和"101"的数目,所以我们可以枚举每一个位置i,如果当前的字符为0那么计算左右两边1的数目,想乘就是当前的方案数目,如果为1那么计算左右两边0的数目,我们可以先计算出s中0的数目tot0,使用一个count0来维护枚举过程中左边字符0的数目,这样通过n,tot0,count0就可以计算出左右两边1的数目。
3. 代码如下:
python:
class Solution:
def get(self, s: str, t: str):
n = len(s)
f = [[0] * 5 for i in range(n + 10)]
for i in range(n): f[i][0] = 1
for i in range(1, n + 1):
for j in range(1, len(t) + 1):
if s[i - 1] == t[j - 1]:
f[i][j] = f[i - 1][j - 1] + f[i - 1][j]
else:
f[i][j] = f[i - 1][j]
return f[n][len(t)]
def numberOfWays(self, s: str) -> int:
return self.get(s, "010") + self.get(s, "101")
class Solution:
def numberOfWays(self, s: str) -> int:
tot0 = s.count("0")
count0 = res = 0
n = len(s)
for i in range(n):
if s[i] == "0":
res += (i - count0) * (n - tot0 - i + count0)
count0 += 1
else:
res += count0 * (tot0 - count0)
return res
go:
package main
import "fmt"
// s为原串, t为目标子序列
func get(s, t string) int64 {
// 数组长度只能够是常量
const N = 1e5 + 10
// f为一个二维数组
f := [N][5]int{}
len_s, len_t := len(s), len(t)
for i := 0; i < len_s; i++ {
f[i][0] = 1
}
for i := 1; i <= len_s; i++ {
for j := 1; j <= len_t; j++ {
if s[i-1] == t[j-1] {
f[i][j] = f[i-1][j-1] + f[i-1][j]
} else {
f[i][j] = f[i-1][j]
}
}
}
// 因为最终结果为int64类型, 所以需要使用int64函数将其转为int64的变量
return int64(f[len_s][len_t])
}
func numberOfWays(s string) int64 {
return get(s, "101") + get(s, "010")
}
O(n ^ 2):超时,一开始的时候想的是先要解出来再考虑如何优化
import "fmt"
// O(n ^ 2)
func numberOfWays(s string) int64 {
n := len(s)
const N = 1e5 + 10
f := [N][5][3]int64{}
if s[0] == '0' {
f[0][1][0] = 1
} else {
f[0][1][1] = 1
}
for i := 1; i < n; i++ {
if s[i] == '0' {
f[i][1][0] = 1
for j := 0; j < i; j++ {
f[i][2][0] += f[j][1][1]
f[i][3][0] += f[j][2][1]
}
} else {
f[i][1][1] = 1
for j := 0; j < i; j++ {
f[i][2][1] += f[j][1][0]
f[i][3][1] += f[j][2][0]
}
}
}
var res int64 = 0
// 计算以当前位置i结尾长度为3的方案数目
for i := 0; i < n; i++ {
res += f[i][3][0] + f[i][3][1]
}
return res
}
package main
import (
"fmt"
"strings"
)
// 对于每一个1计算左边0和右边0的数目, 对于每一个0计算左边1和右边0的数目
func numberOfWays(s string) int64 {
tot0 := strings.Count(s, "0")
count0, res := 0, 0
n := len(s)
for i := 0; i < n; i++ {
if s[i] == '1' {
res += count0 * (tot0 - count0)
} else {
res += (i - count0) * (n - i + count0 - tot0)
count0 += 1
}
}
return int64(res)
}