【LeetCode双周赛75T4】6036. 构造字符串的总得分和【困难】字符串哈希+二分查找

题目来源于leetcode,解法和思路仅代表个人观点。传送门
难度: 困难(T4)
时间:-
字符串哈希没用过,第一次见。

题目

你需要从空字符串开始 构造 一个长度为n的字符串s,构造的过程为每次给当前字符串 前面 添加 一个 字符。构造过程中得到的所有字符串编号为 1 n ,其中长度为 i 的字符串编号为 si

比方说,s = "abaca"s1 == "a" s2 == "ca"s3 == "aca" 依次类推。
si得分sisn最长公共前缀 的长度(注意 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 只包含小写英文字母。

思路

参考:
『 暴力、字符串编码、Z函数 』三种解法详解
LeetCode: 1392. 最长快乐前缀

扫描二维码关注公众号,回复: 14622902 查看本文章

二分查找

枚举每个后缀,在原串中使用 二分查找 最长的公共前缀长度。

为什么可以二分?
当两个字符串的长度为k的前缀相等时,长度为k-1的前缀也相等。

在二分的过程中,当后缀长度为 k k k时,即判断 s [ m − k : m i d ] s[m-k:mid] s[mk: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(rl)。如果需要判断 n n n次,那么时间复杂度为 O ( n ⋅ ( r − l ) ) O(n \cdot (r-l)) O(n(rl))
  • 如果,使用字符串哈希,那么构建时,复杂度为 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,...,i9个字符。我们用其构建长度为 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=0s1(s[i])bases1i
其中, ∣ 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=1102+210+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]范围内。

随之带来另一个问题,哈希碰撞
解决办法:

  1. 多次哈希。 即,取不同 K K K个的 b a s e i base_i basei m o d i mod_i modi,对字符串进行多次哈希操作。当且仅当,两个字符串这 K K K次的哈希值都相同,则两个字符串相同。
  2. 再哈希。 即,第一次哈希 得到 哈希值 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)=010+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[i1]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[l1]baserl+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=123412100=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;
    }
};

简化版:

  1. 使用unsigned,自然溢出,就不用MOD了。
  2. 使用n+1长度的数组,少一个越界的判断。
  3. 使用二分查找-模板二。少一个判断条件。
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+nk=1nlogk)。其中, n n n为字符串 s s s的长度。
空间复杂度: O ( n ) O(n) O(n)。其中, n n n为字符串 s s s的长度。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/LittleSeedling/article/details/123956815
今日推荐