【问题标题】:Longest substring repeating atleast k times最长重复至少 k 次的子串
【发布时间】:2020-04-15 18:06:17
【问题描述】:

我得到一个大长度的字符串(比如 100,000)和一个整数 k,我必须计算在给定字符串中至少重复 k 次的最大子字符串的长度。
我找到了这个特定问题herehere 的答案,但我想知道除了后缀树之外是否还有其他有效的方法来解决这个问题?

【问题讨论】:

  • 它的效率较低,但比后缀树更容易理解:对答案长度进行二分搜索,计算这种长度的子字符串的所有多项式哈希 (cp-algorithms.com/string/string-hashing.html) 并查看是否存在具有>= k 次。计算所有固定长度子串的哈希是O(n),二分查找有log n次迭代,所以总体复杂度是O(n log n).
  • @Fas 进行二分查找时,如何向左或向右移动?
  • @vivek_23 取决于是否存在 >= k 出现的散列。这行得通,因为如果有一个字符串 s 出现 k 次,s[:-1] 也至少有 k 次出现。
  • @fas 但是如何决定向左还是向右呢?较长的字符串很可能出现 >= k 次?
  • @vivek_23 我们不收集子字符串,我们收集它们的多项式哈希(它是什么以及它是如何工作的,您可以在这里阅读cp-algorithms.com/string/string-hashing.html),这可以在O(n) 中为所有子字符串完成由二进制搜索长度固定的m

标签: algorithm performance data-structures memory-efficient


【解决方案1】:

在cmets里面讨论的比较多,我觉得还是写个答案总结一下比较好。 TL;博士Longest substring repeating atleast k times

还有一种效率较低的方法,但它确实比后缀树更容易理解:您只需要知道多项式哈希和二分查找即可。

1。字符串多项式哈希

在此处阅读 https://cp-algorithms.com/string/string-hashing.html。以下是该技术的简短说明。

假设我们有字符串s、整数pmod。现在我们可以定义哈希函数了:

hash(s) = (ord(s[0])*p^(len(s)-1) + ord(s[1])*p^(len(s)-2) + ... + ord(s[len(s)-1])*p^0) % mod 

其中ord 是一个按字符返回整数的函数(假设它是一个字符的ASCII 码)。 O(len(s))中字符串的每个前缀可以很容易地计算多项式哈希:

# h[i] is a hash of prefix of length i.
# For example s = "abacaba",
# h[0] = hash("") = 0
# h[1] = hash("a")
# h[2] = hash("ab")
# ...
# h[7] = hash("abacaba")

h[0] = 0
for i in 1..n:
    h[i] = (h[i-1] * p + ord(s[i-1])) % mod

另外让我们预先计算数组pow中的p^0 % mod, p^1 % mod, ..., p^len(s) % mod

# pow[i] is (p^i) % mod
pow[0] = 1
for i in 1..n:
    pow[i] = (pow[i-1] * p) % mod

使用数组hpow,我们可以轻松计算字符串s 的任何子字符串的哈希:

# get_substring_hash returns hash(s[l] + s[l+1] + ... + s[r-1]).
def get_substring_hash(s, l, r):
    value = h[r] - h[l]*pow[r-l]    # line a
    return (value%mod + mod) % mod  # line b

让我们了解为什么上面的代码有效。

h[r] = (ord(s[r-1])*p^0 + ord(s[r-2])*p^1 + ... + ord(s[l-1])*p^(r-l) + ord(s[l-2])*p^(r-l+1) + ...) % mod
h[l] = (                                          ord(s[l-1])*p^0     + ord(s[l-2])*p^1       + ...) % mod
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

如您所见,我们只需要来自h[r]^^^-part,因此我们必须摆脱~~~-part。 ~~~-part in h[r] p^(r-l)h[l] 大倍,这解释了 line a

Line b 使用% mod 操作时有点神奇,line a 之后的value 可以是负数,所以value%mod + mod 肯定是正数。同时如果valueline a之后为正,value%mod + mod大于mod,那么(value%mod + mod) % mod肯定会返回0、1、...范围内的值, mod-1.

最后,mod 是一个大素数,如 10^9+7value 是一个小数,但比任何可能的 ASCII 码如 239 em>(阅读文章为什么会这样)。

一些注意事项:

  • 哈希可能会发生冲突,因为我们只有mod 可能的哈希值,但字符串的数量是无限的。如何处理请参阅文章。
  • h[r] - h[l]*pow[r-l] 这样的操作可能需要使用 64 位类型的整数。

2。二分查找

只需在 Wikipedia 上阅读,没有具体的 https://en.wikipedia.org/wiki/Binary_search_algorithm

3。最长重复至少k次的子串解

假设我们预先计算了数组hpow。让我们进行二分搜索以找到字符串的最大长度ans,使得在给定的字符串s 中有k 或更多这样的子字符串。

为什么二进制搜索在这里有效?因为如果有一些长度x,例如在s 中有k 或更多相等的子字符串,长度为x,那么在s 中肯定有k 或更多相等的子字符串,长度为x-1(只是从我们的比赛中删除最后一个字母)。

二分查找如何工作?假设我们目前正在测试是否有k 或更多长度相等的子字符串mid。我们将计算长度为mid(使用get_substring_hash)的所有哈希值,如果k 的哈希值相等,我们将决定更改二分搜索的边界。

例如:s = "abcabcdefgdefgdefgdefg", k = 3。答案是“defgdefg”

abcabcdefgdefgdefgdefg
      ^^^^^^^^          occurence 1
          ^^^^^^^^      occurence 2
              ^^^^^^^^  occurence 3

二分搜索迭代:

l =  1, r = 22, mid = 11. No substring of length 11 satisfy.
l =  1, r = 10, mid =  5. There should be hash("defgd")    be seen 3 times.
l =  5, r = 10, mid =  7. There should be hash("defgdef")  be seen 3 times.
l =  7, r = 10, mid =  8. There should be hash("defgdefg") be seen 3 times.
l =  8, r = 10, mid =  9. No substring of length 9  satisfy.
l =  8, r =  8.           That means answer is 8.

如您所见,复杂度为 O(n log n)round(log n) 二分查找迭代和 O(n) 每次迭代的复杂度,如果您使用类似 std::unordered_map 的东西来检查是否存在 >= k 出现的散列。

我真的希望现在一切都清楚了。

【讨论】:

  • 非常感谢您的回答。我理解了算法。只有一件事,这里的“p”是什么?它可以是任何数字,对吧?
  • 它绝对可以是一个大于函数ord 返回的任何数字的素数(大于任何ASCII 码)。我不能肯定地说p 不能是素数,似乎它也可以工作
猜你喜欢
  • 1970-01-01
  • 2012-10-29
  • 2021-10-27
  • 2011-10-02
  • 2020-11-08
  • 1970-01-01
  • 2014-03-22
  • 2016-11-17
  • 2023-03-17
相关资源
最近更新 更多