Leetcode DP解题 按摩师

105 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第26天,点击查看活动详情

题目链接

leetcode-cn.com/problems/th…

题目大意

给一个数组,我们需要找到一个子数组,让这个子数组的最大,返回这个

但是有一个限制:

  • 不能挑选相邻元素

举例

比如说 [1,2,3,1], 我们挑下标0,和下标2的两个元素,是4。

首先解决数组长度很小的几种情况

如果数组长度为0,那么直接返回0.
如果数组长度是1, 那么直接返回这个元素值。
如果数组长度是2, 那么在这两个元素中挑大的那个返回。
如果数组长度是3, 那么有两种情况:

  • 下标0,下标2 组合
  • 下标1

这两种方法,看哪个大,哪个大就返回哪个。

这几种情况的代码很容易写出,我建议直接按照上面的逻辑,提前在解题函数最前面写出来,因为这样就不会犯什么边界的错误。这种写法,一般称之为快速路径(fast path)。

数组长度很长的情况

我们喜欢短的数组,这样好处理。

那么就看有没有办法,把长数组,变成短数组,去处理。

这就是DP算法的核心难点:化长为短。

分析如何化长为短

对于一个数组来说,里面的第一个元素,我可以挑中,也可以丢弃,不管我挑中还是丢弃,问题都能简化:

  • 如果我挑中第一个元素,那么第二个元素就不能要,那么剩余就是从第三个元素开始,待分析的部分减少了2个长度
  • 如果我丢弃第一个元素,那么剩余就是从第二个元素开始,待分析的部分减少了1个长度

这两种情况,都是化长为短。

用伪码来表示:

func massage(nums []int) int {
    ...
    ...
    ...
    // 以上是 fast path ,快速路径
    
    var takeOne, dropOne int // 声明两个变量,代表挑中第一个,和丢弃第一个
    
    { // 第一种情况,挑中第一个
        takeOne = nums[0] // 挑中第一个
        takeOne += massage(nums[2:]) // 加上剩余部分,从第三个开始
    }
    
    { // 第二种情况,丢弃第一个
        dropOne = massage(nums[1:]) // 剩余部分就是从第二个开始
    }
    return max(takeOne, dropOne)
}

从上面我们看出来,递归使得描述问题十分简单,写代码也十分简单。

对于时间的优化

上面的代码,有一个地方不好,那就是,剩余部分的结果,有可能会被重复计算。

也就是说,调用 massage 函数的时候,传进去的数组元素,可能会重复,这样就会耗费更多时间。

我们要想一个办法,让 massage 函数,传进去的数组元素,如果相同,他能立即返回上次计算过的结果,而不用重复计算。

一般这种情况,缓存的概念就派上用场了。

我们用一个额外的数组,来存储这个结果。

这里为了实现逻辑方便,我们对处理函数重新设计:


func calc(nums, cacheNum []int, beginIndex int) int {
	if cacheNum[beginIndex] >= 0 {
		return cacheNum[beginIndex]
	}
	var takeBeginIndex, dropBeginIndex int
	if beginIndex == len(nums)-3 {
		// 快速路径
		takeBeginIndex = nums[beginIndex] + nums[beginIndex+2]
		dropBeginIndex = nums[beginIndex+1]
		return max(takeBeginIndex, dropBeginIndex)
	}
	if beginIndex == len(nums)-2 {
		// 快速路径
		takeBeginIndex = nums[beginIndex]
		dropBeginIndex = nums[beginIndex+1]
		return max(takeBeginIndex, dropBeginIndex)
	}
	if beginIndex == len(nums)-1 {
		// 快速路径
		return nums[beginIndex]
	}
	{
		takeBeginIndex = nums[beginIndex]
		cacheNum[beginIndex+2] = calc(nums, cacheNum, beginIndex+2)
		takeBeginIndex += cacheNum[beginIndex+2]
	}
	{
		cacheNum[beginIndex+1] = calc(nums, cacheNum, beginIndex+1)
		dropBeginIndex = cacheNum[beginIndex+1]
	}
	return max(takeBeginIndex, dropBeginIndex)
}


我们这里,用下标来进行迭代,而不是用slice。

然后第二个参数cacheNum就是来存放上述缓存用的。

我们可以看出,进入这个函数一开始,我们就去cacheNum里查询,对应下标有没有计算过,如果计算过,就直接返回。

注意初始化的时候,cacheNum 要全部置成-1。因为我们这里用-1来代表,没有计算过,需要进行计算。