在给定整数列表的元素之间放置“sum”和“multiply”运算符,以便表达式产生指定的值
Posted
技术标签:
【中文标题】在给定整数列表的元素之间放置“sum”和“multiply”运算符,以便表达式产生指定的值【英文标题】:Place "sum" and "multiply" operators between the elements of a given list of integers so that the expression results in a specified value 【发布时间】:2015-01-08 07:40:21 【问题描述】:我收到了一个棘手的问题。 鉴于: A = [a1,a2,...an](长度为“n”的正整数列表) r(正整数)
查找 *, + 运算符列表 O = [o1,o2,...on-1] 因此,如果我们将这些运算符放在“A”的元素之间,则结果表达式将计算为“r”。只需要一种解决方案。
例如,如果 A = [1,2,3,4] r = 14 然后 O = [*, +, *]
我已经实现了一个简单的递归解决方案,并进行了一些优化,但当然它是指数 O(2^n) 时间,所以对于长度为 40 的输入,它可以工作很长时间。
我想问一下你们中是否有人知道这个的次指数解决方案?
更新 A的元素在0-10000之间, r可以任意大
【问题讨论】:
我当然不记得有任何算法可以做到这一点,但我相当肯定,如果这是一个现实世界的问题,启发式算法会非常有用。我可以想到一些方法,但正如启发式算法倾向于做的那样,它只会对可能输入的子集有帮助,当然仍然会有无限的输入集保持 O(2^n)跨度> 一个简单的启发式方法:+
和 *
(在正整数上)都不能产生小于其较大参数的值。因此,如果您生成的中间结果大于r
,您可以中断搜索。您的问题没有明确说明如何对运算符进行分组:如果乘法优先,则可能有更好的解决方案。
r
可以有多大?
给定的 A 和 r 是否一定存在解决方案?
n
、r
和 a[i]
值的近似(或更好、更精确)约束是什么?
【参考方案1】:
令 A 和 B 为正整数。那么 A + B ≤ A × B + 1。
这个小事实可以用来构造一个非常有效的算法。
让我们定义一个图表。图节点对应于操作列表,例如[+, ×, +, +, ×]。如果 Y 可以通过将 X 中的单个 + 更改为 × 来获得,则存在从图节点 X 到图节点 Y 的边。图在对应于 [+, +, ..., +] 的节点处有源.
现在从源节点执行广度优先搜索,边走边构建图。例如,当扩展节点 [+, ×, +, +, ×] 时,您(可选地构造 then)连接到节点 [×, ×, +, +, ×], [+, ×, ×, +, ×] 和 [+, ×, +, ×, ×]。如果计算结果大于 r + k(O),则不要展开到节点,其中 k(O) 是操作列表 O 中 + 的数量。这是因为“+ 1”在答案开头的事实 - 考虑 a = [1, 1, 1, 1, 1], r = 1 的情况。
这种方法使用 O(n 2n) 时间和 O(2n) 空间(两者都可能是非常松散的最坏情况界限)。这仍然是一个指数算法,但是我认为您会发现它对于非险恶输入非常合理。 (我怀疑这个问题是 NP 完全的,这就是我对这个“非险恶输入”转义子句感到满意的原因。)
【讨论】:
当然,这个“事实”只有在 A 和 B 都大于 1 时才成立。否则 1 + B > 1 × B。 @IlmariKaronen 我完全错过了。这个想法应该仍然有效,我只需要稍微修改一下...... 所以如果我理解的很好,你是从“最小可能”的运算符组合开始,然后尝试在越来越大的组合中达到结果吗?我最初的做法是从列表的第一个元素开始,从左到右递归构建运算符列表,收集产品和总和,并即时计算子结果(如果子结果回退不行)。这是 O(2^n) 时间和 O(n) 空间,所以在最坏的情况下它仍然更好。你说你的解决方案在一般情况下会表现得更好? @aalmos 我这里的方法基本上是branch and bound 的一种形式。这个想法是,只要你开始乘以几个整数 >= 2,你就会得到指数级的大值。因此,尽管在某种意义上有 2^n 个候选解决方案需要考虑,但您确实可以探索一个看起来更像 nCp 大小的小得多的空间,您可以将 p 视为一个小常数。 @TimothyShields 这听起来很合理,绝对值得一提。我正在考虑是否有一些代数魔法可以将问题分解为较小的重叠子问题,以便我可以做一些 DP。【参考方案2】:这是一个O(rn^2)-时间,O(rn)-空间 DP 方法。如果 r pseudo-polynomial time,因为它所花费的时间与其部分输入 (r) 的 值 成正比,而不是其大小(即 log2(r))。具体来说,它需要 rn bits 内存,因此它应该在几秒钟内给出答案,最多可达 rn
关键的观察是 任何涉及所有 n 个数字的公式都有一个由 i 个因子组成的最终项,其中 1 也就是说,任何公式必须是在以下 n 种情况之一:
(前n-1项的公式)+ a[n] (前n-2项的公式)+ a[n-1] * a[n] (前n-3项的公式)+ a[n-2] * a[n-1] * a[n] ... a[1] * a[2] * ... * a[n]让我们称由前 i 个数字组成的 a[] 的“前缀”为 P[i]。如果我们记录,对于每个 0 在 P[i] 上通过某个公式可以达到的完整值集 ,那么基于上述,我们可以很容易地计算出 P[n] 可以达到的完整值 还,但它可以用来得到答案。)
X[1][a[1]] = 真。 X[1][j] = 所有其他 j 的假。对于任何 2 计算 X[i][j]
X[i][j] = X[i - 1][j - a[i]] ||
X[i - 2][j - a[i-1]*a[i]] ||
X[i - 3][j - a[i-2]*a[i-1]*a[i]] ||
... ||
X[1][j - a[2]*a[3]*...*a[i]] ||
(a[1]*a[2]*...*a[i] == j)
请注意,最后一行是一个等式检验,它将 P[i] 中所有 i 数字的乘积与 j 进行比较,并返回真或假。 X[i][j] 的表达式中有 i
重构解决方案
如果 X[n][r] 为真,则问题有解(满足公式),我们可以通过追溯 DP 表在 O(n^2) 时间内重构一个,从 X[ 开始n][r],在每个位置查找使当前位置能够假定值“真”的任何术语 - 即任何真正的术语。 (我们可以通过为每个 (i, j) 组合存储一个以上的位来更快地完成这个重建步骤——但由于 r 被允许“任意大”,并且这种更快的重建不会提高整体时间复杂度,它使用每个 DP 表条目使用最少位的方法可能更有意义。)所有令人满意的解决方案都可以通过这种方式重构,通过回溯所有真正的术语而不是仅仅选择任何一个 --但它们的数量可能是指数级的。
加速
有两种方法可以加快单个 X[i][j] 值的计算速度。首先,因为所有术语都与||
结合在一起,所以我们可以在结果为真时立即停止,因为后面的术语不能再次使其为假。其次,如果 i 左边的任何地方都没有零,我们可以在最终数字的乘积大于 r 时立即停止,因为该乘积无法再次减小。
当 a[] 中没有零时,第二次优化在实践中可能非常重要:它有可能使内部循环比完整的 i-1 迭代小得多。事实上,如果 a[] 不包含零,并且它的平均值是 v,那么在为特定 X[i][j] 值计算 k 项之后,乘积将在 v^k 左右——所以平均而言,所需的内循环迭代次数(项)从 n 下降到 log_v(r) = log(r)/log(v)。这可能比 n 小得多,在这种情况下,此模型的 平均 时间复杂度降至 O(rn*log(r)/log(v))。
[编辑:我们实际上可以通过以下优化保存乘法:)]
8/32/64 一次 X[i][j]s: X[i][j] 独立于 X[i][k],因为 k != j,因此,如果我们使用位集来存储这些值,我们可以使用简单的按位或运算并行计算其中的 8、32 或 64 个(或者可能更多,使用 SSE2 等)。也就是说,我们可以并行计算 X[i][j], X[i][j+1], ..., X[i][j+31] 的第一项,或者将它们变成结果,然后并行计算它们的第二项并将它们进行 OR 等。我们仍然需要以这种方式执行相同数量的减法,但乘积都是相同的,因此我们可以将乘法的数量减少 8/32 倍/64——当然还有内存访问次数。 OTOH,这使得上一段中的第一个优化更难完成——你必须等到整个 8/32/64 位块变为真之后才能停止迭代。
零: a[] 中的零可以让我们提前停止。具体来说,如果我们刚刚计算了一些 i 并且在 a[] 中位置 i 右侧的任何地方都有一个零,然后我们可以停下来:我们已经有一个关于前 i 个数字的公式,计算结果为 r,我们可以使用该零来“杀死”位置 i 右侧的所有数字,方法是创建一个包含所有数字的大乘积项.
个数: 任何包含值 1 的 a[] 条目的一个有趣属性是它可以移动到 a[] 中的任何其他位置,而不会影响是否存在解。这是因为每个令人满意的公式要么在这个 1 的至少一侧有一个*
,在这种情况下,它乘以其他一些项并且在那里没有效果,同样在其他任何地方都没有效果;或者它的两边都有一个+
(想象一下在第一个位置之前和最后一个位置之后有额外的+
符号),在这种情况下,它也可以添加到任何地方。
因此,我们可以在执行其他任何操作之前安全地将所有 1 值分流到 a[] 的末尾。这样做的目的是,现在我们根本不需要评估这些 X[][] 行,因为它们只会以非常简单的方式影响结果。假设 a[] 中有 m 完全由 1 组成,那么至少必须“添加”1——它们不能都乘以其他项。)
【讨论】:
对于冗长而详细的解释和漂亮的 DP 解决方案,我真的很喜欢。虽然不幸的是,在我的确切情况下,我相信它不能应用,因为数字 r 真的非常大(大到我不得不使用 GMP 来处理它 - 我知道我没有在我的问题中提到这一点)。如果你愿意,我可以把测试用例发给你。 @aalmos:啊,那么。仍然有兴趣查看实例。您可以做的一件事是选择一个中等大小的数字 m 使用不同的(和互质的)m 值执行 k > 1 次运行可能(或可能不会)更好:然后您可以使用Xreduced1[i][j mod m1] & Xreduced2[i][j mod m2] & ... & Xreducedk[i][j mod mk]
伪造 Xorig[i][j]。这似乎是一种将“假”值放入 Xorig[i][j] 的更有效方法,因为在该位置有一个“假”只需要一个 m 值——但你必须保留所有 k重建期间周围的位图,因此每个 m 值必须是大小的 1/k 左右。
最后的想法(现在:-P):首先像以前一样将 X[][] 构建到某个最大值 m(比如一百万)。然后启动“正常”蛮力指数时间搜索,从 a[] 的末尾向后 构建公式。假设您当前拥有的公式使用 a[] 的最后 k 个数字并且值为 y;那么如果 r - y
【参考方案3】:
这是另一种可能有用的方法。它有时被称为“中间相遇”算法并在O(n * 2^(n/2))
中运行。基本思路是这样的。假设n = 40
并且您知道中间的插槽是+
。然后,您可以暴力破解每一方的所有N := 2^20
可能性。令A
为长度为N
的数组,存储左侧的可能值,同样让B
为长度为N
的数组,存储右侧的值。
然后,在对A
和B
进行排序之后,不难有效地检查它们中的任何两个是否总和为r
(例如,对于A
中的每个值,对@987654333 进行二分查找@,或者如果两个数组都已排序,您甚至可以在线性时间内完成)。这部分需要O(N * log N) = O(n * 2^(n/2))
时间。
现在,这一切都假设中间的插槽是+
。如果不是,那么它必须是*
,并且您可以将中间的两个元素合二为一(它们的乘积),从而将问题减少到n = 39
。然后你尝试同样的事情,依此类推。如果你仔细分析,你应该得到O(n * 2^(n/2))
作为渐近复杂度,因为实际上最大的项占主导地位。
你需要做一些簿记才能真正恢复 +
和 *
,为了简化解释,我省略了。
【讨论】:
我喜欢这样组合中间两个数字的想法。一个想法:虽然它不会改变整体复杂性,但您可以在 O(2^(n/2)) 时间内为每一侧生成 2^(n/2) 种可能性的列表,而不是 O(n2^(n /2)) 时间通过格雷码顺序的可能性。以上是关于在给定整数列表的元素之间放置“sum”和“multiply”运算符,以便表达式产生指定的值的主要内容,如果未能解决你的问题,请参考以下文章
如何创建一个给定正整数的函数,返回一个向量,该向量的数字是 5 的倍数?