找到包含多数元素的最长子阵列

时间:2018-01-16 03:54:18

标签: arrays algorithm

我正在尝试解决这个算法问题:

https://dunjudge.me/analysis/problems/469/

为方便起见,我总结了下面的问题陈述。

  

给定一个包含[0,1,000,000]范围内整数的长度数组(< = 2,000,000),找到   包含多数元素的最长子阵列

     

多数元素被定义为出现的元素>长度为n的列表中的地板(n / 2)次。

     

时间限制:1.5秒

     

例如:

     

如果给定的数组是[1,2,1,2,3,2],

     

答案是5,因为从位置1到5(0索引)的长度为5的子阵列[2,1,2,3,2]具有数字2,其显示为3> 1。地板(5/2)次。请注意,我们无法获取整个数组,因为3 = floor(6/2)。

<小时/> 我的尝试:

首先想到的是一个明显的暴力(但正确)解决方案,它解决了子阵列的起始和结束索引并循环遍历它以检查它是否包含多数元素。然后我们采用包含多数元素的最长子阵列的长度。这在O(n ^ 2)中工作,具有小的优化。显然,这不会超过时限。

我还在考虑将元素划分为按排序顺序包含其索引的存储桶。

使用上面的示例,这些存储桶将是:

1:0,2

2:1,3,5

3:4

然后对于每个存储桶,我会尝试将索引合并在一起,以找到包含k作为多数元素的最长子数组,其中k是该存储桶的整数标签。 然后我们可以在k的所有值上取最大长度。我没有尝试过这个解决方案,因为我不知道如何执行合并步骤。

<小时/> 有人可以告诉我一个更好的方法来解决这个问题吗?

修改

由于PhamTrung和hk6279的答案,我解决了这个问题。尽管我接受了PhamTrung的答案,因为他首先提出了这个想法,但我强烈建议通过hk6279查看答案,因为他的回答详细阐述了PhamTrung的想法并且更加详细(并且还附带了一个很好的正式证明!)。

4 个答案:

答案 0 :(得分:5)

注意:尝试1是错误的,因为@ hk6279给出了一个反例。谢谢你指出来。

尝试1: 答案非常复杂,所以我将讨论一个简短的想法

让我们逐个处理每个唯一的号码。

