(在这个答案的第一个版本中,我提出了一个不幸包含逻辑错误的算法;这是一个完全重新设计的答案版本。)
正如你所说,生成所有子集并检查它们的乘积是否大于 X 给出了 2N 的复杂度,这对于较大的 N 值是有问题的。为了获得更有效的方法,我们'必须找到一种方法来计算有效和无效子集的数量,而无需实际生成它们。
让我们一步一步来,使用这个例子:
7 2 1 6 3 9 7 5 3 1 (X=240)
乘积大于X的子集的数量与输入中整数的顺序无关;所以我们可以先从大到小对输入进行排序,而不改变结果。
9 7 7 6 5 3 3 2 1 1 (X=240)
向子集添加 1 不会改变其乘积。因此,如果列表末尾有一个或多个 1,我们可以将它们删除,并将缩减列表的结果乘以 2M(其中 M 是 1 的数量)。我们现在有一个整数列表,其中 N=8 和最小值为 2。
9 7 7 6 5 3 3 2 (X=240, M=2)
如果我们从末尾开始迭代列表,并将每个整数添加到一个子集中,直到其乘积大于 X,我们就知道随机子集始终有效的最小大小 P(因为我们使用的是最小的整数)。
9 7 7 6 5 3 3 2
2 = 2
3 x 2 = 6
3 x 3 x 2 = 18
5 x 3 x 3 x 2 = 90
6 x 5 x 3 x 3 x 2 = 540 > X -> P = 5
如果我们向这个子集添加整数,或者用更大的整数替换整数,子集仍然有一个大于 X 的乘积。这意味着所有具有至少 P 个整数的子集都是有效的子集。有 (N 选择 P) + (N 选择 P+1) + ... + (N 选择 N) 这些;在我们的示例中,N = 8 和 P = 5:
subsets with size 5 = (8 choose 5) = 56
subsets with size 6 = (8 choose 6) = 28
subsets with size 7 = (8 choose 7) = 8
subsets with size 8 = (8 choose 8) = 1
--
93 subsets
(请注意,P 受目标 X 的位大小限制。如果 X 是 64 位无符号整数,则任何 64 个整数的子集将始终具有大于 X 的乘积。)
如果我们从头开始迭代列表,并将每个整数添加到一个子集,而它的乘积小于 X,我们就知道随机子集不能对其有效的最大大小 Q(因为我们使用的是最大的整数)。
9 7 7 6 5 3 3 2
9 = 9
9 x 7 = 63 -> Q = 2
9 x 7 x 7 = 441 > X
如果我们从这个子集中删除整数,或者用更小的整数替换整数,这个子集仍然有一个小于 X 的乘积。这意味着所有最多有 Q 个整数的子集都不是有效的子集。
所以现在我们知道大小为 1 和 2 的子集都是无效的,大小为 5 到 8 的 93 个子集都是有效的,我们必须检查大小为 3 和 4 的子集;我们将按尺寸检查它们,先是 3,然后是 4。
为了避免生成和检查所有大小为 3 的子集,我们会将选择分成最后具有一定数量 0(未选择的整数)的组,并计算可能的最低(和最高)乘积,例如对于带有三个尾随 0 的组:
* * * * 1 0 0 0 <- all possibilities
0 0 1 1 1 0 0 0 <- smallest possible product
1 1 0 0 1 0 0 0 <- greatest possible product
在给出这些最小和最大产品的示例中:
9 7 7 6 5 3 3 2 (X=240, SIZE=3)
* * 1 0 0 0 0 0 = 441 ~ 441 > X (2 choose 2) = 1
* * * 1 0 0 0 0 = 294 ~ 378 > X (3 choose 2) = 3
* * * * 1 0 0 0 = 210 ~ 315
* * * * * 1 0 0 = 90 ~ 189 < X
* * * * * * 1 0 = 45 ~ 189 < X
* * * * * * * 1 = 18 ~ 126 < X
我们发现前两组的乘积总是大于 X(我们只需要计算最小乘积即可知道这一点),因此我们计算它们的数量并将其添加到总数中。对于最小值小于 X 的组,我们也计算最大值,我们发现最后三个组总是有一个小于 X 的乘积,所以我们可以忽略它们。
(一旦找到最大值低于 X 的组,您可以跳过检查下一组,因为它们的最大值永远不会更大。)
剩下的就是后面三个 0 的组。要检查该组中有多少子集是有效的,我们选择最小整数 (5),减小子集大小,然后递归:
9 7 7 6 (product * 5 > X=240, SIZE=2)
* 1 0 0 = 63 ~ 63 * 5 > X (1 choose 1) = 1
* * 1 0 = 49 ~ 63 * 5 > X (2 choose 1) = 2
* * * 1 = 42 ~ 54 * 5
我们发现前两组总是有效的,但是第三组的乘积可以小于或大于X。所以我们选择最小的整数(6),递减子集大小,递归:
9 7 7 (product * 5 * 6 > X=240, Size=1)
1 0 0 = 9 * 30 > X (0 choose 0) = 1
* 1 0 = 7 * 30 < X
* * 1 = 7 * 30 < X
我们发现只有第一个组总是有效的,所以我们计算它的数量(这是最低的递归级别,所以这是 1)并将其添加到总数中。所以我们发现大小为 3 的有效子集的数量是 8。然后我们继续对大小为 4 的子集做同样的事情:
9 7 7 6 5 3 3 2 (X=240, SIZE=4)
* * * 1 0 0 0 0 = 2646 ~ 2646 > X (3 choose 3) = 1
* * * * 1 0 0 0 = 1470 ~ 2205 > X (4 choose 3) = 4
* * * * * 1 0 0 = 630 ~ 1323 > X (5 choose 3) = 10
* * * * * * 1 0 = 270 ~ 1323 > X (6 choose 3) = 20
* * * * * * * 1 = 90 ~ 882
我们对最后一组进行递归:
9 7 7 6 5 3 3 (product * 2 > X=240, SIZE=3)
* * 1 0 0 0 0 = 441 ~ 441 * 2 > X (2 choose 2) = 1
* * * 1 0 0 0 = 294 ~ 378 * 2 > X (3 choose 2) = 3
* * * * 1 0 0 = 210 ~ 315 * 2 > X (4 choose 2) = 6
* * * * * 1 0 = 90 ~ 189 * 2
* * * * * * 1 = 45 ~ 189 * 2
我们递归倒数第二组:
9 7 7 6 5 (product * 2 * 3 > X=240, SIZE=2)
* 1 0 0 0 = 63 ~ 63 * 6 > X (1 choose 1) = 1
* * 1 0 0 = 49 ~ 63 * 6 > X (2 choose 1) = 2
* * * 1 0 = 42 ~ 54 * 6 > X (3 choose 1) = 3
* * * * 1 = 30 ~ 45 * 6
我们对最后一组进行递归:
9 7 7 6 (product * 2 * 3 * 5 > X=240, SIZE=1)
1 0 0 0 = 9 * 30 > X (0 choose 0) = 1
* 1 0 0 = 7 * 30 < X
* * 1 0 = 7 * 30 < X
* * * 1 = 6 * 30 < X
我们向后退一步并递归到最后一组:
9 7 7 6 5 3 (product * 2 * 3 > X=240, SIZE=2)
* 1 0 0 0 0 = 63 ~ 63 * 6 > X (1 choose 1) = 1
* * 1 0 0 0 = 49 ~ 63 * 6 > X (2 choose 1) = 2
* * * 1 0 0 = 42 ~ 54 * 6 > X (3 choose 1) = 3
* * * * 1 0 = 30 ~ 45 * 6
* * * * * 1 = 15 ~ 27 * 6 < X
我们递归倒数第二组:
9 7 7 6 (product * 2 * 3 * 5 > X=240, SIZE=1)
1 0 0 0 0 = 9 * 30 > X (0 choose 0) = 1
* 1 0 0 0 = 7 * 30 < X
* * 1 0 0 = 7 * 30 < X
* * * 1 0 = 6 * 30 < X
所以对于大小为 4 的子集,我们总共找到 59 个有效子集;这给出了:
subsets with size 5 ~ 8 = 93
subsets with size 4 = 59
subsets with size 3 = 8
---
160
因为我们在开始时去掉了两个 1,所以我们必须将其与 22 相乘,总共是:
160 * 2^2 = 640
总共 2^10 = 1024 个子集中的有效子集。为了得到这个结果,我们计算了大约 50 个子集的乘积。
从示例中可以看出,有一些子集产品的计算在多次递归中重复。这意味着动态编程技术(例如记忆化)可以大大加快算法速度。
复杂性和最坏的情况
输入的排序需要 O(N.LogN),然后是几个步骤,每个步骤都需要 O(N)。
对于最后一步,最坏情况的输入当然是您可以创建一个包含 1 个整数的有效子集和一个包含 N-1 个整数的无效子集,例如如果列表包含大量小整数和比目标 X 大一个大整数;这意味着您必须详细检查所有子集大小。
最后一步很难量化;最坏的情况至少是 O(N2),但随后递归深度开始发挥作用。我猜一个包含很多重复值的列表会产生很多重叠范围和大量递归,以及 O(N3) 的复杂性。
将最后一步视为最复杂的步骤似乎很明显,因此将整体复杂度置于 O(N3),但因为大小为 64 或更大的子集始终有效(对于X)的64位值,最后一步的复杂度可以增长多少是有限制的,对于较大的N值,算法开头的排序可能会成为主导因素。