(也许是NP-Hard)求一个集合的子集总数,使得每个子集在与其所有元素相乘时的值都大于X

(maybe NP-Hard) Find total number of subsets of a set such that every subset when multiplied with all its elements has value greater than X

本文关键字:子集 元素 大于 NP-Hard 也许 一个 集合      更新时间:2023-10-16

有一个大小n的数组arr。那么,有多少2^n子集中的总乘积大于一个数字,比如X?

n大约是 2^5,X 可以在 2^60 左右更大(任何可以容纳在 C++long变量中)

我认为类似于子集和的东西会起作用,但现在我真的不这么认为。


我从Codeforce过去的比赛中想到了这一点。虽然这个问题不需要我问什么,但我很好奇。

您的问题可以通过按原样动态编程来解决,即不使用日志。

当输入整数以二进制(而不是一元)给出时, 在非自适应 2 查询缩减下,您的问题是 NP-hard 的,因为:

乘积等于 X&等于的子集数

;
乘积大于 X-1

的子集数−
乘积大于 X 的子集数。

我没有立即看到任何显示实际 NP 硬度的方法(即,在 1 个查询还原下)。

(在这个答案的第一个版本中,我提出了一个算法,不幸的是包含一个逻辑错误;这是一个完全重新设计的答案版本。

正如您所说,生成所有子集并检查它们的乘积是否大于 X 会给出 2 N 的复杂度,这对于较大的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 的大值, 算法开始时的排序可能成为主导因素。