(也许是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
有一个大小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 的大值, 算法开始时的排序可能成为主导因素。
- Mongodb c++驱动程序:如何查询元素的数组
- 将数组作为参数传递给函数安全吗?作为第三方职能部门,可以探索他们想要的之外的其他元素
- 使用strcpy将char数组的元素复制到另一个数组
- 使用不带参数的函数访问结构元素
- 给定n个元素的m个集合.在C++中找到出现在最大集合数中的元素
- C++如何通过用户输入删除列表元素
- lower_bound()返回最后一个元素
- 基于多个条件处理地图中的所有元素
- 调整大小后指向元素值的指针unordered_map有效?
- 使用std::transform将一个范围的元素添加到另一个范围中
- (也许是NP-Hard)求一个集合的子集总数,使得每个子集在与其所有元素相乘时的值都大于X
- 在查找子集中元素和元素数量之间的二进制比较背后的逻辑是什么?
- 如果连接了换成气泡排序的元素,请找到所有非连接元素的最大子集
- 通过单个向量进行迭代并删除作为其他元素子集的元素
- 从向量生成元素的子集(无嵌套循环)
- C++:从只能通过迭代器访问的数据类型中高效地提取元素子集
- 使用0来显示不存在元素的集合的子集
- 如何将一个集合分成K个子集,使得子集中元素的和最小
- 获取受不存在元素限制的QSet子集
- 从__m256中选择元素子集