《计算之魂》阅读笔记 02



1.3 怎样寻找最好的算法


例题 1.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 个数: a 1 , a 2 , . . . , a K \bm{a_{1}, a_{2}, ..., a_{K}} a1,a2,...,aK
    • 选取区间为: [ p , q ] \bm{[p, q]} [p,q]
    • 区间内的数字总和: S ( p , q ) = a p + a p + 1 + . . . + a q \bm{S(p,q)} = a_{p} + a_{p+1} + ... + a_{q} S(p,q)=ap+ap+1+...+aq

  • 其中
    1. p ∈ [ 1 , K ] \bm{p} ∈ [1, K] p[1,K]
    2. q ∈ [ p , K ] \bm{q} ∈ [p, K] q[p,K]
    3. 每一对 [ p , q ] \bm{[p, q]} [p,q] 组合平均要做 K / 3 \pmb{K / 3} K/3K/3 次加法
    • 综上,时间复杂度是 O ( n 3 ) \pmb{O(n^3)} O(n3)O(n3)

注意:这里第3点,每对组合平均要做的加法次数,原文给的是 K / 4 K / 4 K/4(有误),勘误如下:

证明:参考大牛思路,可用数学归纳法,如下:

在这里插入图片描述
在这里插入图片描述


方法二:二重循环
  • 记录三个中间值
    1. p 到当前位置 q 的总和 S ( p , q ) \bm{S(p,q)} S(p,q)
    2. p 到当前位置 q 所有总和中的最大值 Max
    3. 区间结束的位置 r

  • 示例
    • 假设区间起点 p = 500,这时 S(500,500) = a500Max = a500r = 500
    • 遇到 a501 后,根据 a501 是否大于0 决定 是否更新 qMaxr
    • 以此类推,继续往后扫描

  • 分析
    1. p ∈ [ 1 , K ] p ∈ [1, K] p[1,K],有 K 种取法
    2. ∀ p ∈ [ 1 , K ] \bm{\forall} p ∈ [1, K] p[1,K],需要从头到尾试 K-p
    • 综上,时间复杂度是 O ( n 2 ) \pmb{O(n^2)} O(n2)O(n2)

方法三:分而治之
  • 方法
    • 将序列一分为二: [ 1 , K / 2 ] [1,K/2] [1,K/2] [ K / 2 , K ] [K/2, K] [K/2,K]
    • 分别求两个子序列的总和最大区间 AB
    • 递归算法 求每个子序列的总和最大区间

  • 结论
    • 如果 A、B 间没有间隔,且区间总和均为正整数,则整个序列的总和最大区间就是 [ p , q ] \bm{[p, q]} [p,q]

    • 如果 A、B 间有间隔,假设 A = [ p 1 , q 1 ] A=[p_{1}, q_{1}] A=[p1,q1] B = [ p 2 , q 2 ] B=[p_{2}, q_{2}] B=[p2,q2],则整个序列的总和最大区间是 [ p 1 , q 1 ] \bm{[p_{1}, q_{1}]} [p1,q1] [ p 2 , q 2 ] \bm{[p_{2}, q_{2}]} [p2,q2] [ p 1 , q 2 ] \bm{[p_{1}, q_{2}]} [p1,q2] 中最大的一个

      证明:分情况讨论即可:

      在这里插入图片描述

    • 时间复杂度是 O ( n l o g n ) \bm{O(nlogn)} O(nlogn)


