查找字符串的子串,使得子串的长度与其出现次数的乘积最大化

Posted

技术标签:

【中文标题】查找字符串的子串,使得子串的长度与其出现次数的乘积最大化【英文标题】:Finding substrings of string such that product of the length of the substring with its number of occurrences is maximized 【发布时间】:2014-09-08 00:38:06 【问题描述】:

我正在考虑以下问题: 给定一个字符串 S,设第 ith 个子串的长度为 li,第 ith 个子串的出现次数为 o。打印子字符串,使 li*oi 最大化。

对于这个问题,我有 O(n3) 解决方案(蛮力),我正在生成所有子字符串并找到具有最大值的子字符串。我的代码如下:

public static void solve(String S) 
    long max = Integer.MIN_VALUE;
    String res = "";
    for (int i = 0; i < S.length(); i++) 
        for (int j = 1; j <= S.length() - i; j++) 
            String s = S.substring(i, i + j);
            int o = countOccurrences(S, s);
            long p = (long) o * (long) s.length();
            if (max < p) 
                max = p;
                res = s;
            
        
    
    System.out.println(res);

countOccurrences() 方法需要 O(n) 时间。我想知道是否有更有效的方法来实现这一点。

【问题讨论】:

这些事件可以相互重叠吗?例如。对于 S = 'ababa',答案是否 = 3 * 2 = 6? 是的。事件可以重叠。 【参考方案1】:

这是一个线性时间算法:

    在输入字符串上构建后缀树。这需要 O(n) 时间和空间。 在后序 DFS 中遍历后缀树,通过对其子节点的值求和来计算每个节点的子节点数。一旦节点的这个数量是已知的,就将它乘以它的字符串长度(这是从根开始的所有边的长度之和),并在必要时更新迄今为止的最佳总数。这也需要 O(n) 时间。

重点是

后缀树只包含线性数量的内部节点,并且 任何不对应于内部节点的子字符串都不能产生最大分数。这是因为当您从后缀树根跟踪它时,它必须到达某个边缘的“中途”——但您总是可以在不减少出现次数(即后代的数量)的情况下进一步扩展它,从而通过继续向下到下一个节点来增加分数。

也可以使用后缀数组而不是后缀树来做到这一点,在这种情况下,它可能需要一个常数因子更少的内存,但在运行时间中添加一个对数因子。

【讨论】:

为什么后缀数组要在运行时间上加上对数因子?有算法在线性时间内构建 SA... @EvgenyKluev:我知道 SA 构建的线性时间算法,但我不确定 DFS 所需的时间。也许这也可以使用 LCA 表在线性时间内完成?请提出您的方法,或直接编辑我的答案。 有一种已知的方法来模拟 SA 中的 DFS,方法是预处理 LCA 数组以进行范围最小查询,然后使用指针对遍历数组。线性时间预处理和恒定时间查询RMQ是可能的,但并不容易实现。 有一篇很好的论文 "Replacing suffix trees with enhanced suffix arrays" 解释了如何在 SA 中执行 DFS 和许多其他任务。 @EvgenyKluev:谢谢!【参考方案2】:

一个想法:您可以通过创建一个出现列表来缩短算法。这样,在运行 countOccurences 之前,您会检查它是否还没有被计算在内,并且您不会再次运行 countOccurences 来处理同样的事件。

这是第一次优化,应该还有其他优化,但这是一个开始。

【讨论】:

【参考方案3】:

大概你可以用你剩下的来计算最大可用的“分数”,从而避免检查超出某个点的子字符串。我猜它的扩展性仍然很差,但它会人为地降低你的 'n' 值。

例如,如果您有一个长度为 10 的字符串,并且您设法在前 3 个字符中两次获得 3 个字符的子字符串(即 0-2、3-5),那么您可以说 max = 6。那一点,一旦超过i=4,你的得分不能超过6,所以你可以停下来?

【讨论】:

【参考方案4】:

改进的算法如下:-(伪代码)

int max = 0;

 for(int i=0;i<str.length;) 

   occurences = get_index(str[i]);

   for(int j=i+1;j<str.length;j++) 

      if(occurences.size()==0)
         break;

      for(int k=0;k<occurences.size();k++) 

           int next_ind = occurrences[k]+j-i;

           if(next_ind==length||str[next_ind]!=str[j]) 
               delete(occurences[k]);
           
       

       int score = ocurrences.size()*(j-i+1);

       if(max<score)
         score = max; 
    

 

时间复杂度:-

理想情况下,子字符串的出现次数应该像n,n/2,n/3.... 一样减少到最大值,因此算法是n*n*(1+1/2+1/3...) = O(n^2*logn) as (1+1/2+1/3...1/n) =&gt; logn。使用链表在O(1)中可以很容易地完成删除。

【讨论】:

以上是关于查找字符串的子串,使得子串的长度与其出现次数的乘积最大化的主要内容,如果未能解决你的问题,请参考以下文章

子串统计

怎么查找一个string 字符串中的子字符串出现的次数和位置

最短假子串

1297. 子串的最大出现次数

[LeetCode]子串的最大出现次数(字符串)

求字符串中出现次数最多的子串及其出现次数