【问题标题】:Count how many palindromes in a string计算一个字符串中有多少个回文
【发布时间】:2019-06-19 23:21:59
【问题描述】:

我想找出使用​​给定字符串中的子字符串可能出现的所有可能的回文。

Example: input = abbcbbd.
Possible palindromes are a,b,b,c,b,b,d,bb,bcb, bbcbb,bb

这是我实现的逻辑:

public int palindromeCount(String input) {
    int size = input.length();// all single characters in string are treated as palindromes.
    int count = size;
    for(int i=0; i<size; i++) {
        for(int j=i+2; j<=size; j++) {
            String value = input.substring(i, j);
            String reverse = new StringBuilder(value).reverse().toString();
            if(value.equals(reverse)) {
                count++;
            }
        }
    }
    return count;
}

这里时间复杂度比较高,请问如何提高这个逻辑的性能?

【问题讨论】:

  • all possible palindromes that can be possible from a given string. 是什么意思?你的意思是重新排列字母还是严格的子字符串
  • @JClassic,是的,只有严格的子字符串更新了我的问题以使其清楚
  • 你的例子中的bcb怎么样?
  • @Code-Apprentice,是的,我错过了,现在包含在其中
  • 您确定要像您的示例那样多次显示相同的回文吗?我希望 abbcbbd 产生 a、b、c、d、bb、bab、bbb、bcb、bdb、bbbb、bbabb、bbcbb、bbdbb。

标签: java string algorithm palindrome


【解决方案1】:

在优化此算法时,您可以考虑以下几点:

  1. 什么是回文?回文是一个对称字符串,这意味着它必须有一个中心枢轴!枢轴可能是以下之一:

    • 一个字母,如“aba”,或

    • 两个字母之间的位置,如字母“aa”之间的位置

这意味着总共有 2n − 1 个可能的支点。

然后,您可以从每个枢轴向外搜索。这是一个例子:

  • 示例字符串:“abcba”
  • 首先,让我们以“c”为轴:
    • abcba → c 是回文,然后将搜索范围每边扩大 1。
    • abcba → bcb 是回文,然后将搜索范围每边扩大 1。
    • abcba → abcba 是回文,所以我们知道字符串中至少有 3 个回文。
  • 对所有枢轴继续此操作。

这种方法,运行时复杂度是O(n2)。

【讨论】:

  • 如何确定复杂度为O(n^2)?你能更详细地解释一下这个分析吗?另外,OP的解决方案是O(n^2)吗?我看到了嵌套的 for 循环,但是如果我的计算正确,substring()reverse() 中隐藏了一些额外的循环,这使得他的解决方案 O(n^3)
  • 是的,我确实认为 OP 的答案是 n^3,因为它使用了更糟糕的 n 的反向。我的答案是n^2,因为它在每个枢轴上的运行时间最多为n/22n - 1 枢轴
  • @Code-Apprentice:正如你所说的,OP 的解决方案确实是 O(n³)。这个答案中的解决方案是 O(n²),因为搜索以给定枢轴为中心的长度为 m 的回文只需要 O(1),这是可能的,因为它使用搜索的结果一个长度为 *m*−2 的回文,以同一枢轴为中心。
【解决方案2】:

如果您对破坏一些重量级数据结构感到满意,则可以在 O(n) 时间内完成,尽管我承认这不是特别容易编码的东西。 :-)

我们需要两个工具来解决这个问题。

工具一:广义后缀树。广义后缀树是一种数据结构,直观地说,它是一个包含两个字符串 S 和 T 的所有后缀的 trie,但以更节省空间的方式表示.

工具二:最低共同祖先查询。最低共同祖先查询结构(或 LCA 查询)是围绕特定树构建的数据结构。它旨在有效地回答“给定树中的两个节点,它们的最低共同祖先是什么?”形式的查询。

重要的是,可以在 O(m) 时间内构建两个长度为 m 的字符串的通用后缀树,并且可以在 O(m) 时间内构建 LCA 查询,这样所有查询都需要 O(1) 时间。这些不是明显的运行时;此处所需的算法和数据结构在首次发现时是可发布的结果。