方法四:正反扫描
  1. 在序列中扫描找到第一个大于0的数,复杂度 O ( n ) O(n) O(n)

    如果所有数字非正(≤0),那么要找的区间就是 整个序列中最大的数

  2. 借鉴方法二,令 p = 1 p=1 p=1 q = 2 , 3 , . . . , K q=2,3,...,K q=2,3,...,K,计算 S ( 1 , q ) \bm{S(1,q)} S(1,q)Maxf(前向最大)和 r
  3. 扫描到最后( q = K q=K q=K),所保留的 Maxf 对应的 r 就是要找区间的右边界

    如果 ∀ q ∈ [ 2 , K ] \bm{\forall} q ∈ [2, K] q[2,K],都有 S ( 1 , q ) ≥ 0 S(1,q)≥0 S(1,q)0,情况会比较简单

  4. 反向扫描,同理计算出 Maxb(后向最大)和 l \bm{l} l(左边界)

  • 我们使用例题1.3的数据,依次计算前向累积后向累积值,结果如下:
    序列中元素的值、前向累计之和以及后向累计之和
    从图表中我们易知:
    • Maxf = 39.3,对应 r = 10 r=10 r=10
    • Maxb = 40.8,对应 l = 5 l=5 l=5

  • 但如果 S ( 1 , q ) S(1,q) S(1,q) 在某处小于0,并且之后一直小于0,情况就变复杂了

    比如我们将1.3的数据改动 2 个(8 和 9 改为 -62.2 和 44.2),如下表:
    改动后的元素、前向累计之和和后向累计之和
    前向累计之和在某个位置之后就一直小于零的情况,其峰值在后向累计之和峰值之前
    由图表知,Maxf 出现在 r = 6 r=6 r=6 的位置,Maxb 出现在 l = 9 l=9 l=9 的位置:

    • 右边界在左边界的左边,算法错误
    • 原本 [9, 10] 之间元素和为 49.6,是真正的 Maxf
    • 在累加了前 8 个元素之后和小于0,并一直小于0,因此没找到

  • 对步骤2、3进行改进
    • 步骤2:先把左边界 p p p 固定在第一个大于0的位置,令 q = p , p + 1 , . . . , K q=p,p+1,...,K q=p,p+1,...,K,计算 Maxfr

      如果算到 S ( p , q ) < 0 S(p,q)<0 S(p,q)<0,从 q q q 开始反向计算 Maxb,此时可以确定从第1个数到第 q q q 个数的和最大区间 [ l 1 , r 1 ] [l_{1}, r_{1}] [l1,r1](这里 l 1 = p l_{1}=p l1=p)和 Max1

    • 步骤3:从 q + 1 q+1 q+1 开始向后扫描,重复步骤2,可能在算到某个 q ′ q' q 时,又会出现 S ( q + 1 , q ′ ) < 0 S(q+1,q')<0 S(q+1,q)<0,以此可以得到第二个局部最大区间 [ l 2 , r 2 ] [l_{2}, r_{2}] [l2,r2]Max2,接着确定从头开始到 q ′ q' q 的和最大区间

      比较 Max1Max2Max1 + Max2 + S ( l 1 , r 2 ) S(l_{1}, r_{2}) S(l1,r2),发现从头开始到 q ′ q' q 的和最大区间是 [ l 1 , r 1 ] [l_{1}, r_{1}] [l1,r1] [ l 2 , r 2 ] [l_{2}, r_{2}] [l2,r2],每次保留更大的区间到中间变量 Max [ l , r ] [l,r] [l,r] 中即可

    • 接着,步骤4用步骤3的方法,向后扫描完整个序列,更新完 Max
    • 最后,得到的局部和最大区间 [ l , r ] [l,r] [l,r],就是全局和最大区间

  • 小结一下序列在累计求和时出现的两种情况,如下两图所示:
    简单情况:总和一直大于0的上下波动
    复杂情况:分段总和大于0的上下波动
    简单来说,就是 全局和最大区间 = Max {大于0的局部和最大区间}

  • 综上,这个算法只需要 将整个序列扫描两遍(正反),时间复杂度为 O ( n ) \bm{O(n)} O(n)



【思考题 1.3.1】

【问】将例题1.3的线性复杂度算法写成伪代码。

【答】参考了网络大牛的做法,加上了一些自己的理解:

