《计算之魂》第1章 毫厘千里之差——大O概念(1.3节)

1.3 怎样寻找最好的算法

例题1.3 总和最大区间问题(难度系数3颗星)

给定一个实数序列,设计一个最有效的算法,找到一个总和最大的区间。
(与“寻找一只股票的最长的有效增长期”题目,即计算一只股票从哪天开始买进到哪天卖出的收益最大,表述不同,解法相同)
比如在下面的序列中:
1.5, −12.3, 3.2, −5.5, 23.2, 3.2, −1.4, −12.2, 34.2, 5.4, −7.8, 1.1, −4.9
总和最大的区间是从第5个数(23.2)到第10个数(5.4)。

题目数学表述:

假设此序列有 K K K个数: a 1 , a 2 , . . . , a K a_1, a_2, ..., a_K a1,a2,...,aK
假定区间起始数字的序号是 p p p, 结束数字的序号是 q q q,区间数字的总和为 S ( p , q ) S(p, q) S(p,q),即 S ( p , q ) = a p + a p + 1 + . . . + a q S(p, q) = a_p + a_{p+1} + ... + a_q S(p,q)=ap+ap+1+...+aq
区间左边界的序号为 l l l,右边界的序号为 r r r

方法1:做一次三重循环

复杂度为 O ( K 3 ) O(K^3) O(K3)

p p p取值可以从 1 1 1 K K K, q q q取值从 p p p K K K,这是两重循环,复杂度为 O ( K 2 ) O(K^2) O(K2),循环的内部计算 S ( p , q ) S(p, q) S(p,q)平均要做 K / 4 K/4 K/4次加法,这是第三重循环,所以总的复杂度为 O ( K 3 ) O(K^3) O(K3)

方法1代码如下:

(自己根据理解写的代码)

# python
def method1(list):
    l = -1
    r = -1
    Max = -10e100
    for i in range(len(list)):
        for j in range(i,len(list)):
            temp = 0
            for t in range(i, j):
                temp += list[t]
            # temp = sum(list[i:j])
            if Max < temp:
                l = i
                r = j
                Max = temp
    print("区间左边界序号是:", l)
    print("区间右边界序号是:", r)
    print("最大总和是:", Max)

结果如下:
例子计算结果

方法2:做两重循环

复杂度为 O ( k 2 ) O(k^2) O(k2)

在方法一的最后加法循环中,做完 p p p q q q的加法,下一步重新再做 p p p q + 1 q+1 q+1的加法, p p p q q q的加法部分又重新做了一遍,出现了大量重复计算。简化方法是保存上一步的和,下一步再加一次就可以。

方法2代码如下:

(自己根据理解写的代码)

# python
def method2(list):
    l = -1
    r = -1
    Max = -10e100
    for i in range(len(list)):
        temp = list[i]
        for j in range(i+1, len(list)):
            temp += list[j] # 这里只需一步加法计算
            if Max < temp:
                l = i
                r = j
                Max = temp
    print("区间左边界序号是:", l+1)
    print("区间右边界序号是:", r+1)
    print("最大总和是:", Max)


a = [1.5, -12.3, 3.2, -5.5, 23.2, 3.2, -1.4, -12.2, 34.2, 5.4, -7.8, 1.1, -4.9]
method2(a)

结果同上

方法3 利用分治(Divide-and-Conquer)算法

复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)

思考题: 这种方法答案为什么是(1) [ p 1 , q 1 ] [p_1, q_1] [p1,q1], (2) [ p 2 , q 2 ] [p_2, q_2] [p2,q2], (3) [ p 1 , q 2 ] [p_1, q_2] [p1,q2]中的一种?
个人作答: 一个区间分成左右两部分,很显然最大子序列要么出现在左半边小区间,要么右半边小区间,要么就是跨左右小区间,所以答案是这三种的一种。

代码如下 [1]:(这里只计算了最大和,没有列出最大和对应区间边界,显然,是不会添加代码T.T)

