《算法导论》第4章 分治策略(递归,最大子数组问题,矩阵的Strassen乘法)

        (最近在自己学习《算法导论》一本书,之前本来喜欢手写笔记,但是随即发现自己总是把笔记弄丢,所以打算做一个电子版的笔记)

        (另外书中用的都是伪代码,笔记中如果需要尝试的地方都是python代码)

分治策略:递归地求解问题,往往可以分为三个步骤:

分解(Divide)将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小。

解决(Conquer)递归地求解出子问题,如果子问题的规模足够小就直接求解

合并(Combie)将子问题的解组合成原问题的解。

        我们一般把一开始面对的复杂的问题称作递归情况,划分到足够小之后的问题称为基本情况

最大子数组问题:

(书中给出的预知股价的那个问题不是特别好理解,问题的数学模型大概是:)

        对于一组已知的数据y=f(x),如果我们需要取x1、x2,使得y1和y2的差尽量大,那是非常容易的(数学模型告诉我们只需要取的两个y是定义域里的最大值和最小值就可以),但是如果我们要求取到的小值y1和大值y2满足y1<y2,这个问题就复杂起来了:

        上图中的1是第一种情况,也就是对于两个点的位置关系没有要求,只要求差距尽可能的大,而在书中的股票模型中——因为我们不可能先卖出股票再买入股票,所以要求买入的低价一定要出现在我们选择的卖出的高价之前,例如图1这样的情况就是在实际情况中达不到的。所以要采用算法来求解这个问题。

        书中采取的处理方式是:首先将这一系列(x,y)转化成第i天的价格和第i-1天的价格的差(类似于求导的过程)。然后求新的数组中哪个子数组的和是最大的(也就是很多天的价格的累积量),进而求解问题。

        相比于暴力枚举破解,计算所有可能的和,能够节省计算量的过程从哪里来呢?

        例如,当我们在计算第3天到第10天到价格变化量的和的同时,也会计算第3天到第9天到变化量和,这两个过程在暴力枚举到过程中是独立的,但是事实上作为一个人类,应该可以想到第三天到第十天到价格变化量的和就是第三天到第九天到价格变化量的和加上第十天的价格变化量,类似的可以利用之前算出来的基础数据,节省很多计算量。

废话少说,先上代码:

#  算法导论第四章,寻找最大子数组
#  调试过程中学到的东西:python缺省的最大调用深度是999
#  可以import sys, sys.setrecursionlimit(100000) 给他改深

def find_cross_mcs(array, low, mid, high):
    print(low, mid, high)
    max_left, max_right = 0, 0
    left_sum = -1000000
    sum = 0
    for i in range(mid, low - 1, -1):  # 注意一下这里的步长的写法,初学者很容易忘记这个参数。
        sum += array[i]
        if sum > left_sum:
            left_sum = sum
            max_left = i
    right_sum = -1000000
    sum = 0
    for i in range(mid + 1, high + 1):  # 注意这里是mid+1,对于输入样例,加1给出的结果是7 10 43,不加1给出61,18会被多算一遍,证明有一个cross分界点是18
        sum += array[i]
        if sum > right_sum:
            right_sum = sum
            max_right = i
    return max_left, max_right, left_sum + right_sum


def find_mcs(array, low, high):
    print(low, high)
    if low == high:
        return low, high, array[low]
    else:
        mid = (low + high) // 2
        left_low, left_high, left_sum = find_mcs(array, low, mid)
        right_low, right_high, right_sum = find_mcs(array, mid + 1, high)  # 注意这里是mid+1
        cross_low, cross_high, cross_sum = find_cross_mcs(array, low, mid, high)
        if left_sum >= right_sum and left_sum >= cross_sum:
            return left_low, left_high, left_sum
        elif right_sum >= left_sum and right_sum >= cross_sum:
            return right_low, right_high, right_sum
        else:
            return cross_low, cross_high, cross_sum


arr = [100, 113, 110, 85, 105, 102, 86, 63, 81, 101, 94, 106, 101, 79, 94, 90, 97]
array = []
for i in range(0, len(arr) - 1):
    array.append(arr[i + 1] - arr[i])
