【问题标题】:Stable merging two arrays to maximize product of adjacent elements稳定合并两个数组以最大化相邻元素的乘积
【发布时间】:2012-07-25 19:36:55
【问题描述】:

以下是一个我无法以低于指数复杂度的复杂度回答的面试问题。虽然这似乎是一个 DP 问题,但我无法形成基本案例并正确分析它。任何帮助表示赞赏。

给你 2 个大小为“n”的数组。你需要稳定合并 这些数组使得在新数组中连续乘积的总和 元素被最大化。

例如

A= { 2, 1, 5}

B= { 3, 7, 9}

稳定合并 A = {a1, a2, a3} 和 B = {b1, b2, b3} 将创建一个包含 2*n 个元素的数组 C。例如,通过合并(稳定)A 和 B 说 C = { b1, a1, a2, a3, b2, b3 }。那么总和 = b1*a1 + a2*a3 + b2*b3 应该是最大值。

【问题讨论】:

标签: algorithm data-structures dynamic-programming


【解决方案1】:

对于 A = {a1,a2,...,an},B = {b1,b2,...,bn},

将 DP[i,j] 定义为 {ai,...,an} 和 {bj,...,bn} 之间的最大稳定合并和。

(1

DP[n+1,n+1] = 0, DP[n+1,k] = bk*bk+1 +...+ bn-1*bn, DP[k,n+1] = ak*ak+1 +...+ an-1*an.

DP[n,k] = max{an*bk + bk+1*bk+2 +..+ bn-1*bn, DP[n,k+2] + bk*bk+1}

DP[k,n] = max{ak*bn + ak+1*ak+2 +..+ an-1*an, DP[k+2,n] + ak*ak+1}

DP[i,j] = 最大{DP[i+2,j] + ai*ai+1, DP[i,j+2] + bi*bi+1, DP[i+1,j+ 1] + ai*bi}.

然后你返回 DP[1,1]。

说明: 在每个步骤中,您必须考虑 3 个选项:从剩余的 A 中获取前 2 个元素,从剩余的 B 中获取前 2 个元素,或者从 A 和 B 中同时获取(由于您无法更改 A 和 B 的顺序,因此您将拥有从 A 取第一个,从 B 取第一个)。

【讨论】:

  • 我看不出您的算法如何不考虑一组中两次或多次相同的元素。
  • 看这里——DP[i+1,j+1] + ai*bi,一个来自A,一个来自B
  • 对不起,我不是这个意思。你如何确保在你的算法中你没有以不同的总和形式两次取 A 或 B 的一个元素?我想不通。
  • 如果我决定采用一个元素,我会增加索引 - 所以我不能再次采用相同的元素。例如,如果我选择从 A 中取出 2 个元素并且在这一步不使用 B,则 DP[i+2,j] 将返回。所以我们没有机会再拿 ai 或 a+1 了。
  • 好吧,这让我信服:)。顺便说一下,为这个漂亮的解决方案 +1!
【解决方案2】:

我的解决方案相当简单。我只是探索所有可能的稳定合并。跟随工作的 C++ 程序:

#include<iostream>

using namespace std;

void find_max_sum(int *arr1, int len1, int *arr2, int len2, int sum, int& max_sum){
  if(len1 >= 2)
    find_max_sum(arr1+2, len1-2, arr2, len2, sum+(arr1[0]*arr1[1]), max_sum);
  if(len1 >= 1 && len2 >= 1)
    find_max_sum(arr1+1, len1-1, arr2+1, len2-1, sum+(arr1[0]*arr2[0]), max_sum);
  if(len2 >= 2)
    find_max_sum(arr1, len1, arr2+2, len2-2, sum+(arr2[0]*arr2[1]), max_sum);
  if(len1 == 0 && len2 == 0 && sum > max_sum)
    max_sum = sum;
}

int main(){
  int arr1[3] = {2,1,3};
  int arr2[3] = {3,7,9};
  int max_sum=0;
  find_max_sum(arr1, 3, arr2, 3, 0, max_sum);
  cout<<max_sum<<endl;
  return 0;
}

【讨论】:

  • 上面的解决方案很简单。您可以通过将子树创建为 {a1[0],a1[1]}、{a1[0],a2[0]} 和 {a2[0],a2[1]} 来可视化它。现在根据数组中剩余的元素用可能的子节点(最多 3 个)扩展每个节点。
  • 这是指数级的复杂性!甚至不会返回像 70 个元素这样的数组大小/
【解决方案3】:

我认为如果您提供更多的测试用例会更好。但我认为两个数组的正常合并类似于合并排序中的合并将解决问题。

合并数组的伪代码见Wiki

基本上是普通的merging algorithm used in Merge Sort。在 合并排序,数组已排序,但在这里我们应用相同的合并 未排序数组的算法。

Step 0:设ifirst array(A) 的索引,jindex for second array(B)。 i=0 , j=0

Step 1Compare A[i]=2 &amp; B[j]=3。由于2&lt;3,它将成为新merged array(C) 的第一个元素。 i=1, j=0(将该数字添加到较小的新数组中)