def maxSubArray(arr):
    # 算到 S(p,q)<0 时,得到的最大局部区间和
    sum_tmp = 0
    # 当前最大总和初始化
    sum_max = arr[0]
    # 左边界初始化
    left = 0
    # 右边界初始化
    right = 0
    # 正向局部左边界初始化
    left_tmp = 0
    # 完整遍历序列
    for i in range(len(arr)):
        # 正向累加
        sum_tmp += arr[i]
        # 局部最大 > 当前最大
        if sum_tmp > sum_max:
            # 更新当前最大值
            sum_max = sum_tmp
            # 更新右边界值 r
            right = i
            # 更新左边界值 l(上一轮S<0的局部左边界值)
            left = left_tmp
        # 局部总和值小于0,跳过当前数(简化)
        if sum_tmp < 0:
            # 更新局部和
            sum_tmp = 0
            # 更新局部左边界值
            left_tmp = i + 1
    # 返回全局最大值和全局总和最大区间
    return sum_max, arr[left:right + 1]

if __name__ == '__main__':
    arr = [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]
    # (49.6, [44.2, 5.4])
    print(maxSubArray(arr))

结测试,例题1.3的两种情况均验证成功。


【思考题 1.3.2】

【问】在一个数组中寻找一个区间,使得区间内的数字之和等于某个事先给定的数字。

【答】参考了网络大牛的做法:

def subarraySum(arr, T):
    '''
    1. 用字典代替哈希表(当子序列和已经存在于字典中时,可直接跳过)
    2. 循环计算子序列和 S(1, i) 时,可以寻找 S(1, i) - T 是否在字典中,
       找到的key值满足:对字典中已存在的 S(1, j),j < i
    '''
    hash_dict = {}
    temp_sum = 0
    for i in range(0, len(arr), 1):
        temp_sum += arr[i]
        if temp_sum == T:
            return (0, i)
            if temp_sum - T in hash_dict.keys():
                return (has_dict[temp_sum - T] + 1, i)

            if temp_sum in hash_dict.keys():
                continue
            else:
                hash_dict[temp_sum] = i

【思考题 1.3.3】

【问】在一个二维矩阵中,寻找一个矩形的区域,使其中的数字之和达到最大值。

【答】参考了网络大牛的做法:

def getMaxMatrix(matrix):
    r1 = c1 = r2 = c2 = 0
    max_sum_matrix = matrix[0][0]
    for i in range(0, len(matrix), 1):
        arr_list = [0] * len(matrix[0])
        for j in range(i, len(matrix), 1):
            # 将新的一行与原矩阵逐项累加,生成新的一维序列
            arr_list = [arr_list[k] + matrix[j][k] for k in range(0, len(arr_list), 1)]
            (temp_max_sum, temp_left, temp_right) = self.maxSubArray(arr_list)
            if temp_max_sum > max_sum_matrix:
                max_sum_matrix = temp_max_sum
                c1 = temp_left
                c2 = temp_right
                r1 = i
                r2 = j
    return [r1, c1, r2, c2]



总结

  • 在计算机科学领域,从业者要想提高解决问题的能力,就需要不断培养对计算机的感觉。阅读例题1.3的四种解法,我们可以总结出如下建立感觉的方法:
    • 认识问题边界(至少扫描整个序列一次)
    • 优化算法(少做无用功)
    • 逆向思维(反向累加)



参考资料

  • 《计算之魂》第一章
  • https://zhuanlan.zhihu.com/p/476641538
  • 大牛的聊天记录
  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
