在接受采访时提到了这个问题。
你有一个小整数数组。你必须倍增所有这些。你不必担心溢出,你有足够的支持。你能做些什么来加速机器上的乘法运算?
在这种情况下,多次添加会更好吗?
我建议使用分而治之的方法进行乘法,但面试官并没有留下深刻的印象。什么是最好的解决方案?
答案 0 :(得分:4)
以下是一些想法:
使用多线程进行分治:将输入拆分为n个不同的大小为b的块,并递归地将每个块中的所有数字相乘。然后,递归地将所有n / b块再次相乘。如果你有多个内核并且可以并行运行部分内核,那么你可以节省大量的时间。
字级并行:我们假设你的数字都是从上面被一些数字U限制,这恰好是2的幂。现在,假设您想要将a,b,c和d相乘。通过计算开始(4U 2 a + b)×(4U 2 c + d)= 16U 4 ac + 4U 2 ad + 4U 2 bc + bd。现在,请注意这个表达式mod U 2 只是bd。 (由于bd< U 2 ,我们不需要担心mod U 2 步骤弄乱了它)。这意味着,如果我们计算这个产品并采用mod U 2 ,我们就会回到bd。由于U 2 是2的幂,因此可以使用位掩码来完成。
接下来,请注意
4U 2 ad + 4U 2 bc + bd< 4U 4 + 4U 4 + U 2 < 9U 4 < 16U 4
这意味着如果我们将整个表达式除以16U 4 并向下舍入,我们最终会收回广告。这种除法可以通过比特移位完成,因为16U 4 是2的幂。
因此,通过一次乘法,您可以通过应用后续的bitshift和bitmask来获取ac和bd的值。一旦你有了ac和bd,你可以直接将它们相乘以获得abcd的值。假设位掩码和位移比乘法更快,这会将所需的乘法次数减少33%(这里有两次而不是三次)。
希望这有帮助!
答案 1 :(得分:2)
你的分而治之的建议是一个好的开始。它只需要更多解释来留下深刻印象。
使用快速乘法算法来乘以大数(大整数),乘以类似大小的被乘数比一系列不匹配的大小更有效。
这是Clojure中的一个例子
; Define a vector of 100K random integers between 2 and 100 inclusive
(def xs (vec (repeatedly 100000 #(+ 2 (rand-int 99)))))
; Naive multiplication accumulating linearly through the array
(time (def a1 (apply *' xs)))
"Elapsed time: 7116.960557 msecs"
; Simple Divide and conquer algorithm
(defn d-c [v]
(let [m (quot (count v) 2)]
(if (< m 3)
(reduce *' v)
(*' (d-c (subvec v 0 m)) (d-c (subvec v m))))))
; Takes less than 1/10th the time.
(time (def a2 (d-c xs)))
"Elapsed time: 600.934268 msecs"
(= a1 a2) ;=> true (same result)
请注意,此改进不依赖于数组中整数大小的设置限制(100个任意选择并演示下一个算法),但只是它们的大小相似。这是一个非常简单划分的征服。随着数字变得越来越大且成本越来越高,将更多时间花在按类似大小迭代分组上是有意义的。在这里,我依赖于随机分布和大小将保持相似的机会,但即使在最坏的情况下,它仍然会比天真的方法明显更好。
正如Evgeny Kluev在评论中所建议的那样,对于大个小整数,会有很多重复,因此有效的取幂也更好比天真的乘法。这取决于相对参数而不是分而治之,即数量必须足够小,相对于计数足够重复累积而烦恼,但肯定对这些参数表现良好(范围2-中的100K数字) 100)。
; Hopefully an efficient implementation
(defn pow [x n] (.pow (biginteger x) ^Integer n))
; Perform pow on duplications based on frequencies
(defn exp-reduce [v] (reduce *' (map (partial apply pow) (frequencies v))))
(time (def a3 (exp-reduce xs)))
"Elapsed time: 650.211789 msecs"
请注意,在这个试验中,非常简单的分而治之,只是表现得更好,但如果预计会有更少的重复,那就更好了。
当然我们也可以将两者结合起来:
(defn exp-d-c [v] (d-c (mapv (partial apply pow) (frequencies v))))
(time (def a4 (exp-d-c xs)))
"Elapsed time: 494.394641 msecs"
(= a1 a2 a3 a4) ;=> true (all equal)
注意有更好的方法来组合这两个,因为取幂步骤的结果将导致不同大小的被乘数。增加复杂度的值取决于输入中预期数量的不同数字。在这种情况下,很少有不同的数字,因此增加很多复杂性是不会付出代价的。
另请注意,如果有多个内核可以轻松并行化这两种内核。
答案 2 :(得分:1)
如果许多小整数出现多次,您可以从计算每个唯一整数开始。如果c(n)
是整数n
的出现次数,则可以将产品计算为
P = 2 ^ c(2) * 3 ^ c(3) * 4 ^ c(4) * ...
对于取幂步骤,你可以通过平方来使用取幂,这可以大大减少乘法次数。
答案 3 :(得分:1)
如果数字的数量与范围相比确实很大,那么我们已经看到了两种渐近解决方案,可以大大降低复杂度。一个是基于连续平方来计算每个数字c的O(log k)时间中的c ^ k,如果最大数字是C则给出O(C mean(log k))时间,并且k给出每个数字之间的指数1如果每个数字出现的次数相等,则均值(log k)项最大化,因此如果你有N个数,则复杂度变为O(C log(N / C)),这非常依赖于N.基本上只是O(C),其中C指定数字的范围。
我们看到的另一种方法是按照出现的次数对数字进行排序,并跟踪前导数字的乘积(从所有数字开始)并将其提高到一个幂,以便从中删除最不频繁的数字。数组,然后更新数组中剩余元素的指数并重复。如果所有数字出现相同的次数K,则得到O(C + log K),这是对O的改进(C log K)。但是,假设第k个数字出现2 ^ k次。然后,如果C> 1,这仍将给出O(C ^ 2 + C log(N / C))时间,其在技术上比先前的方法O(C log(N / C))更差。日志(N / C)。因此,如果您没有关于每个数字的出现均匀分布的良好信息,您应该采用第一种方法,只需通过使用连续平方,获取产品中出现的每个不同数字的适当功率,并取结果的产物。总时间O(C log(N / C))如果有C个不同的数字和N个总数。
答案 4 :(得分:0)
我有一个解决方案。让我们与其他解决方案讨论。
问题的关键部分是如何减少乘法次数。整数很小但设置很大。
我的解决方案:
让我用一个例子来说明我们需要做多少乘法:假设 我们有5个数字[1,2,3,4,5]。数字1出现100次,数字 2出现150次,3次出现200次,4次出现300次 时间,数字5出现400次。
方法1:直接乘以或使用分而治之 我们需要100 + 150 + 200 + 300 + 400-1 = 1149乘以得到结果。
方法2:我们做(1 ^ 100)(2 ^ 150)(3 ^ 200)(4 ^ 300)(5 ^ 400) 强> (100-1)+(150-1)+(200-1)+(300-1)+(400-1)+4 = 1149. [与方法1相同] 因为n ^ m将以m-1的形式乘以契约。另外,你需要时间来浏览所有数字,但这个时间很短。
这篇文章中的方法: 首先,您需要时间来浏览所有数字。与乘法时间相比,它可以被丢弃。
您正在进行的实际计数是: ((2 * 3 * 4 * 5)^ 150)*((3 * 4 * 5)^ 50)*((4 * 5)^ 100)*(5 ^ 100)
然后你需要做3 + 149 + 2 + 49 + 1 + 99 + 99 + 3 = 405倍