在索引x处从左到右处理每个出现的数字i,让添加一个段(i, i)表示当前子数组的开始和结束。之后,我们需要查看此细分的左侧,并尝试将此细分的左邻居合并到(i, i),(因此,如果左边是(st, ed),我们会尝试将其合并如果可能的话,如果它满足条件,则变为(st, i),并继续合并它们直到我们无法合并,或者没有左邻居。

我们将所有这些细分保存在堆栈中,以便更快地查找/添加/删除。

最后,对于每个细分,我们尝试尽可能大地扩大它们,并保持最大的结果。

时间复杂度应为O(n),因为每个元素只能合并一次。

尝试2

让我们逐个处理每个唯一号码

对于每个唯一编号x,我们维护一个计数器数组。从0到数组的末尾,如果我们遇到值x,我们会增加计数,如果我们不这样做,我们会减少,所以对于这个数组 [0,1,2,0,0,3,4,5,0,0]和数字0,我们有这个数组计数器

[1,0,-1,0,1,0,-1,-2,-1,0]

因此,为了使有效的子数组以特定的索引i结束,counter[i] - counter[start - 1]的值必须大于0(如果您将数组视为制作,则可以很容易地解释来自1和-1条目;使用1 is when there is an occurrence of x, -1 otherwise;并且可以将问题转换为查找子数为正的子数组。

因此,在二分搜索的帮助下,上述算法仍然具有O(n ^ 2 log n)的复杂度(如果我们有n / 2个唯一数字,我们需要执行上述过程n / 2次,每次取O(n log n))

为了改进它,我们观察到,我们实际上不需要存储所有计数器的所有值,而只需要存储x计数器的值,我们看到我们可以存储上面的数组计数器:

[1,#,#,0,1,#,#,#, - 1,0]

这将导致O(n log n)解决方案,它只能遍历每个元素一次。

答案 1 :(得分:2)

这详细说明了@PhamTrung解决方案中的尝试2是如何工作的

获得最长子阵列的长度。我们应该

  1. 找到最大值有效数组中的多数元素数,表示为m
    • 这是通过@PhamTrung解决方案中的尝试2完成的。
  2. 返回min(2 * m - 1,给定数组的长度)
  3. <强>概念

    尝试源于a method to solve longest positive subarray

    我们为每个唯一编号x维护一个计数器数组。遇到+1时,我们会x。否则,请执行-1

    取数组[0,1,2,0,0,3,4,5,0,0,1,0]和唯一数字0,我们有数组计数器[1,0,-1 ,0,1,0,-1,-2,-1,0,-1,0]。如果我们盲目那些不是目标唯一数字,我们得到[1,#,#,0,1,#,#,#, - 1,0,#,0]。

    当存在两个计数器时,我们可以从盲计数器数组获得有效数组,使得右计数器的值大于或等于左计数器。见证明部分。

    为了进一步改进它,我们可以忽略所有#,因为它们没用,我们得到[1(0),0(3),1(4), - 1(8),0(9),0(11 )]以count(索引)格式。

    我们可以通过不记录大于其先前有效计数器的记录计数器来进一步改善这一点。以索引8,9的计数器为例,如果你可以用索引9形成子数组,那么你必须能够形成索引为8的子数组。所以,我们只需要[1(0),0(3), - 1 (8)]用于计算。

    您可以通过查找计数器阵列上的二进制搜索,通过查找小于或等于当前计数器值的最接近值(如果找到),使用当前索引形成具有当前索引的有效子阵列

    证明

    对于特定的x,当右计数器大于左计数器r时,其中k,r> = 0,必须有 k + r x个数左计数器后存在 k x个数。因此

    1. 两个计数器位于索引位置i且r + 2k + i
    2. [i,r + 2k + i]之间的子阵列形式恰好 k + r + 1 x
    3. 子阵列长度 2k + r + 1
    4. 子阵列有效( 2k + r + 1 )&lt; = 2 *( k + r + 1 )-1
    5. <强>程序

      1. m = 1
      2. 从左到右循环数组
      3. 对于每个索引p i
        • 如果第一次遇到这个号码,
            
              
          1. 创建新的计数器数组 [1(p i )]
          2.   
          3. 创建新索引记录,存储当前索引值(p i )和计数器值(1)
          4.   
        • 否则,重用数字的计数器数组和索引数组并执行
            
              
          1. 通过c prev + 2-计算当前计数器值c i (p i - p prev ),其中c prev ,p prev 是索引记录中的计数器值和索引值
          2.   
          3. 执行二分查找以查找可以使用当前索引位置和所有先前索引位置形成的最长子阵列。的即。在计数器数组中找到最接近的c,c 最近的,其中c <= c i 。如果未找到,请跳至步骤5
          4.   
          5. 计算第2步中找到的子数组中x 的数量

                 

            r = c i - c nearest

                 

            k =(p i -p 最近 -r)/ 2

                 

            x = k + r + 1

          6. 的数量   
          7. 如果子阵列的数量为m&gt;,则按x的数量更新计数器x
          8.   
          9. 更新计数器数组,如果计数器值小于上次记录的计数器值,则附加当前计数器
          10.   
          11. 按当前索引更新索引记录(p i )和计数器值(c i
          12.   

答案 2 :(得分:0)

为完整起见,这是O(n)理论的概述。考虑以下情况,其中*是不同于c的字符:

    * c * * c * * c c c
 i: 0 1 2 3 4 5 6 7 8 9

1添加c并为1以外的字符减去c的图看起来像:

  sum_sequence

  0    c               c
 -1  *   *   c       c
 -2        *   *   c
 -3              *

上面c所示的上述总和序列最小值的图看起来像:

  min_sum

  0    c * *
 -1  *       c * *
 -2                c c c

很明显,对于每次出现c,我们都在寻找最左边的c,其中sum_sequence小于或等于当前sum_sequence。非负差值表示c是多数,并且最左边保证间隔到我们的位置是最长的。 (我们可以从c的内部边界中推断出一个由c以外的字符所界定的最大长度,因为前者可以灵活地更改而不影响多数。)

观察到,从c到下一次出现,其sum_sequence可以减小任意大小。但是,它只能在两次连续出现的1之间增加c。除了记录min_sum的每个c值之外,我们还可以记录由c出现次数标记的线性段。视觉示例:

[start_min
    \
     \
      \
       \
      end_min, start_min
                  \
                   \
                   end_min]

我们遍历c的出现,并维护一个指向min_sum最佳段的指针。显然,我们可以从前一个值中得出sum_sequence的下一个c值,因为它之间的字符数正好减少了它。

sum_sequencec的增加对应于1向后移动或指向最佳min_sum段的指针没有变化。如果指针没有变化,我们将当前sum_sequence的值作为当前指针值的键进行哈希处理。可以有O(num_occurrences_of_c)个此类哈希键。

c的{​​{1}}值任意减小时,(1)sum_sequence低于记录的最低sum_sequence段,因此我们添加了一个新的细分并更新指针,或者(2)我们之前已经看到了这个确切的min_sum值(因为所有的增加都仅增加sum_sequence),并且可以使用我们的哈希值来检索最优的1 min_sum中的细分。

正如Matt Timmermans在问题注释中指出的那样,如果我们只是通过遍历列表来不断更新指向最佳O(1)的指针,那么我们仍然只会执行min_sum摊销时间迭代每个字符出现。我们看到,对于O(1)的每个递增段,我们可以更新sum_sequence中的指针。如果我们仅对下降使用二进制搜索,则每发生O(1)次事件,我们最多将添加(log k)次迭代(假定我们一直向下跳),这使我们的整体时间保持在{{1} }。

答案 3 :(得分:-1)

算法: 从本质上讲,Boyer-Moore所做的是寻找nums的后缀sufsuf,其中suf [0] suf [0]是该后缀中的主要元素。为此,我们维护一个计数,每当我们看到当前多数元素候选者的实例时,它就会递增,并且每当我们看到其他任何元素时都会递减。只要count等于0,我们就会有效地忘记当前索引中的nums中的所有内容,并将当前数字视为多数元素的候选者。为什么我们可以忘掉nums的前缀并不是很明显 - 请考虑以下示例(将管道插入到非零计数的单独运行中)。

[7,7,5,7,5,1 | 5,7 | 5,5,7,7 | 7,7,7,7]

这里,索引0处的7被选择为多数元素的第一候选者。在处理索引5之后,count最终将达到0,因此索引6处的5将成为下一个候选者。在这种情况下,7是真正的多数元素,因此忽略这个前缀,我们忽略了相同数量的多数和少数元素 - 因此,7仍将是通过丢弃第一个前缀形成的后缀中的多数元素。 / p>

[7,7,5,7,5,1 | 5,7 | 5,5,7,7 | 5,5,5,5]

现在,多数元素是5(我们将数组的最后一次运行从7s更改为5s),但我们的第一个候选者仍然是7.在这种情况下,我们的候选人不是真正的多数元素,但我们仍然不能丢弃多数元素而不是少数元素(这意味着在重新分配候选人之前,计数可能达到-1,这显然是错误的。)

因此,鉴于不可能(在两种情况下)丢弃多数元素而不是少数元素,我们可以安全地丢弃前缀并尝试递归地解决后缀的多数元素问题。最终,将找到一个后缀,其中count不会达到0,并且该后缀的多数元素必然与整个数组的多数元素相同。

这是Java解决方案:

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

    public int majorityElement(int[] nums) {
        int count = 0;
        Integer candidate = null;
    
        for (int num : nums) {
            if (count == 0) {
                candidate = num;
            }
            count += (num == candidate) ? 1 : -1;
        }
    
        return candidate;
    }