【问题标题】:Dynamic algorithm to find maximum sum of products of "accessible" numbers in an array查找数组中“可访问”数字乘积的最大和的动态算法
【发布时间】:2016-02-14 08:42:29
【问题描述】:

我被要求提供一个 动态 算法,该算法将采用偶数(正数和负数)的序列并执行以下操作:

每个“转”两个数字被选择相乘。该算法只能访问序列的任一端。但是,如果选择的第一个数字是最左边的数字,则 第二个数字可以是最右边的数字,也可以是新的最左边的数字(因为旧的最左边的数字已经“删除/选择”),反之亦然。该程序的目标是找出每轮选择的两个数字的乘积的最大总和

示例:

序列:{ 10, 4, 20, -5, 0, 7 }

最佳结果:7*10 + 0*-5 + 4*20 = 150

我的进步:

我一直在尝试找到一种动态方法,但运气不佳。我已经能够推断出该程序基本上只允许每次将结束数字乘以“相邻”数字,并且目标是将最小可能的数字乘以最小可能的数字(导致双负乘法 - 一个正数,或可达到的最小最小数),并且每次都继续应用此规则直到完成。相比之下,这条规则也适用于相反的方向——每次将最大可能数字乘以最大可能数字。也许最好的方法是同时应用这两种方法?我不确定,正如我所提到的,我在为这个问题实施算法时运气不佳。

【问题讨论】:

  • 编写一个递归算法来检查每一种可能性非常容易;但是,这将多次检查相同的序列:在您的示例中,有三种不同的方式可以到达最后一步 {20,-5}。为避免计算三次,您应先计算短子数组的结果,存储结果,然后使用它来求解数组的较大部分。
  • 对于 20 个数字的数组,简单的递归算法需要 29,524 次递归,而从小部分到大部分的算法(另见 dfb 的答案)只需要计算 100 个案例(19 + 17 + ... + 1)。

标签: algorithm recursion dynamic numbers divide-and-conquer


【解决方案1】:

让我们看看递归和自下而上的表格方法。首先是递归:

{10, 4,20,-5, 0, 7}

First call:

  f(0,5) = max(f(0,3)+0*7, f(2,5)+10*4, f(1,4)+10*7)

Let's follow one thread:

  f(1,4) = max(f(1,2)+(-5)*0, f(3,4)+4*20, f(2,3)+4*0)

