问题描述:
给定一个非负数列,每次从中取出一个数字,而这个数字左右两边的邻居值的乘积,作为本次取数的得分。被取出数字之后,剩余组成新的数列,且相对位置不变,如果最后这个数字没有左邻居或者右邻居,则补1即可。求,这些数字被取完,所有得分的总和最大是多少?
例如下图
注:这个题目是同学出的,传说是,三星实习生转正的测试题目,其实,如果没有左邻居或者右邻居是补 1, 还是补 0, 我不清楚,现在是按照补 1,计算。
问题分析:
(1)首先理解一个事情,就是把整个数列看做一个整体,那么最优解值,一定是最后一个数字 i 的得分是1(i 是未知的),加上以 i 为分界线,左右两个区间的最优解(有点像二分法)。这个不难理解,最后就剩下一个数字时,很显然,把两边的最优解加上最后一个 1 ,就是整体的最优解。这样一来就满足了动态规划的两个基本条件:
最优子结构:每个区间都符合这样解的结构。
重叠子问题:左右区间又生成左右区间,构成重叠问题。
(2)问题:最后被取走的数字i 是哪一个?因为已经知道区间内的所有数字,那么就可以枚举(遍历)这些数字,找到最优的那个 i 。这类问题一般称区间 dp 。
(3)问题:左右小区间的最优解如何确定?那么现在就是,从最小区间开始计算,通俗的讲,先计算长度2的小区间,然后依次增加区间的长度。每次计算时,小区间就像一个滑动窗口,例如从长度为2的小区间作为一个窗口,从左向右依次滑动,求出所有小区间的最优解保存到dp[l][r]中。可以看出计算过程是自底向上的。所以dp[l][r]只用到了上三角的空间(l<r)。
(4)dp状态转换方程:
dp方程式为:dp[left][right] = max(dp[left][right], nums[left] * nums[right] + dp[left][i] + dp[i][right])
dp[left][right],表示区间 left-->right 的最优解(不包括,left 和 right 两个边界)
nums[left] * nums[right],表示最后一个被取出的数字的得分,为什么要乘上两个边界值那?因为它是最后一个被取出来的,整体来看,最后就剩下一个,就是乘上两个1,在子区间内,就是乘上两边的边界值了。
max(dp[left][right], nums[left] * nums[right] + dp[left][i] + dp[i][right]),表示从区间内开始枚举 i,计算出本次区间的最优解。
Python3实现(时间复杂度O(n3),空间复杂度O(n2)):
# @Time :2018/7/14
# @Author :LiuYinxing
# 解题思路 区间动态规划
# 题目大意,给定一个数组,每次删除一个数字,并把这个数字相邻的两个数的积作为本次得分,如果左右没有相邻的数字,则补 1 。
# 求这个数列的最大得分是多少?
class Solution:
def maxScore(self, Nums):
nums = [1] + Nums + [1] # 首尾添加[1],方便计算
n = len(nums)
dp = [[0] * n for _ in range(n)] # 初始化dp
for k in range(2, n): # k 确定一个滑动窗口的大小,从2开始
for left in range(0, n - k): # 滑动窗口,从左向右滑动,确定区间的开始(left)、结束(right)位置
right = left + k
for i in range(left + 1, right): # 开始枚举,区间内选择一个数字,并使其得分最高
dp[left][right] = max(dp[left][right],
nums[left] * nums[right] + dp[left][i] + dp[i][right])
return dp[0][n - 1]
if __name__ == '__main__':
nums = [3, 1, 5, 8]
solu = Solution()
print(solu.maxScore(nums))
欢迎指正哦。