为什么这个最大产品子阵列算法有效?

时间:2016-03-21 22:25:57

标签: algorithm

问题是在数组(包含至少一个数字)中找到具有最大产品的连续子阵列。

例如,给定数组[2,3], 连续的子阵列6具有最大的乘积if(nums == null || nums.Length == 0) { throw new ArgumentException("Invalid input"); } int max = nums[0]; int min = nums[0]; int result = nums[0]; for(int i = 1; i < nums.Length; i++) { int prev_max = max; int prev_min = min; max = Math.Max(nums[i],Math.Max(prev_max*nums[i], prev_min*nums[i])); min = Math.Min(nums[i],Math.Min(prev_max*nums[i], prev_min*nums[i])); result = Math.Max(result, max); } return result;

为什么以下工作?任何人都可以提供有关如何证明其正确性的任何见解吗?

AUG

2 个答案:

答案 0 :(得分:2)

从逻辑方面开始,了解如何解决问题。每个子阵列有两个相关特征需要考虑:

  • 如果它包含0,则子阵列的乘积也是0。
  • 如果子阵列包含奇数个负值,则其总值为负,否则为正(或0,将0视为正值)。

现在我们可以从算法本身开始:

规则1:零

由于0将子阵列的乘积归零,因此解决方案的子阵列不能包含0,除非输入中仅包含负值和0。这可以非常简单地实现,因为只要在数组中遇到0,maxmin都会重置为0:

max = Math.Max(0 , Math.Max(prev_max * 0 , prev_min * 0));
min = Math.Min(0 , Math.Min(prev_max * 0 , prev_min * 0));

无论到目前为止的输入是什么,逻辑上都会评估为0。

arr:    1  1  1  1  0  1  1  1  0  1  1  1  0  1  1  0
result: 1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1
min:    0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
max:    1  1  1  1  0  1  1  1  0  1  1  1  0  1  1  0
//non-zero values don't matter for Rule 1, so I just used 1

规则2:负数

使用规则1,我们已经隐式地将数组拆分为子数组,这样子数组可以包含单个0或多个非零值。现在的任务是找到子阵列中最大的可能产品(我将从这里开始称之为数组)。

如果数组中的负值数是偶数,则整个问题变得非常简单:只需乘以数组中的所有值,结果就是数组的最大乘积。对于奇数个负值,有两种可能的情况:

  1. 数组只包含一个负值:在这种情况下,所有索引小于负值的子数组或子数组的索引都大于负值的子数组将成为具有最大值<的子数组< / LI>
  2. 该数组包含至少3个负值:在这种情况下,我们必须消除第一个负数及其所有前任,或最后一个负数及其所有后继。< / LI>

    现在让我们来看看代码:

    max = Math.Max(nums[i] , Math.Max(prev_max * nums[i] , prev_min * nums[i]));
    min = Math.Min(nums[i] , Math.Min(prev_max * nums[i] , prev_min * nums[i]));
    

    案例1:min的评估实际上是无关紧要的,因为对于负值,数组乘积的符号只会翻转一次。一旦遇到负数(= nums[i]),max将为nums[i],因为maxmin都至少为1,因此乘以nums[i]会生成<= nums[i]个数字。对于负数nums[i + 1]之后的第一个数字,max将再次为nums[i + 1]。由于到目前为止发现的最大值在每个步骤后在resultresult = Math.Max(result, max);)中保持不变,因此这将自动导致该数组的正确结果。

    arr:     2   3   2  -4   4   5
    result:  2   6  12  12  12  20
    max:     2   6  12  -4   4  20  
    //Omitted min, since it's irrelevant here.
    

    案例2:此处min也变得相关。在我们遇到第一个负值之前,min是到目前为止在数组中遇到的最小数字。在我们遇到数组中的第一个正元素后,该值变为负数。我们会继续构建这两种产品(minmax),并在每次遇到负值时进行交换,并不断更新result。当遇到数组的最后一个负值时,result将保存子阵列的值,该值消除了最后一个负值及其后继值。在最后一个负值之后,max将是子阵列的产物,它消除了第一个负值并且它的前辈和min变得无关紧要。现在我们继续将max与数组中的剩余值相乘并更新result,直到到达数组的末尾。

    arr:    2   3   -4    3    -2    5     -6    3
    result: 2   6    6    6   144  770    770  770
    min:    2   6  -24  -72    -6  -30  -4620  ...
    max:    2   6   -4    3   144  770    180  540 
    //min becomes irrelevant after the last negative value
    

    将各个部分放在一起

    由于每次遇到0都会重置minmax,因此我们可以轻松地为每个不包含0的子数组重用它们。因此,隐式应用规则1而不会干扰使用规则2.由于每次检查新子数组时result都不会重置,因此该值将在所有运行中保持不变。因此这个算法有效。

    希望这是可以理解的(说实话,我对此表示怀疑,如果出现任何问题,我会尝试改进答案)。为那个可怕的答案而烦恼。

答案 1 :(得分:1)

让我们假设生成最大乘积的连续子阵列为a[i], a[i+1], ..., a[j]。由于它是具有最大产品的数组,因此它也是a[0], a[1], ..., a[j]的后缀,可生成最大的产品。

您给定算法的想法如下:对于每个前缀数组a[0], ..., a[j],找到最大的后缀数组。在这些后缀数组中,取最大值。

最初,最小和最大的后缀产品只是nums[0]。然后它迭代数组中的所有其他数字。最大的后缀数组总是以三种方式之一构建。它只是最后一个数字nums[i],它是缩短列表的最大后缀乘以乘以最后一个数字(如果nums[i] > 0),或者它是最小的(< 0)后缀 - 乘积乘以按最后一个数字(如果nums[i] < 0)。 (*)

使用辅助变量result,存储到目前为止找到的最大此类后缀产品。

(*)这个事实很容易证明。如果你有不同的情况,例如存在一个产生更大数字的不同后缀产品,而不是最后一个数字nums[i],你会创建一个更大的后缀,这将是一个矛盾。