f(1,2)f(3,4)f(2,3) 是“基本情况”并且有直接的解决方案。该函数现在可以将这些保存在由i,j 索引的表中,以便稍后由递归的其他线程访问。例如,f(2,5) = max(f(2,3)+0*7... 也需要f(2,3) 的值,并且如果该值已经在表中,则可以避免创建另一个函数调用。随着递归函数调用的返回,函数可以将f(1,4)f(2,5)f(0,3) 的下一个值保存在表中。由于此示例中的数组很短,因此函数调用的减少并不那么显着,但对于较长的数组,重叠函数调用的数量(对相同的i,j)可能会大得多,这就是为什么 memoization 可以证明更多高效。

表格方法是我试图在其他答案中展开的方法。在这里,我们依靠(在这种情况下)类似的数学公式来计算表中的下一个值,而不是递归,而是依赖于表中已经计算的其他值。数组下的星号用于说明我们计算值的顺序(使用两个嵌套的for 循环)。您可以看到,为每个 (i,j) 大小的子集计算公式所需的值要么是基本情况,要么存在于循环顺序的前面;它们是:一个子集向左扩展了两个元素,一个子集向右扩展了两个元素,一个子集向每一侧扩展了一个元素。

【讨论】:

  • 感谢您的深入解释!
【解决方案2】:

您可能正在寻找一种动态规划算法。让A 是数字数组,这个问题的重复将是

f(start,stop) = max( // last two numbers multiplied + the rest of sequence,
                     // first two numbers multiplied + the rest of sequence,
                     // first number*last number + rest of sequence  )

f(start,stop) 然后是从 start,stop 开始的数组子序列的最佳结果。您应该使用动态编程或记忆化计算所有有效值的 f(start,stop)。

提示:第一部分 // last two numbers multiplied + the rest of sequence 看起来像:

 f(start,stop-2) + A[stop-1]*A[stop-2]

【讨论】:

  • 最后一行不应该是f(start,stop-2)吗?
  • 哦,好吧,有道理!顺便说一句,你知道每次存储乘法的便捷方法,这样我就不会计算两次相同的乘法了吗?我想不出一个好的方法..
  • @user3495690 如果你愿意,我可以发布我的 javascript 代码,但你应该能够用你现在的答案弄清楚。
  • @m69 我想我明白了,我只是不确定如何存储要相乘的值以避免多次计算乘法并使其成为 动态 算法。你能给出一个好的方法来完成这个吗?
  • @user3495690 有很多选项,其中最明显的是求解每个长度为 2 的子数组并将结果存储在一个数组中,然后使用求解每个长度为 4 的子数组存储的结果,然后求解每个长度为 6 的子数组,依此类推...有关这是否相当于动态规划和/或记忆和/或制表的讨论,请参阅stackoverflow.com/questions/6184869/…
【解决方案3】:

ij 代表上一轮之后数组A 的第一个和最后一个索引。显然,它们必须代表A 的某个大小相等的连续子集。那么dp[i][j] 的一般情况应该是max(left, right, both),其中left = A[i-2]*A[i-1] + dp[i-2][j]right = A[j+1]*A[j+2] + dp[i][j+2]both = A[i-1]*A[j+1] + dp[i-1][j+1];解决方案是max(A[i]*A[i+1] + dp[i][i+1]),除最后一个之外的所有i

幸运的是,我们可以按降序计算 dp,这样总是代表较大的周围子集的所需值已经计算出来(星号代表计算的子集):

{10, 4,20,-5, 0, 7}
  *  *  *  *  *  *
  *  *  *  *
  *  *
     *  *  *  *    (70)
     *  *
        *  *  *  *
        *  *
           *  *    left = (80 + 70)
              *  *

【讨论】:

    【解决方案4】:

    下面是递归方法的代码sn-p。

      public class TestClass {
    
      public static void main(String[] args) {
        int[] arr = {10, 4, 20, -5, 0, 7};
        System.out.println(findMaximumSum(arr, 0, arr.length - 1));
      }
    
      private static int findMaximumSum(int[] arr, int start, int end) {
        if (end - start == 1)
            return arr[start] * arr[end];
    
        return findMaximum(
            findMaximumSum(arr, start + 2, end) + (arr[start] * arr[start + 1]), 
            findMaximumSum(arr, start + 1, end - 1) + (arr[start] * arr[end]),
            findMaximumSum(arr, start, end - 2)+ (arr[end] * arr[end - 1])
            );
      }
    
      private static int findMaximum(int x, int y, int z) {
        return Math.max(Math.max(x, y), z);
      }
    }
    

    结果是 10*4 + 20*7 + -5*0 = 180

    同样对于输入 {3,9,7,1,8,2},答案是 3*2 + 9*8 + 7*1 = 85

    【讨论】:

    • 这使用了不正确的启发式方法,即任何时候三个选项之间的最优选择也将是长期的最优选择。考虑[3,9,7,1,8,2];您的算法将得到 3*9 + 8*2 + 7*1 = 50,而最佳答案是 3*2 + 9*8 + 7*1 = 85。
    • @m69 感谢您指出错误。更正了解决方案。
    • 作为一个练习,我用我能想到的所有方法(枚举、简单递归、制表、记忆、启发式)解决了这个问题,到目前为止,我还没有找到一种方法不能尝试所有方法可能性并返回正确的结果。基于 4、6、8... 外部数字做出决定的启发式算法永远无法为 [-1,1,1,1...-99...1,1, 1,-1] 就是保存一个-1来翻转中间的-99。
    【解决方案5】:

    让我们把它变成一个甜蜜的动态规划公式。

    我们将子问题定义如下:

    • 我们希望最大化总和,同时选择子数组 i、j 的前两个、第一个和最后一个或最后两个值。

    那么recurrence方程如下所示:

    set OPT(i,j) =
        if i == j
            v[i]
        else if i < j:
            max (
                v[i] + v[i+1] + OPT(i + 2,j),
                v[i] + v[j]   + OPT(i + 1,j + 1),
                v[j] + v[j-1] + OPT(i, j - 2)
            )
        else:
            0
    

    拓扑顺序:i 和 j 中的一个或两个都变小了。

    现在基本情况出现在 i 等于 j 时,返回值。

    回到原始问题,调用 OPT(0,n-1) 返回最大和。

    时间复杂度为 O(n^2)。由于我们使用动态编程,它使我们能够缓存所有值。每个子问题调用,我们最多使用 O(n) 次,并且我们这样做 O(n) 次。

    【讨论】:

      猜你喜欢
      • 2013-12-31
      • 2020-08-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-03-09
      • 2017-03-01
      • 1970-01-01
      相关资源
      最近更新 更多