6036. 构造字符串的总得分和
题目来源于leetcode,解法和思路仅代表个人观点。传送门。
难度: 困难(T4)
时间:-
字符串哈希没用过,第一次见。
题目
你需要从空字符串开始 构造 一个长度为n
的字符串s
,构造的过程为每次给当前字符串 前面 添加 一个 字符。构造过程中得到的所有字符串编号为 1
到 n
,其中长度为 i 的字符串编号为 si
。
比方说,s = "abaca"
,s1 == "a"
,s2 == "ca"
,s3 == "aca"
依次类推。
si
的 得分 为 si
和sn
的 最长公共前缀 的长度(注意 s == sn
)。
给你最终的字符串 s
,请你返回每一个 si
的 得分之和 。
示例 1:
输入:s = "babab"
输出:9
解释:
s1 == "b" ,最长公共前缀是 "b" ,得分为 1 。
s2 == "ab" ,没有公共前缀,得分为 0 。
s3 == "bab" ,最长公共前缀为 "bab" ,得分为 3 。
s4 == "abab" ,没有公共前缀,得分为 0 。
s5 == "babab" ,最长公共前缀为 "babab" ,得分为 5 。
得分和为 1 + 0 + 3 + 0 + 5 = 9 ,所以我们返回 9 。
示例 2 :
输入:s = "azbazbzaz"
输出:14
解释:
s2 == "az" ,最长公共前缀为 "az" ,得分为 2 。
s6 == "azbzaz" ,最长公共前缀为 "azb" ,得分为 3 。
s9 == "azbazbzaz" ,最长公共前缀为 "azbazbzaz" ,得分为 9 。
其他 si 得分均为 0 。
得分和为 2 + 3 + 9 = 14 ,所以我们返回 14 。
提示:
1 <= s.length <= 105
s
只包含小写英文字母。
思路
二分查找
枚举每个后缀,在原串中使用 二分查找 最长的公共前缀长度。
为什么可以二分?
当两个字符串的长度为k的前缀相等时,长度为k-1的前缀也相等。
在二分的过程中,当后缀长度为 k k k时,即判断 s [ m − k : m i d ] s[m-k:mid] s[m−k:mid]与 s [ : m i d ] s[:mid] s[:mid]的是否相同。
其中, m m m为字符串的长度, m i d mid mid为二分的mid指针。
如何快速判断字符串p
,是否等于字符串s
中
[
l
,
r
]
[l,r]
[l,r]区间的子串?
- 如果,逐一比较,那么时间复杂度为 O ( r − l ) O(r-l) O(r−l)。如果需要判断 n n n次,那么时间复杂度为 O ( n ⋅ ( r − l ) ) O(n \cdot (r-l)) O(n⋅(r−l))。
- 如果,使用字符串哈希,那么构建时,复杂度为 O ( m ) O(m) O(m),其中 m m m为字符串长度。每次判断两个字符串是否相等 仅需 O ( 1 ) O(1) O(1)。如果需要判断 n n n次,那么时间复杂度为 O ( n + m ) O(n + m) O(n+m)。
字符串哈希(Rabin-Karp字符串编码)
我们选一个大于 字符种数 的整数 b a s e base base,就可以将字符串看成 b a s e base base 进制的整数。
假设有字符集
a
,
b
,
c
,
.
.
.
,
i
a,b,c,...,i
a,b,c,...,i共9
个字符。我们用其构建长度为
k
k
k的字符串。
每个字符可以重复取。
我们需要把字符串映射成数字(哈希值)。
- 将每个字符编码成10进制数。设 b a s e = 10 base = 10 base=10。
- 设 a = 1 , b = 2 , c = 3 , . . . , i = 9 a=1, b=2,c=3,...,i=9 a=1,b=2,c=3,...,i=9。
字符串编码方式为:
e
n
c
r
y
p
t
s
=
∑
i
=
0
∣
s
∣
−
1
(
s
[
i
]
)
∗
b
a
s
e
∣
s
∣
−
1
−
i
encrypt_s = \sum_{i=0}^{|s|-1}(s[i]) * base^{|s|-1-i}
encrypts=i=0∑∣s∣−1(s[i])∗base∣s∣−1−i
其中,
∣
s
∣
|s|
∣s∣为字符串
s
s
s的长度,
s
[
i
]
s[i]
s[i]为单个字符的编码值,如
a
=
1
,
b
=
2
a=1,b=2
a=1,b=2。
例子,
当 s = a b c s=abc s=abc。 s s s的哈希值 e n c r y p t s = 1 ∗ 1 0 2 + 2 ∗ 10 + 3 = 123 encrypt_s = 1*10^2 + 2*10 + 3 = 123 encrypts=1∗102+2∗10+3=123。
这样,我们可以发现 两个字符串 s s s 和 t t t 相等,当且仅当它们的长度相等且编码值相等。
问题:
当字符串很长时,对应的编码值可能会很大。当
b
a
s
e
=
10
base=10
base=10时,
∣
s
∣
>
9
|s|>9
∣s∣>9就会产生溢出的问题(C++、Java中Int类型)。
解决办法:
对一个数进行
m
o
d
mod
mod运算,使得 哈希值
H
H
H 保持在
[
0
,
m
o
d
]
[0,mod]
[0,mod]范围内。
随之带来另一个问题,哈希碰撞。
解决办法:
- 多次哈希。 即,取不同 K K K个的 b a s e i base_i basei和 m o d i mod_i modi,对字符串进行多次哈希操作。当且仅当,两个字符串这 K K K次的哈希值都相同,则两个字符串相同。
- 再哈希。 即,第一次哈希 得到 哈希值 H H H,再选择哈希函数 F F F再次进行哈希得到 F ( H ) F(H) F(H)。当且仅当,两个字符串的 F ( H ) F(H) F(H)相同,则两个字符串相同。
注意:
当对单个字符进行编码时,最好从
1
1
1开始编码。如果从
0
0
0开始编码,将会出现前导0问题。
例子,
如当 a = 0 , b = 1 a=0,b=1 a=0,b=1时, H ( ′ a b ′ ) = 0 ∗ 10 + 1 = 1 = H ( ′ b ′ ) H('ab') =0*10+1=1= H('b') H(′ab′)=0∗10+1=1=H(′b′)。
获得字符串[l,r]区间的哈希编码
p
r
e
f
i
x
[
i
]
prefix[i]
prefix[i]表示 字符串前缀
s
[
0
,
.
.
.
,
i
]
s[0,...,i]
s[0,...,i]的编码值。
那么有,
p
r
e
f
i
x
[
i
]
=
p
r
e
f
i
x
[
i
−
1
]
∗
b
a
s
e
+
s
[
i
]
prefix[i] = prefix[i-1]*base + s[i]
prefix[i]=prefix[i−1]∗base+s[i]
其中,
p
r
e
f
i
x
[
0
]
=
s
[
0
]
prefix[0]= s[0]
prefix[0]=s[0]。
类似前缀和的思想,
区间
[
l
,
r
]
[l,r]
[l,r]的编码就等于
e
n
c
o
d
e
(
l
,
r
)
=
p
r
e
f
i
x
[
r
]
−
p
r
e
f
i
x
[
l
−
1
]
∗
b
a
s
e
r
−
l
+
1
encode(l,r) = prefix[r] - prefix[l-1]*base^{r-l+1}
encode(l,r)=prefix[r]−prefix[l−1]∗baser−l+1
当
l
=
0
l=0
l=0时,
e
n
c
o
d
e
(
l
,
r
)
=
p
r
e
f
i
x
[
r
]
encode(l,r) = prefix[r]
encode(l,r)=prefix[r]。
例子:
e n c o d e ( ′ a b c d e ′ ) = 12345 encode('abcde') = 12345 encode(′abcde′)=12345
那么,
e n c o d e ( ′ c d ′ ) = e n c o d e ( ′ a b c d ′ ) − e n c o d e ( ′ a b ′ ) ∗ 1 0 2 = 1234 − 12 ∗ 100 = 34 \begin{aligned} encode('cd') &= encode('abcd') - encode('ab')*10^2\\ &=1234 - 12*100 \\ &=34 \end{aligned} encode(′cd′)=encode(′abcd′)−encode(′ab′)∗102=1234−12∗100=34
代码
class Solution {
public:
using ll = long long;
int MOD = 1e9 + 7;
int BASE = 131;
long long sumScores(string s) {
int n = s.length();
vector<ll> prefix(n),pw(n);
prefix[0] = s[0];
pw[0] = 1;
// prefix[i]表示s[0...i]的编码值
// pw[i]表示长度为i的位权重
for(int i=1;i<n;i++){
prefix[i] = (prefix[i-1] * BASE + s[i]) % MOD;
pw[i] = (pw[i-1]*BASE) % MOD;
}
ll ans = 0;
for(int k=1;k<=n;k++){
int left = 0;
int right = k-1;
while(left < right){
int mid = (left + right + 1) >> 1;
// prefix = [0...mid]
// suffix = [n-k ... n-k+mid]
ll prefix_hash = prefix[mid];
ll suffix_hash = 0;
if(k<n) suffix_hash = (prefix[n-k+mid] - prefix[n-k-1]*pw[mid+1] % MOD + MOD)%MOD;
else suffix_hash = prefix[n-k+mid];
if(prefix_hash == suffix_hash){
left = mid;
}else{
right = mid-1;
}
}
// left == right
if(s[left] == s[n-k+left]){
ans += left+1;
}else{
ans += left;
}
}
return ans;
}
};
简化版:
- 使用
unsigned
,自然溢出,就不用MOD
了。 - 使用
n+1
长度的数组,少一个越界的判断。 - 使用二分查找-模板二。少一个判断条件。
class Solution {
public:
using ull = unsigned long long;
using ll = long long;
int BASE = 131;
long long sumScores(string s) {
int n = s.length();
vector<ull> prefix(n+1),pw(n+1);
// prefix[0] = 0
pw[0] = 1;
// prefix[i+1]表示s[0...i]的编码值
// pw[i]表示长度为i的位权重
for(int i=0;i<n;i++){
prefix[i+1] = prefix[i] * BASE + s[i];
pw[i+1] = pw[i]*BASE;
}
ll ans = 0;
for(int k=1;k<=n;k++){
// 二分查找区间为[left, right-1]
int left = 0;
int right = k;
while(left < right){
int mid = (left + right + 1) >> 1;
// 前缀prefix == [0...mid-1]
// 后缀suffix == [n-k ... n-k + mid-1]
ll prefix_hash = prefix[mid];
ll suffix_hash = prefix[n-k+mid] - prefix[n-k]*pw[mid];
if(prefix_hash == suffix_hash){
left = mid;
}else{
right = mid-1;
}
}
// 公共前缀为 [0...left-1] , [n-k ... n-k + left-1]
ans += left;
}
return ans;
}
};
二分查找 模板
模板一:
int left = 0;
int right = n-1;
# 二分查询 区间为[left,right] (包含右端点)此时,right >= left。
while(left < right){
int mid = (left + right + 1) >> 1;
# 当使用mid时,使用的是mid。
...
if(f(mid) == g(target)){
left = mid;
}else{
right = mid-1;
}
}
# mid = (left == right) 的某个情况,未进入while循环。
# 当left == right时,退出。目标(答案)为left。
注:
left不动时,即left=mid。mid需要取上界,即mid = (left + right + 1) >> 1;
right不动时,即right=mid。mid需要取下届,即mid = (left + right) >> 1;
left和right都动时,即left=mid+1,right=mid-1。mid都取上界、下界都可以。
模板二:
int left = 0;
int right = n;
# 二分查询 区间为[left,right)(不包含右端点)此时,right > left。
while(left < right){
int mid = (left + right) >> 1;
# 当使用mid时,使用的是mid-1。mid看作 右端点 不可访问。
...
}
# mid-1 = [left,right)所有情况,都进入了while循环。
# 当left == right时,退出。目标(答案)为left-1。
当mid取下界时,即mid=(left + right) >> 1,mid使用的是mid。目标(答案)为left+1。
当mid取上界时,即mid = (left + right + 1) >> 1,mid使用的是mid-1。目标(答案)为left-1。
当left和right都动时,即left=mid+1,right=mid-1。添加if(f(mid) == g(target))判断即可。
算法复杂度
时间复杂度:
O
(
n
l
o
g
n
)
=
O
(
n
+
n
∑
k
=
1
n
l
o
g
k
)
O(nlogn) = O(n+n \sum_{k=1}^n logk)
O(nlogn)=O(n+n∑k=1nlogk)。其中,
n
n
n为字符串
s
s
s的长度。
空间复杂度:
O
(
n
)
O(n)
O(n)。其中,
n
n
n为字符串
s
s
s的长度。