《仙剑奇侠传》之父姚壮宪热情推荐,技术作家孟岩高度评价! 云风也是我在中国最佩服的游戏开发者。看了云风的研发历程,我觉得就是一部中国的游戏程序史,从最早的 Z80 , 6502 , PC8088 , 286 , 386…DOS ,保护模式, Assembler 到 C++ 的整个发展轨迹。这本书可以说横跨了游戏程序的过去、现在和未来。 书中传达的不仅是一些实用的技术经验,更是传达一种理念——虽然研发的环境随着时代而变,但研发的精神是不变的,那就是“在实践中积累”。 ——《仙剑奇侠传》之父 姚壮宪 之前我经常奇怪,云风还非常年轻,他程序思想中的那种老练的智慧是从何处得来的呢?读完这本书之后,我终于明白,还是那句话:“无他,唯手熟耳”。 面对这沉甸甸的作品,我确实感到,这是云风用心写的书。用心写的书,当然出色。 ——技术作家 孟岩 我现在是中国并不成熟的游戏制作行业中的一员,游戏给了我太多,我告诉自己需要做一点事情。分享知识和经验是我的义务,别无它。 ——云风 内容简介 本书忠实地记录了作者十余年来对游戏编程的所思、所感、所悟。全书按照作者本人学习和实践的过程,带着读者从基础的计算机知识到高级的编程技术,从非常专业的汇编优化到非常实际的项目管理进行了一次游戏开发的全景探索。 本书不仅适合游戏开发者阅读,也会给所有的开发者和程序爱好者带来启示。 作者简介 云风,时年二十七岁。自幼学习编程,十数年从未间断,对程序设计有所领悟。大学时代开发的游戏图像引擎“风魂”曾用于多家游戏公司的游戏项目。参与过《大话西游》系列、《梦幻西游》、《网易泡泡游戏》的开发。现从事新一代网络游戏引擎的研究与开发,并在游戏模式上做一些新的尝试。 性格开朗,兴趣广泛,好交友,绝非沉浸在计算机世界中的书呆子。国学、历史书籍常备案头,以先贤之教诲修其心;休息时常作户外运动,尤其喜爱攀岩。 目录 第1章 计算机,游戏,我 1 1.1 计算机 2 1.2 计算机游戏 3 1.3 计算机与我 7 1.3.1 启蒙 7 1.3.2 编程 9 第2章 算法,程序的灵魂 13 2.1 程序=算法+数据结构 14 2.1.1 算法 15 2.1.2 数据结构 17 2.2 搜索算法 23 2.2.1 地图寻路问题 23 2.2.2 博弈问题 27 2.2.3 更为广泛的运用 28 2.3 智能算法 29 2.3.1 遗传算法(Genetic Algorithm) 29 2.3.2 模拟退火算法(Simulated Annealing) 31 2.3.3 禁忌搜索(Tabu Search) 33 2.3.4 人工神经网络 (Artificial Neural Network) 34 2.4 优化 36 2.4.1 质数问题 36 1.4.2 俄罗斯方块竞赛 37 2.5 Apple II上的编程之路 39 第3章 编程语言 45 3.1 C 语言 46 3.2 BASIC 50 3.3 C++ 51 3.4 汇编语言 54 3.4.1 概述 55 3.4.2 程序的本质 57 3.4.3 寄存器 58 3.4.4 寻址方式 60 3.4.5 汇编指令 61 3.4.6 C/C++ 语言和汇编 62 3.4.7 小结 63 3.5 其他语言 63 3.5.1 Forth 63 3.5.2 Lisp 64 3.5.3 Java 64 3.5.4 Python、Lua、更多 65 第4章 前Windows 时代 67 4.1 386保护模式 68 4.2 VGA 到VESA 70 4.2.1 超越 BGI 70 4.2.2 VGA 72 4.2.3 VESA 标准 72 4.2.4 花絮 74 4.3 保护模式下的开发工具 75 4.4 闲话 Allegro 81 4.4.1 用C与汇编写成的程序库 81 4.4.2 BITMAP 82 4.4.3 Sprite 85 4.4.4 几何图形和 3D 89 4.4.5 数据文件 91 4.4.6 声音 92 4.4.7 其他的部分 93 4.4.8 小结 94 4.5 cfido 中国惠多网 94 第5章 Windows 编程 101 5.1 Windows编程入门 104 5.1.1 Windows版本综述 105 5.1.2 操作系统的核心 107 5.1.3 Windows API和DLL 110 5.1.4 COM 111 5.1.5 Windows的窗口和消息处理与传递 114 5.1.6 Windows GDI 125 5.2 控制游戏的速度 130 5.3 浅谈MFC 132 5.4 小结 132 第6章 汇编优化 135 6.1 浅谈代码优化 138 6.2 并不仅仅是汇

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值