这是一个简单的目录
前言
最近正在学习dp,其中一个很经典的例子是最大连续子段和。为了打开思路,我就想着用三种方法来求解这个问题的最优解。如果有任何问题,欢迎看文的小伙伴们指正呀~
正文
1. 暴力法
本人实在是太菜了,如果有哪里说的不对的,看文的小伙伴请直接指出,谢谢~*×1
算法思想:顾名思义,暴力法就是把所有的子段的和都求解出来并记录下来,通过多次比较再最终得出最大的子段和并返回。于是我们枚举每个子段**可能的起点**和**长度**,然后**计算出从某起点开始、某长度的子段和**。
这样想的话我们很容易可以想到是三层循环:第一层控制起点,第二层控制长度,第三层计算该子段的和,时间复杂度为O(n³)。
可这也太慢了,我们自然而然就想要找到更好的算法。
对于时间复杂度为O(n³)的暴力法,我们可以很容易就想到其中的一点优化:对于第三层循环其实并没有必要,既然子段的起点是一样的,只是长度不同,那就没有必要在每次长度增加的时候,再用一层循环从同一个起点开始求和,**只要用一个变量作为该起点的所有长度的累加和就好了呀**。这样就避免了一层循环,**将时间复杂度提升到了O(n²)**!
# 输入
lst = [int(i) for i in input().split()]
n = len(lst)
# 先将最大值设为负无穷,这样的话后续求到的和可以很快替代掉这个值
maximum = float('-inf')
# i代表最大连续子段和的起点,j代表终点
i = 0
j = 0
# 用循环变量start枚举起点,用循环变量move枚举长度的变化,用cur_sum来记录当前计算的子段和
for start in range(n):
# 先将把起点的值赋给当前子段和
cur_sum = lst[start]
# 与maxisum比较,若cur_sum更大则更新maxisum,并立刻更新下标
if cur_sum > maximum:
maximum = cur_sum
i = j = start
for move in range(start + 1, n):
# 随着长度的变化,cur_sum的值也开始不断累加
cur_sum += lst[move]
# 如果cur_sum更大就更新,下标同理
if cur_sum > maximum:
maximum = cur_sum
i = start
j = move
print(maximum)
print(i, j)
2. 1 分治法(不返回最优解版本)
本人实在是太菜了,如果有哪里说的不对的,看文的小伙伴请直接指出,谢谢~*×2
算法思想:分治法的笼统思想就是**把大问题划分为问题相同的更小的子问题去求解,简称“分而治之”**,可以分为三步:**Divide, Conquer and Combine**。运用分治法求解最大子段和问题可以想到,我们的最优解会出现在三种情况:
- 最优解出现在序列的左边
- 最优解出现在序列的右边
- 最优解出现在序列的中点,并向两端蔓延
Divide:我们把序列一直二分,直到长度短到无法再分并可以直接解决,即len == 1;
Conquer:“治”其实看具体的题目,不同的题目体现不同。比如归并排序或者是双调排序,治就体现在将数据按照升序或降序或者是序列的特性来排序。对于最大子段和问题,我个人的理解是,其中的“治”就体现在MaxSubSum的递归出口和MaxSubSum_Cross中,优雅地、分别地处理了子问题的左边的最大和、右边的最大和及从中点起向两边扩散的最大和;
Combine:这是分治法中很容易被忽略的一步。我个人的理解是,其中的“合并”就体现在递归回溯上。从不可再分的最小的子问题的答案一直回溯到最初的问题的答案,妙不可言~
整理好这三部曲后,我们便可以着手实现:将序列分分分后,直到不可再分的子段,此时便直接返回该子段的唯一的那个值,这也就是为什么分治法要分到可以直接解决的子问题,这也就是递归出口啦!接下来我们再分别调用递归,分别求出左部分的和,右部分的和及中间部分的和,最后返回这三部分中的最大值便是问题的最优值啦!
分治法的思想大多是用递归实现的,所以要掌握好分治的思想要有递归的基础,但并不是有了递归的基础就可以很容易地写出分治(比如我这个菜鸡…),所以还是要多加练习,多分析!
比起暴力法的O(n²),分治法的时间复杂度提升到了O(nlogn)!
lst = [int(i) for i in input(