【问题标题】:Trying to solve using recursion without using other algorithms尝试使用递归解决而不使用其他算法
【发布时间】:2020-10-08 23:19:28
【问题描述】:

我正在努力更好地理解递归,以便更好地实施动态编程原则。我知道这个问题可以使用 Kadane 的算法来解决;但是,我想使用递归来解决它。

问题陈述:

给定一个整数数组,找到总和最大的不相邻元素的子集。计算该子集的总和。

我写了以下部分解决方案:

const maxSubsetSum = (arr) => {
    let max = -Infinity

    const helper = (arr, len) => {
        if (len < 0) return max
        let pointer = len
        let sum = 0
        while (pointer >= 0) {
            sum += arr[pointer]
            pointer -= 2
        }
        return max = Math.max(sum, helper(arr, len - 1))
    }
    return helper(arr, arr.length - 1)
}

如果我有这些数据:

console.log(maxSubsetSum([3, 5, -7, 8, 10])) //15 
//Our subsets are [3,-7,10], [3,8], [3,10], [5,8], [5,10] and [-7,10]. 

我的算法计算出 13。我知道这是因为当我开始我的算法时,我的 (n-2) 个值被计算出来,但我没有考虑其他 (n-3) 个或更多仍然验证问题陈述的子集健康)状况。我无法弄清楚解释其他值的逻辑,请指导我如何实现这一点。