Step 2:又是Compare A[i]=1 and B[j]=3. 1&lt;3,因此insert 1 in C. i++, j=0;

Step 3:又是Compare A[i]=3 and B[j]=3Any number can go in C(both are same). i++, j=0;(基本上我们正在增加插入数字的那个数组的索引)

Step 4:既然array A is complete就直接insert the elements of Array B in C。否则重复前面的步骤。

Array C = { 2, 1, 3, 3, 7,9}

我没有做太多的研究。所以如果有任何可能失败的测试用例,请提供一个。

【讨论】:

  • 为什么只进行归并排序?如果排序在 stable 合并方面没有意义,那么任何算法都可以解决问题。在这里查看我的答案:stackoverflow.com/a/11709421/538514
  • 我没有说我正在使用归并排序。我没有做任何排序。只需使用合并排序中使用的合并算法。您的解决方案是错误的,因为我们必须进行稳定的合并(en.wikipedia.org/wiki/Sorting_algorithm#Stability
  • @Android 也许我理解错了,但我认为当最佳解决方案是{2, 1, 9, 7, 3, 3} 时,将此算法应用于{2, 1, 9}, {7, 3, 3} 会产生{2, 1, 7, 3, 3, 9}
  • 试试 [10, 0, 100] 和 [1,2,3],用你的技术你会得到 [1, 2, 3, 10, 0, 100],所以答案是1*2+2*3+3*10+10*0+0*100,但我们应该有一个解 > 100*3 = 300
【解决方案4】:

F(i, j) 定义为通过稳定合并Ai...AnBj...Bn 可以获得的最大成对和。

在合并的每一步,我们可以选择以下三个选项之一:

  1. A 的前两个剩余元素。
  2. A 的第一个剩余元素和B 的第一个剩余元素。
  3. B 的前两个剩余元素。

因此,F(i, j) 可以递归定义为:

F(n, n) = 0
F(i, j) = max
(
    AiAi+1 + F(i+2, j), //Option 1
    AiBj + F(i+1, j+1), //Option 2
    BjBj+1 + F(i, j+2)  //Option 3
)

为了找到两个列表的最佳合并,我们需要找到F(0, 0),天真,这将涉及多次计算中间值,但是通过缓存每个找到的F(i, j),复杂度降低到@ 987654333@.

这里有一些快速而肮脏的 c++ 可以做到这一点:

#include <iostream>

#define INVALID -1

int max(int p, int q, int r)
{
    return p >= q && p >= r ? p : q >= r ? q : r;
}

int F(int i, int j, int * a, int * b, int len, int * cache)
{
    if (cache[i * (len + 1) + j] != INVALID)    
        return cache[i * (len + 1) + j];

    int p = 0, q = 0, r = 0;

    if (i < len && j < len)
        p = a[i] * b[j] + F(i + 1, j + 1, a, b, len, cache);

    if (i + 1 < len)
        q = a[i] * a[i + 1] + F(i + 2, j, a, b, len, cache);

    if (j + 1 < len)
        r = b[j] * b[j + 1] + F(i, j + 2, a, b, len, cache);

    return cache[i * (len + 1) + j] = max(p, q, r);
}

int main(int argc, char ** argv)
{
    int a[] = {2, 1, 3};
    int b[] = {3, 7, 9};
    int len = 3;

    int cache[(len + 1) * (len + 1)];
    for (int i = 0; i < (len + 1) * (len + 1); i++)
        cache[i] = INVALID;

    cache[(len + 1) * (len + 1)  - 1] = 0;

    std::cout << F(0, 0, a, b, len, cache) << std::endl;
}

如果您需要实际的合并序列而不仅仅是总和,您还必须缓存 p, q, r 中的哪一个被选中并回溯。

【讨论】:

【解决方案5】:

通过动态规划解决它的一种方法是始终存储:

S[ i ][ j ][ l ] = "合并 A[1,...,i] 和 B[1,...,j] 的最佳方法,如果 l == 0,则最后一个元素是A[i],如果l == 1,最后一个元素是B[j]"。

那么,DP就是(伪代码,在A[0]和B[0]处插入任意数字,让实际输入在A[1]...A[n],B[1] ]...B[n]):

S[0][0][0] = S[0][0][1] = S[1][0][0] = S[0][1][1] = 0; // If there is only 0 or 1 element at the merged vector, the answer is 0
S[1][0][1] = S[0][1][1] = -infinity; // These two cases are impossible
for i = 1...n:
    for j = 1...n:
        // Note that the cases involving A[0] or B[0] are correctly handled by "-infinity"
        // First consider the case when the last element is A[i]
        S[i][j][0] = max(S[i-1][j][0] + A[i-1]*A[i], // The second to last is A[i-1].
                         S[i-1][j][1] + B[j]*A[i]); // The second to last is B[j]
        // Similarly consider when the last element is B[j]
        S[i][j][1] = max(S[i][j-1][0] + A[i]*B[j], // The second to last is A[i]
                         S[i][j-1][1] + B[j-1]*B[j]); // The second to last is B[j-1]
 // The answer is the best way to merge all elements of A and B, leaving either A[n] or B[n] at the end.