print(array)
print(find_mcs(array, 0, len(array) - 1))  # 带进去的两个参数作为位置索引而不是循环变量,根据python的特性len应该要减一【len=6的列表最后一个值索引是[5]】

输出如下:

[13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
0 15
0 7
0 3
0 1
0 0
1 1
0 0 1
2 3
2 2
3 3
2 2 3
0 1 3
4 7
4 5
4 4
5 5
4 4 5
6 7
6 6
7 7
6 6 7
4 5 7
0 3 7
8 15
8 11
8 9
8 8
9 9
8 8 9
10 11
10 10
11 11
10 10 11
8 9 11
12 15
12 13
12 12
13 13
12 12 13
14 15
14 14
15 15
14 14 15
12 13 15
8 11 15
0 7 15
(7, 10, 43)

进程已结束,退出代码为 0

解说:对于一个任意规模的数组,我们求最大子数组。我们假设数组的中点是mid,从中点把它分开,那么这个数组的最大子数组存在以下三种可能:

1,它的最大子数组范围正好处在切片的前一半

2,它的最大子数组范围处于切片的后一半

3,最大子数组包含切点。

进而,1和2的情况会回到初始的问题,继续迭代求解,对于3情况可以用一种简明的方法来处理:将最大子数组拆成两个部分,对于这两个部分,因为已经确定一个边界是中点,从原来的需要两个点(时间复杂度是数据规模的平方)到现在只需要确定一个点,很容易就可以求解中点左侧的最大孙子数组和右侧的最大孙子数组,两个孙子连在一起就成了过中点的最大子数组了。之后再将123情况的解比较一下,就可以得到问题的解

当问题规模足够小,也就是low=high的时候,直接可以求解。也就是递归的回归条件。

最大子数组问题的解法2

我们考察如下问题:

如果已知[0, i]数组切片的最大子数组,那么[0, i+1]的最大子数组应该只有两种可能:

1,和[0, i]的相同;

2,是[k, i+1];

于是我们可以衍生出如下方法:

# 算法导论第四章 寻找最大子数组 解法2

def k_to_jplus1(array, end):  # 对应文中的情况2,计算右界为j+1的时候的值
    max = -1000000
    sum = 0
    max_left = 0
    for i in range(end, -1, -1):
        sum += array[i]
        if sum > max:
            max = sum
            max_left = i
    return max_left, max

arr = [100, 113, 110, 85, 105, 102, 86, 63, 81, 101, 94, 106, 101, 79, 94, 90, 97]
array = []
for i in range(0, len(arr) - 1):
    array.append(arr[i + 1] - arr[i])
print(array)
max_left, max_right = 0, 0
max_sum = 0
for i in range(0, len(array)-1):
    ktj_max_left, ktj_sum = k_to_jplus1(array, i+1)
    print(ktj_max_left,i+1,  ktj_sum)
    if ktj_sum > max_sum:  # 比较之前的最大子数组和新计算的j+1为右界的最大子数组的大小关系,并更新
        max_sum = ktj_sum
        max_left = ktj_max_left
        max_right = i+1
print(max_left, max_right, max_sum)

输出如下:

[13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
0 1 10
0 2 -15
3 3 20
3 4 17
3 5 1
3 6 -22
7 7 18
7 8 38
7 9 31
7 10 43
7 11 38
7 12 16
7 13 31
7 14 27
7 15 34
7 10 43

进程已结束,退出代码为 0

        虽然也可以输出7 10 43的正确结果,且没有采用分治算法。但是这个算法速度上并不如分治算法快,我尝试import time之后,将array规模扩大了1000倍,分治算法用时是0.15s,而解法2用时需要6.53秒。分治算法的nlogn复杂度在处理大规模数据的时候比解法2更加优秀。

        书中提到该问题还有线性时间的算法,但是个人并没有work出来到底是个什么样的算法……

        矩阵的strassen乘法在这篇博客​​​​​​​中讲的很详细,而且相关内容很多很容易找到,这里就不再赘述了~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值