证明没有重叠的子问题?

时间:2018-03-29 20:49:55

标签: algorithm dynamic-programming

我刚收到以下面试问题:

Given a list of float numbers, insert “+”, “-”, “*” or “/” between each consecutive pair of numbers to find the maximum value you can get. For simplicity, assume that all operators are of equal precedence order and evaluation happens from left to right.

Example:
(1, 12, 3) -> 1 + 12 * 3  = 39

如果我们构建了一个递归解决方案,我们会发现我们会得到一个O(4 ^ N)解决方案。我试图找到重叠的子问题(以提高此算法的效率),并且无法找到任何重叠的问题。然后采访者告诉我,没有任何重叠的分数。

我们如何检测何时存在重叠的解决方案?何时没有?我花了很多时间试图“逼迫”子句出现,最终面试官告诉我没有。

我目前的解决方案如下:

def maximumNumber(array, current_value=None):

    if current_value is None:
       current_value = array[0]
       array = array[1:] 

    if len(array) == 0:
       return current_value

    return max(
        maximumNumber(array[1:], current_value * array[0]),
        maximumNumber(array[1:], current_value - array[0]),
        maximumNumber(array[1:], current_value / array[0]),
        maximumNumber(array[1:], current_value + array[0])
    )

3 个答案:

答案 0 :(得分:1)

寻找“重叠的子问题”听起来像是在尝试自下而上的动态编程。在面试中不要理会这一点。写出明显的递归解决方案。然后回忆一下。这是自上而下的方法。工作起来容易得多。

你可能会受到质疑。以上是我最后一次询问此事时的回复。

动态编程有两种方法,自上而下和自下而上。自下而上的方法通常使用更少的内存,但更难写。因此,如果我需要最后一盎司的性能,我会自上而下递归/记忆,并且只采用自下而上的方法。

这是一个完全正确的答案,我被录用了。

现在您可能会注意到有关动态编程的教程会花更多时间在自下而上。他们甚至经常跳过自上而下的方法。他们这样做是因为自下而上更难。你必须以不同的方式思考。它确实提供了更有效的算法,因为您可以丢弃那些您不会再次使用的数据结构部分。

在面试中提出一个有效的解决方案已经很难了。不要让自己比你需要的更难。

编辑以下是面试官认为不存在的DP解决方案。

def find_best (floats):
    current_answers = {floats[0]: ()}
    floats = floats[1:]
    for f in floats:
        next_answers = {}
        for v, path in current_answers.iteritems():
            next_answers[v + f] = (path, '+')
            next_answers[v * f] = (path, '*')
            next_answers[v - f] = (path, '-')
            if 0 != f:
                next_answers[v / f] = (path, '/')
        current_answers = next_answers
    best_val = max(current_answers.keys())
    return (best_val, current_answers[best_val])

答案 1 :(得分:0)

通常,重叠子问题方法是将问题分解为较小的子问题,解决方案在解决大问题时解决的问题。当这些子问题表现出最优的子结构时,DP是解决它的好方法。

关于您使用新号码做什么的决定与您已处理的号码几乎没有关系。当然,除了考虑迹象。

所以我会说这是一个重叠的子问题解决方案,但一个动态编程问题。您可以使用潜水和征服甚至更直接的递归方法。

最初让我们忘记负面花车。

根据以下规则处理每个新的浮动

  1. 如果新浮点数小于1,请在其前面插入/
  2. 如果新浮点数大于1,请在其前插入*
  3. 如果为1,则插入+
  4. 如果你看到零,则不要分或加
  5. 这将解决所有正浮动的问题。

    现在让我们处理投入混合的负数的情况。

    扫描输入一次以确定您有多少负数。

    隔离列表中的所有负数,将绝对值小于1的所有数字转换为乘法逆。然后按大小排序。如果你有偶数个元素我们都很好。如果你有一个奇数个元素将这个列表的头部存储在一个特殊的var中,比如k,并将processed标志与它关联,并将标志设置为False

    继续使用一些更新的规则

    1. 如果您看到负数小于0但大于-1,请在其前插入/除法
    2. 如果您看到负数小于-1,请在其前面插入*
    3. 如果您看到特殊var且processed标记为False,请在其前面插入-。将processed设为True
    4. 还有一个优化你可以执行哪个正在删除负面的巴黎作为我们的初始负数列表中的一揽子减法的候选者,但这只是一个边缘情况,我很确定你的面试官赢了&#39 ; t care
    5. 现在总和只是您要添加的数字的函数,而不是您要添加的总和的函数:)

答案 2 :(得分:0)

计算上一步中每个操作的最大/最小结果。不确定整体正确性。

时间复杂度O(n),空间复杂度O(n)

const max_value = (nums) => {
  const ops = [(a, b) => a+b, (a, b) => a-b, (a, b) => a*b, (a, b) => a/b]

  const dp = Array.from({length: nums.length}, _ => [])
  dp[0] = Array.from({length: ops.length}, _ => [nums[0],nums[0]])

  for (let i = 1; i < nums.length; i++) {
    for (let j = 0; j < ops.length; j++) {

      let mx = -Infinity
      let mn = Infinity

      for (let k = 0; k < ops.length; k++) {
        if (nums[i] === 0 && k === 3) {
          // If current number is zero, removing division
          ops.splice(3, 1)
          dp.splice(3, 1)
          continue
        }

        const opMax = ops[j](dp[i-1][k][0], nums[i])
        const opMin = ops[j](dp[i-1][k][1], nums[i])
        mx = Math.max(opMax, opMin, mx)
        mn = Math.min(opMax, opMin, mn)
      }

      dp[i].push([mx,mn])
    }
  }

  return Math.max(...dp[nums.length-1].map(v => Math.max(...v)))
}


// Tests

console.log(max_value([1, 12, 3]))
console.log(max_value([1, 0, 3]))
console.log(max_value([17,-34,2,-1,3,-4,5,6,7,1,2,3,-5,-7]))
console.log(max_value([59, 60, -0.000001]))
console.log(max_value([0, 1, -0.0001, -1.00000001]))