最大连续乘积子数组
题目描述
{−2.5, 4, 0, 3, 0.5, 8, −1},则取出的最大乘积子数组为{3 , 0.5, 8}。也就是说,在上述数组中,3、0.5和8这三个数是连续的,而且乘积是最大的。
分析与解法
此最大连续乘积子数组与最大乘积子序列不同,因为最大乘积子序列不要求元素连续,而最大连续乘积子数组要求元素是连续的。
解法一:蛮力轮询
初看此题,读者可能立刻会想到用最简单粗暴的方式:用两个循环直接轮询。
但是,这种蛮力方法的时间复杂度为O(n2),能否想办法降低时间复杂度呢?
解法二:动态规划
乘积子数组中可能有正数、负数,也可能有 0。因为有负数的存在,所以可以考虑同时找出最大乘积和最小乘积。于是,问题简化成这样:在数组中找到一个子数组,使得它的乘积最大,同时再找到另一个子数组,使得它的乘积最小(含有负数的情况)。也就是说,不但记录最大乘积,也记录最小乘积。
假设数组为 a[],直接利用动态规划来求解。考虑到负数的情况,用maxend来表示以 a[i]结尾的最大连续子数组的乘积值,用 minend 表示以 a[i]结尾的最小连续子数组的乘积值,那么状态转移方程为:
maxend = max(max(maxend * a[i], minend * a[i]), a[i]);
minend = min(min(maxend * a[i], minend * a[i]), a[i]);
初始状态为maxend=minend=a[0]。参考代码如下:
上述动态规划求解方法只用了一个for循环,所以时间复杂度为O(n)。
背景知识
可能有不少读者对动态规划不是很了解,下面简单介绍一下动态规划的基本原理。
定义
动态规划一般用来求解最优化问题,其适用的条件是要求待求解的最优化问题具备两个因素:最优子结构和子问题重叠。通过求解一个个最优子问题,将解存入一张表中,当后续子问题的求解需要用到之前子问题的解时直接查表,每次查表的代价为常数时间。
示例
下面来看两个简单的例子。
例如,现在要找一条从起点A到终点B的最短路径,那么在寻找A到B的最短路径时,可能会经过x1, x2, x3等几个点,然后从中选择出一条最短的路径即可。动态规划求解最短路径问题的一般思路是,先枚举从A到B可能要经过的所有路径,然后比较各种路径的长短,找出最短的路径。
再如,要求在一个二维矩阵m×n矩阵matrix中找出一条路径,使得路径经过的元素之和最小,但每次只能向右或向下走。假定当前位置是(i, j),那么上一个位置只可能是(i, j−1)或(i−1, j),故可以写出深度优先方程求解:path[i][j]=min(path[i][j−1], path[i−1][j]) + matrix[i][j]。
求解过程
通过之前最短路径的例子我们知道,动态规划可以看成是在一个有向无环图的状态图上“折腾”,图的节点代表各个状态,图的起点表示初始状态,图的终点集合(可能有多个)表示最终状态,图的边表示从状态A能转换到状态B,而从图上的某个点到另外一个点是否可达意味着最终要求解的最短路径问题是否有解。
动态规划解决最优化问题的关键在于以下几点。
·状态的定义。
·方案的枚举。注意,没必要枚举“全部”。我们以前或许习惯了逐个枚举,但当我们把具有某些性质的(部分)解作为一个集合一起考虑时,这个集合对应多个解,而在一个集合里只有最优解才有意义,因而要减少状态个数。
·最优子问题和无后效性保证。一个状态下的最优值取决于到达这个状态的全部子状态的最优值(最优子问题),且状态之间的最优值不互相影响(即各自独立,无后效性)。
·子问题有重叠。
·最为关键的就是编写状态转移方程,而状态转移方程的实质就是状态图上边的表示,一条边说明两个端点之间的关系。比如,寻找状态x的所有前驱y,由于当前节点的值完全由其前驱节点确定,所以只需要通过状态转移方程来求解哪一个前驱节点可以导致当前节点的最优值即可。
从另一层意义上讲,动态规划本质上是一种递推。假设当前状态是S,所有能达到S的状态是S′,用f(state)表示在状态state下的最优值,payoff表示代价,则有:
f(S)=min{ f(S′ ) + payoff(从S′ 到S)}
打印一张表的时间复杂度就是O(状态总数×某状态下最大转移数)。换言之,分析动态规划的时间复杂度一般看子问题的个数和每个子问题需要考虑多少种选择,时间复杂度即为子问题的个数和每个子问题需要考虑的选择的个数的乘积。因此,解决动态规划问题一般只需想清楚状态表示,直接写递推式即可。
与贪心法的区别
贪心法比较直观,容易想到,但对“图”的要求高,有时甚至需要先排序然后才能用贪心法。而动态规划不是纯粹的枚举,是“聪明的”枚举,枚举的一个状态通常对应“很多”种可能。因此,相比较而言,贪心法是特殊的动态规划(子问题不重叠)。
总而言之,贪心法是“最优子结构且局部最优”,动态规划是“最优独立重叠子结构且全局最优”。一句话理解动态规划就是:枚举所有状态,然后剪枝,寻找最优状态,同时将每一次求解子问题的结果保存在一张“表”中,以后再遇到重叠的子问题时从表内保存的状态中查找(俗称记忆化搜索)。
举一反三
计算组合中乘积最大的一组
给定一个长度为n的整数数组,只允许用乘法,不能用除法,请计算任意n−1个数的组合中乘积最大的一组,并写出算法的时间复杂度。
点评:可以把所有可能的n−1个数的组合找出来,然后分别计算它们的乘积并比较大小。由于总共有n个数的组合,所以总的时间复杂度为O(n2)。读者可以继续思考更好的解法。