一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第26天,点击查看活动详情。
题目链接
题目大意
给一个数组,我们需要找到一个子数组,让这个子数组的和
最大,返回这个和
。
但是有一个限制:
- 不能挑选相邻元素
举例
比如说 [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
来代表,没有计算过,需要进行计算。