return max(S[n][n][0], S[n][n][1]);

【讨论】:

    【解决方案6】:

    合并并排序。可能是归并排序。排序后的数组给出最大值。(合并只是附加数组)。复杂度为 nlogn。

    【讨论】:

    • 虽然我不怀疑排序数组 确实 最大化产品的总和,但 OP 需要它以保留原始数组中元素的顺序。因此,使用该示例,{1, 2, 3, 3, 7, 9} 是不可接受的,因为 2 需要保持在 1 的前面。
    【解决方案7】:

    这是 Clojure 中的一个解决方案,如果您对一些不走寻常路的东西感兴趣的话。它是 O(n3),因为它只生成所有 n2 个稳定合并并花费 n 时间对产品求和。与我见过的基于数组的命令式解决方案相比,偏移量和算术的混乱要少得多,这有望使算法更加突出。而且它也非常灵活:例如,如果您想包含 c2*c3 以及 c1*c2 和 c3*c4,您可以简单地将 (partition 2 coll) 替换为 (partition 2 1 coll)

    ;; return a list of all possible ways to stably merge the two input collections
    (defn stable-merges [xs ys]
      (lazy-seq
       (cond (empty? xs) [ys]
             (empty? ys) [xs]
             :else (concat (let [[x & xs] xs]
                             (for [merge (stable-merges xs ys)]
                               (cons x merge)))
                           (let [[y & ys] ys]
                             (for [merge (stable-merges xs ys)]
                               (cons y merge)))))))
    
    ;; split up into chunks of two, multiply, and add the results
    (defn sum-of-products [coll]
      (apply + (for [[a b] (partition 2 coll)]
                 (* a b))))
    
    ;; try all the merges, find the one with the biggest sum
    (defn best-merge [xs ys]
      (apply max-key sum-of-products (stable-merges xs ys)))
    
    user> (best-merge [2 1 5] [3 7 9])
    (2 1 3 5 7 9)
    

    【讨论】:

    • 这里如何防止子问题的重新计算?
    • 不是。我只是把比 OP 声称的“指数复杂性”更好的东西放在一起。 TBH 很难想象比 n^3 慢,因为这就是蛮力解决方案的速度。
    【解决方案8】:

    让我们将 c[i,j] 定义为相同问题的解决方案,但数组从 i 开始到左侧结束。和 j 以右结束。 因此 c[0,0] 将解决原始问题。

    c[i,j] 由。

    1. MaxValue = 最大值。
    2. NeedsPairing = true 或 false = 取决于最左边的元素未配对。
    3. Child = [p,q] 或 NULL = 定义子键,最终达到此级别的最佳总和。

    现在为这个 DP 定义最优子结构

    c[i,j] = if(NeedsPairing) { left[i]*right[j] } + Max { c[i+1, j], c[i, j+1] }
    

    在这段代码中更详细地捕获了它。

    if (lstart == lend)
    {
        if (rstart == rend)
        {
            nodeResult = new NodeData() { Max = 0, Child = null, NeedsPairing = false };
        }
        else
        {
            nodeResult = new NodeData()
            {
                Max = ComputeMax(right, rstart),
                NeedsPairing = (rend - rstart) % 2 != 0,
                Child = null
            };
        }
    }
    else
    {
        if (rstart == rend)
        {
            nodeResult = new NodeData()
            {
                Max = ComputeMax(left, lstart),
                NeedsPairing = (lend - lstart) % 2 != 0,
                Child = null
            };
        }
        else
        {
            var downLef = Solve(left, lstart + 1, right, rstart);
    
            var lefResNode = new NodeData()
            {
                Child = Tuple.Create(lstart + 1, rstart),
            };
    
            if (downLef.NeedsPairing)
            {
                lefResNode.Max = downLef.Max + left[lstart] * right[rstart];
                lefResNode.NeedsPairing = false;
            }
            else
            {
                lefResNode.Max = downLef.Max;
                lefResNode.NeedsPairing = true;
            }
    
            var downRt = Solve(left, lstart, right, rstart + 1);
    
            var rtResNode = new NodeData()
            {
                Child = Tuple.Create(lstart, rstart + 1),
            };
    
            if (downRt.NeedsPairing)
            {
                rtResNode.Max = downRt.Max + right[rstart] * left[lstart];
                rtResNode.NeedsPairing = false;
            }
            else
            {
                rtResNode.Max = downRt.Max;
                rtResNode.NeedsPairing = true;
            }
    
            if (lefResNode.Max > rtResNode.Max)
            {
                nodeResult = lefResNode;
            }
            else
            {
                nodeResult = rtResNode;
            }
        }
    }
    

    我们使用记忆来防止再次解决子问题。

    Dictionary<Tuple<int, int>, NodeData> memoization = new Dictionary<Tuple<int, int>, NodeData>();
    

    最后我们使用 NodeData.Child 来追溯路径。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-03-01
      • 1970-01-01
      • 2017-02-02
      • 1970-01-01
      • 2021-05-09
      • 1970-01-01
      • 2021-11-23
      • 1970-01-01
      相关资源
      最近更新 更多