1.问题引入
前面介绍了利用分治算法求解股票买卖问题,并且已经知道这个问题可以转化为求连续子序列和的最大值问题。如股票价格为
[
10
,
11
,
7
,
10
,
6
]
[10,11,7,10,6]
[10,11,7,10,6],那么前后两日的收益差值为
[
1
,
−
4
,
3
,
−
4
]
[1,-4,3,-4]
[1,−4,3,−4]。原问题就变换成给定一个序列,找出其中连续累加值最大的子序列,序列
[
1
,
−
4
,
3
,
−
4
]
[1,-4,3,-4]
[1,−4,3,−4]中
[
3
]
=
3
[3]=3
[3]=3即为连续累加值最大的子序列,意味着在股票价格为7的时候买入,股价为10的时候卖出将获得最佳收益,收益值为3。
当给定n个元素的序列A,求和最大的连续子序列时,输入序列元素必须包含负值才有意义。否则当输入序列均为正值,那么连续子序列和最大的就是原序列本身。
该问题最简单的算法就是穷举给定序列的所有子序列,然后求得子序列累加和的最大值。对于有
n
n
n个元素的序列,其所有连续子序列的个数为
n
2
n^2
n2。说明:n个元素的序列,每一个元素都可以作为连续子序列起点和终止点,而每一个起点可以对应n个终止点,故连续子序列的个数共有n2。该问题也可以用分治法来求解,分治算法的时间复杂度为
O
(
n
l
o
g
n
)
O(n\,log\,n)
O(nlogn)。下面将根据动态规划求解问题的基本步骤,介绍时间复杂度为
O
(
n
)
O(n)
O(n)的算法。
2.定义子问题
该问题输入的是一个有 n n n个元素的序列 A A A,不妨设 P ( i ) P(i) P(i)为直到第 i i i个元素的最大和,也就是将输入序列的前缀作为子问题。每一个元素就对应一个子问题,子问题的个数为 Θ ( n ) \Theta(n) Θ(n)。
3.猜测解
对于子问题 P ( i ) P(i) P(i),考察元素 A [ i ] = − 5 A[i]=-5 A[i]=−5,如图9.6所示。该元素要么在子问题的解中,要么不在,也就是:
- 子问题 P ( i ) P(i) P(i)的解包括第 i i i个元素,如图( 1 1 1)第二行所示。此时有 P ( i ) = P ( i − 1 ) + A ( i ) P(i)=P(i-1)+A(i) P(i)=P(i−1)+A(i);
- 子问题 P ( i ) P(i) P(i)的解不包括第 i i i个元素(即包括第 i i i个元素不是最大和,需要更换起点为,此时起止点重合在 A [ i ] A[i] A[i]取得最大和),如图(1)第三行所示。意味着需要从第 i i i个元素开始重新计算值,即 P ( i ) = A ( i ) P(i)=A(i) P(i)=A(i)
− 2 11 − 4 13 − 5 ) ‾ − 2 11 − 4 13 ) − 5 − 2 11 − 4 13 ( − 5 (1) \underline{\begin{array}{c} \begin{matrix} -2& 11& -4& 13\\ \end{matrix}-\left. 5 \right)\\ \end{array}} \\ \begin{matrix} -2& 11& -4& \left. 13 \right)& -5\\ \end{matrix} \\ \begin{matrix} -2& 11& -4& 13& \left( -5 \right.\\ \end{matrix} \tag 1 −211−413−5)−211−413)−5−211−413(−5(1)
4.子问题之间的递归关系
根据以上分析,可以建立子问题间的递归关系如下:
P
(
i
)
=
m
a
x
{
P
(
i
−
1
)
+
A
(
i
)
,
A
(
i
)
}
\begin{aligned} P(i)=max\{P(i-1)+A(i),A(i)\} \end{aligned}
P(i)=max{P(i−1)+A(i),A(i)}
初始条件为
P
(
0
)
=
0
\begin{aligned} P(0)=0 \end{aligned}
P(0)=0
这个递归式与之前拾捡硬币问题的递归式类似,都是当前子问题的解只与已经求解的子问题有关,也就是递归求解的过程满足拓扑排序。
5.自底向上构造动态规划表
对于每一个子问题的解,由于只与已经求出的子问题解有关。也就是说,如果将个子问题间的关系画成图,求解该问题的过程就相当于在图上做拓扑排序。因此,可自底向上实现的递归关系,见代码6。下表是输入序列 A = [ − 2 , 11 , − 4 , 13 , − 5 , 2 ] A=[-2, 11, -4, 13, -5, 2] A=[−2,11,−4,13,−5,2]时,自底向上构造的动态规划表table
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
table(i) | 0 | -2 | 11 | 7 | 20 | 15 | 17 |
6. Python实现时间复杂度O(n)的算法求解连续子序列和最大值
代码6
def bottom_up_cont_sub_seq(seq):
"""
自底向上实现时间复杂度O(n)的算法求解连续子序列和最大值
:param seq:原始序列
:return: 连续子序列和最大值,连续子序列和最优解
"""
item_nums = len(seq)
table = [0] * (item_nums + 1) # 初始化动态规划表
table[1] = seq[0] # 表中第2项为序列第1项
global_best = [] # 初始化最优解序列
for i in range(2, item_nums + 1):
table[i] = max(table[i - 1] + seq[i - 1], seq[i - 1]) # 由于表长比序列大1,所以当前元素索引为i-1
max_sum = max(table)
ind_max = table.index(max_sum)
while ind_max >= 1:
# 表当前元素值等于表左边子问题的值+序列当前元素值(索引比表小1)
if table[ind_max] == table[ind_max - 1] + seq[ind_max - 1]:
global_best.append(seq[ind_max - 1])
ind_max -= 1
# 达到起点终止
else:
global_best.append(seq[ind_max - 1])
break
return max_sum, global_best[::-1]
7.测试
if __name__ == '__main__':
sequence = [-2, 11, -4, 13, -5, 2]
max_value,global_best_sub_seq = bottom_up_cont_sub_seq(sequence)
print("原始序列为:{}\n连续子序列和最大值为:{}\n连续子序列和最优解为:{}".format(sequence,max_value,global_best_sub_seq))