假设我们有这两个结构,我们可以构建第三种数据结构,我们将使用它来获得最终算法:

工具三:最长公共扩展查询。最长公共扩展查询数据结构(或 LCE 查询)是围绕两个字符串 S 和 T 构建的数据结构。它支持以下形式的查询:给定字符串 S 中的索引 i 和字符串 T 中的索引 j,从 S 中的索引 i 和 T 中的索引 j 开始出现的最长字符串的长度是多少?

以这两个字符串为例:

S: OFFENSE
   0123456

T: NONSENSE
   01234567

如果我们从字符串 S 中的索引 3 和字符串 T 中的索引 4 开始执行 LCE 查询,答案将是字符串 ENSE。另一方面,如果我们从字符串 S 中的索引 4 和字符串 T 中的索引 0 开始执行 LCE 查询,我们会得到字符串 N。

(更严格地说,LCE 查询结构实际上并没有返回您在这两个地方找到的实际字符串,而是返回它的长度。)

可以在时间 O(m) 内为一对长度为 m 的字符串 S 和 T 构建 LCE 数据结构,这样每个查询都需要时间 O(1)。这样做的技术包括为两个字符串构建一个通用的后缀树,然后在顶部构建一个 LCA 数据结构。关键的见解是,从字符串 S 中的位置 i 和字符串 T 中的 j 开始的 LCE 由后缀树中字符串 S 的后缀 i 和字符串 T 的后缀 j 的最低共同祖先给出。

LCE 结构对于这个问题非常有用。要了解原因,让我们使用您的示例字符串abbcbbd。现在,考虑该字符串及其反转,如下所示:

 S: abbcbbd
    0123456

 T: dbbcbba
    0123456

字符串中的每个回文都采用两种形式之一。首先,它可能是一个奇数长度的回文。像这样的回文有一些中心字符 c,加上一些从该中心向前和向后延伸的“半径”。例如,字符串bbcbb 是一个奇长回文串,中心为c,半径为bb

我们可以通过使用 LCE 查询来计算字符串中有多少奇数长度的回文。具体来说,在字符串及其反向上构建 LCE 查询结构。然后,对于原始字符串中的每个位置,询问原始字符串中该位置的 LCE 及其在镜像字符串中的对应位置。这将为您提供以该点为中心的最长奇数回文。 (更具体地说,它会给你半径加一的长度,因为角色本身总是在这两点匹配)。一旦我们知道 最长 个以该位置为中心的奇数长度回文串,我们就可以计算 个以该位置为中心的奇数长度回文串:将等于我们可以通过切断前后字符来获取更长的回文并缩短它的所有方法。

考虑到这一点,我们可以按如下方式计算字符串中所有奇数长度的回文:

for i from 0 to length(S) - 1:
    total += LCE(i, length(S) - 1 - i)

另一类回文数是偶数长度的回文数,它没有中心,而是由两个相等的半径组成。我们也可以使用 LCE 查询找到这些,除了查看某个位置 i 及其在反转字符串中的对应位置,我们将查看原始字符串中的位置 i 和对应于索引 @987654330 的位置@ - 1 在反向字符串中。可以在这里完成:

for i from 1 to length(S) - 1:
    total += LCE(i, length(S) - i)

总的来说,这个解决方案

  • 从原始字符串和反向字符串构造 LCE 查询结构。反转字符串需要时间 O(m),构建 LCE 查询结构需要时间 O(m)。此步骤的总时间:O(m)
  • 对 LCE 结构进行总共 2m - 1 次查询。每个查询花费时间 O(1)。此步骤的总时间:O(m)

我相当肯定在不使用这些重量级工具的情况下实现这个运行时是可能的,但这至少表明存在线性时间解决方案。

希望这会有所帮助!

【讨论】:

    猜你喜欢
    • 2019-11-03
    • 2013-11-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-26
    • 1970-01-01
    • 2023-03-22
    • 2016-11-22
    相关资源
    最近更新 更多