# python
def method3(list):
    n = len(list)
    if n == 1:
        return list[0]
    else:
        # 递归计算左半边最大子序和
        max_left = method3(list[0:n//2])
        # 递归计算右半边最大子序和
        max_right = method3(list[n//2:n])

    # 计算中间的最大子序和,从右到左计算左边的最大子序和,
    # 从左到右计算右边的最大子序和,再相加
    max_l = list[n // 2 - 1]
    temp = 0
    for i in range(n//2-1, -1, -1):
        temp += list[i]
        max_l = max(temp, max_l)
    max_r = list[n // 2]
    temp = 0
    for i in range(n//2, n):
        temp += list[i]
        max_r = max(temp, max_r)
    # 返回三个值中的最大值
    return max(max_left, max_right, max_l+max_r)


a = [1.5, -12.3, 3.2, -5.5, 23.2, 3.2, -1.4, -12.2, 34.2, 5.4, -7.8, 1.1, -4.9]
print("最大子序和是:", method3(a))

答案如下:
方法3代码运行结果

方法4 正、反两遍扫描的方法

复杂度为 O ( N ) O(N) O(N)

如果不考虑右边界在左边界左边的情况,代码如下:
(自己根据理解写的代码)

# python
def method4(list):
    result = list[0]
    num = -1
    ll = -1  # 第一个大于0的元素下标
    for i in range(len(list)):
        if list[i] > result:
            result = list[i]
            num = i
        if list[i] > 0:
            ll = i
            break
        else:
            print("最大和是一个数:", result)
            print("这个数对应的序号为:", i)

    temp = list[ll:]
    result = 0
    temp_sum = 0
    lr = -1  # 右边界下标
    for i in range(len(temp)):
        temp_sum += temp[i]
        if temp_sum > result:
            result = temp_sum
            lr = i + ll
    # 反过来从右到左找左边界
    rr = -1
    for i in range(len(list)-1, -1, -1):
        if list[i] > 0:
            rr = i
            break

    temp = list[:rr+1]
    result = 0
    temp_sum = 0
    rl = -1  # 左边界下标
    for i in range(len(temp)-1, -1, -1):
        temp_sum += temp[i]
        if temp_sum > result:
            result = temp_sum
            rl = i
    # 计算总和
    result = sum(list[rl: lr+1])
    print("区间左边界序号是:", rl+1)
    print("区间右边界序号是:", lr+1)
    print("最大总和是:", result)


a = [1.5, -12.3, 3.2, -5.5, 23.2, 3.2, -1.4, -12.2, 34.2, 5.4, -7.8, 1.1, -4.9]
method4(a)

结果如下:
方法4一般情况运行结果

如果更改数组为下面这种情况:

# python
b = [1.5, -12.3, 3.2, -5.5, 23.2, 3.2, -1.4, -62.2, 44.2, 5.4, -7.8, 1.1, -4.9]
method4(b)

运算结果如下:
特殊情况运行结果
这显然是不对的。所以需要更改代码,书上更改代码部分说“ 如果我们算到某一步时,发现 S ( p , q ) < 0 S(p,q)<0 S(p,q)<0,这时,我们需要从位置 q q q开始,反向计算 M a x b Maxb Maxb”,这里就循环套循环了,复杂度应该是 O ( N 2 ) O(N^2) O(N2)了(实在写不出来这部分代码T.T,得3分已经超出想象了,就这水平还要啥自行车)。

总结
  1. 这题能很好体现算法复杂度,找出复杂度最小的算法,需要具备的能力:(1)考虑问题周全;(2)头脑清晰,能把复杂问题想清楚(真好,我一个没有)
  2. 从问题解决者,进步到变成能找到最佳解决方案的高度,需要培养对计算机科学的感觉(题感?),例如对于这个题目,有经验的从业者一开始就能够大致判断出它一定有优于平方复杂度[即 O ( N 2 ) O(N^2) O(N2)]的解法。这样,他们才会直接朝这个方向努力。这样的感觉如何建立呢?书中吴军老师给的三点个人体会如下:
    (1)对一个问题边界的认识:在这道例题中,至少要扫描整个序列一次,因此最优解法的下界不可能低于线性复杂度。
    (2)在计算机科学中,优化算法最常用的方法就是检查一种算法是否在做大量无用功。 O ( N 2 ) O(N^2) O(N2)复杂度的算法显然存在大量无用功。
    (3)逆向思维, 倒着想问题很重要。

思考题1.3

Q1.将例题1.3的线性复杂度算法写成伪代码。(难度系数2颗星)
已经直接写成代码了,伪代码就省略了(偷懒ing)

Q2.在一个数组中寻找一个区间,使得区间内的数字之和等于某个事先给定的数字。
(AB、FB、LK等公司的面试题,后面会解答。(难度系数3颗星))
[2]

个人作答(只会 O ( N 2 ) O(N^2) O(N2)的解法):

# python
def method_q2(nums, k):
    l = -1
    r = -1
    for i in range(len(nums)):
        temp = 0
        for j in range(i, len(nums)):
            temp += nums[j]
            if temp == k:
                l = i
                r = j
                return [l+1, r+1]

Q3.在一个二维矩阵中,寻找一个矩形的区域,使其中的数字之和达到最大值。
(例题1.3的变种,硅谷公司真实的面试题。(难度系数4颗星))

个人作答:(复杂度 O ( N 4 ) O(N^4) O(N4)…)

# python
def method_q3(nums):
    result = 0
    l1 = -1
    l2 = -1
    r1 = -1
    r2 = -1
    for i in range(len(nums)):
        for j in range(len(nums[0])):
            s = 0
            for m in range(i, len(nums)):
                for n in range(j, len(nums[0])):
                    s += nums[m][n]
                    if s > result:
                        result = s
                        l1 = i
                        l2 = m
                        r1 = j
                        r2 = n
    return [nums[i][r1:r2+1] for i in range(l1, l2+1)], result
参考内容:

[1] leetcode 第53题:最大子数组和。 题解里PandaWaKaKa的解法
[2] 这题与leetcode 第560题 和为K的子数组比较相似

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值