【问题讨论】:

    标签: javascript algorithm recursion


    【解决方案1】:

    代码将递归(在helper 中调用helper)与迭代(在helper 中的while 循环)结合起来。你应该只使用递归。

    对于数组的每个元素,有两种选择:

    1. 跳过当前元素。在这种情况下,总和不变,可以使用下一个元素。所以递归调用是 sum1 = helper(arr, len - 1, sum)
    2. 使用当前元素。在这种情况下,当前元素被添加到总和中,并且必须跳过下一个元素。所以递归调用是 sum2 = helper(arr, len - 2, sum + arr[len])

    所以代码看起来像这样:

    const maxSubsetSum = (arr) => {
    
        const helper = (arr, len, sum) => {
            if (len < 0) return sum
            let sum1 = helper(arr, len - 1, sum)
            let sum2 = helper(arr, len - 2, sum + arr[len])
            return Math.max(sum1, sum2)
        }
    
        return helper(arr, arr.length - 1, 0)
    }
    

    【讨论】:

    • 这很有帮助,你会如何记住这个解决方案?是二维数组还是一维数组?
    • @Altaf 我认为这是一个一维数组,因为唯一需要记住的是给定len 的部分总和。
    • 我无法记住解决方案,所以我所做的是在帮助器const newArr = new Array(arr.length).fill(-1) 编写返回if (newArr[len] !== -1) return newArr[len] 的条件之前声明一个数组,然后将帮助器函数的返回更改为return newArr[len] = Math.max(sum1, sum2),但我做的不对。
    • 这为输入 [-3, -5, -7, -8, -10] 给出 0
    • @SomeDude 当然,假设空集的总和为 0,这对于该示例来说是最好的。
    【解决方案2】:

    您的想法是正确的,一旦您从当前索引开始,您需要从 (n-2) 递归。但是你似乎不明白你不需要遍历你的数组来得到总和然后递归。 所以正确的做法是

    • 要么包含当前项并递归剩余的 n-2 项,要么

    • 不包括当前项并在剩余的 n-1 项上递归

    让我们看看这两个选择:

    选择 1:

    您选择在当前索引中包含该项目。然后你对剩余的 n-2 个项目进行递归。因此,您的最大值可能是项目本身,而不添加到任何剩余的 n-2 项目或添加到 n-2 项目中的某些项目。 所以 Math.max( arr[idx], arr[idx] + recurse(idx-2)) 是这个选择的最大值。如果 recurse(idx-2) 给您 -Infinity,您只需考虑当前索引处的项目。

    选择 2:

    您没有选择在当前索引中包含该项目。所以只需对剩余的 n-1 项进行递归 - recurse(n-1)

    最终的最大值是这两个选项中的最大值。

    代码是:

    const maxSubsetSum = (arr) => {
        let min = -Infinity
        const helper = (arr, idx) => {
          if ( idx < 0 ) return min
          let inc = helper(arr, idx-2)
          let notInc = helper(arr, idx-1)
          inc = inc == min ? arr[idx] : Math.max(arr[idx], arr[idx] + inc)
          return Math.max( inc, notInc )
        }
        return helper(arr, arr.length - 1)
    }
    
    console.log(maxSubsetSum([-3, -5, -7, -8, 10]))
    console.log(maxSubsetSum([-3, -5, -7, -8, -10]))
    console.log(maxSubsetSum([-3, 5, 7, -8, 10]))
    console.log(maxSubsetSum([3, 5, 7, 8, 10]))
    

    输出:

    10
    -3
    17
    20
    
    • 对于所有项目均为负数的情况:

    在这种情况下,您可以说没有任何项目可以组合在一起以获得最大总和。如果这是要求,则结果应为零。在这种情况下,只需将 0 作为默认结果返回 0。这种情况下的代码是:

    const maxSubsetSum = (arr) => {
        const helper = (arr, idx) => {
          if ( idx < 0 ) return 0
          let inc = arr[idx] + helper(arr, idx-2)
          let notInc = helper(arr, idx-1)
          return Math.max( inc, notInc )
        }
        return helper(arr, arr.length - 1)
    }
    
    • 有记忆:

    您可以为您在递归期间访问的索引记住此解决方案。只有一种状态,即索引,所以你的备忘录是一维的。带备忘录的代码是:

    const maxSubsetSum = (arr) => {
        let min = -Infinity
        let memo = new Array(arr.length).fill(min)
        const helper = (arr, idx) => {
          if ( idx < 0 ) return min
          if ( memo[idx] !== min) return memo[idx]
          let inc = helper(arr, idx-2)
          let notInc = helper(arr, idx-1)
          inc = inc == min ? arr[idx] : Math.max(arr[idx], arr[idx] + inc)
          memo[idx] = Math.max( inc, notInc )
          return memo[idx]
        }
        return helper(arr, arr.length - 1)
    }
    

    【讨论】:

    • 我也喜欢你的解决方案,试着像上面的解决方案一样记忆它,但不能正确地做到这一点,你能展示一下你如何记忆这个解决方案吗?
    • @Altaf 我在备忘录中添加了。
    • 我试过你的解决方案,它看起来是正确的;但是,我在这个问题hackerrank.com/challenges/max-array-sum/problem 中的很多测试用例都遇到了运行时错误,你知道可能是什么问题吗?
    • @Altaf 请考虑任何答案作为方向,您需要自行进一步调查您可能会看到的任何其他问题。
    • 只是想理解这一行inc = inc == min ? arr[idx] : Math.max(arr[idx], arr[idx] + inc) 我知道这一行的目的是比较这些索引处的数字总和,看看总和是否更大,或者是否只是index 更大,但我不知道真实条件如何满足这种情况。
    【解决方案3】:

    具有明显递归的基本版本足够简单。我们要么在总和中包含当前值,要么不包含。如果我们这样做,我们需要跳过下一个值,然后在剩余的值上重复。如果我们不这样做,那么我们需要在当前值之后重复所有值。我们选择这两个结果中较大的一个。这几乎直接转化为代码:

        const maxSubsetSum = ([n, ...ns]) => 
          n == undefined  // empty array
            ? 0
            : Math .max (n + maxSubsetSum (ns .slice (1)), maxSubsetSum (ns))
    

    更新

    这缺少一个案例,我们的最高和只是数字本身。此处已修复(以及在下面的 sn-ps 中)

    const maxSubsetSum = ([n, ...ns]) => 
      n == undefined  // empty array
        ? 0
        : Math .max (n, n + maxSubsetSum (ns .slice (1)), maxSubsetSum (ns))
    
    console.log (maxSubsetSum ([3, 5, -7, 8, 10])) //15 

    但是,正如您在 cmets 中所指出的,出于性能原因,我们确实可能想要记住这一点。我们可以选择几种方法来做到这一点。一种选择是将我们在一次函数调用中测试的数组转换为可以用作ObjectMap 中的键的东西。它可能看起来像这样:

    const maxSubsetSum = (ns) => {
      const memo = {}
      const mss = ([n, ...ns]) => {
        const key = `${n},${ns.join(',')}`
        return n == undefined
          ?  0
        : key in memo
          ? memo [key]
        : memo [key] = Math .max (n, n + maxSubsetSum (ns .slice (1)), maxSubsetSum (ns))
      }
      return mss(ns)
    }
    
    console.log (maxSubsetSum ([3, 5, -7, 8, 10])) //15 

    我们也可以使用一个辅助函数来实现这一点,该函数作用于索引并使用索引作为键进行记忆。它的复杂程度大致相同。

    不过,这有点难看,也许我们可以做得更好。

    这种记忆有一个问题:它只持续当前运行。我要记住一个函数,我宁愿它保存 any 调用相同数据的缓存。这意味着函数的定义中的记忆。我通常使用可重用的外部 memoize 助手来执行此操作,如下所示:

    const memoize = (keyGen) => (fn) => {
      const cache = {}
      return (...args) => {
        const key = keyGen (...args)
        return cache[key] || (cache[key] = fn (...args))
      }
    }
    
    const maxSubsetSum = memoize (ns => ns .join (',')) (([n, ...ns]) => 
      n == undefined
        ? 0
        : Math .max (n, n + maxSubsetSum (ns .slice (1)), maxSubsetSum (ns)))
    
    console.log (maxSubsetSum ([3, 5, -7, 8, 10])) //15

    memoize 接受一个函数,该函数使用你的参数生成一个字符串键,并返回一个接受你的函数并返回它的记忆版本的函数。它通过在您的输入上调用密钥生成来运行,检查该密钥是否在缓存中。如果是,我们只需返回它。如果没有,我们调用您的函数,将结果存储在该键下并返回。

    对于这个版本,生成的键只是通过将数组值与',' 连接起来创建的字符串。可能还有其他同样好的选择。

    请注意,我们不能这样做

    const recursiveFunction = (...args) => /* some recursive body */
    const memomizedFunction = memoize (someKeyGen) (recursiveFunction)
    

    因为memoizedFunction 中的递归调用将是非记忆的recursiveFunction。相反,我们总是必须像这样使用它:

    const memomizedFunction = memoize (someKeyGen) ((...args) => /* some recursive body */)
    

    但是为了能够简单地用密钥生成器包装函数定义来记忆函数,这是一个很小的代价。

    【讨论】:

    • 添加了基于来自 @גלעד ברקן 的 cmets 的轻微更新。
    【解决方案4】:

    此代码已被接受:

    function maxSubsetSum(A) {
      return A.reduce((_, x, i) =>
        A[i] = Math.max(A[i], A[i-1] | 0, A[i] + (A[i-2] | 0)));
    }
    

    但是尝试递归那么远(我尝试提交 Scott Sauyet 的 last memoised example),我相信会导致运行时错误,因为我们可能会超过递归限制。

    为了好玩,这里是自上而下填充的自下而上:)

    function f(A, i=0){
      if (i > A.length - 3)
        return A[i] = Math.max(A[i] | 0, A[i+1] | 0);
        
      // Fill the table
      f(A, i + 1);
    
      return A[i] = Math.max(A[i], A[i] + A[i+2], A[i+1]);
    }
    
    var As = [
      [3, 7, 4, 6, 5], // 13
      [2, 1, 5, 8, 4], // 11
      [3, 5, -7, 8, 10] // 15
    ];
    
    for (let A of As){
      console.log('' + A);
      console.log(f(A));
    }

    【讨论】:

    • 一个不错的方法,虽然我更喜欢修改 reduce 累加器而不是我们的输入列表。但这是一个小变种。我寻找了类似的东西,但错过了max 的一个参数:当前值本身。但请注意,这个问题专门关于如何递归地执行此操作,从“我正在努力更好地理解递归......”开始。
    • @ScottSauyet 这就是为什么我提供了我的第二种方法,它是递归的(和记忆的)。
    • @ScottSauyet 我没有注意到您自己忘记了元素的大小写,我想知道这是否是您的代码中某些测试用例失败的原因,而不是我对递归深度的假设。但似乎有不少。
    • 是的,我没有去那个网站尝试。我现在可能会修正我的答案。但我确实喜欢你的方法,除了我真的不想修改输入。
    • @ScottSauyet 我希望答案中的所有信息都对 OP 有所帮助。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-11
    • 2015-10-25
    • 1970-01-01
    • 1970-01-01
    • 2012-11-14
    • 1970-01-01
    • 2019-07-11
    相关资源
    最近更新 更多