最长子串练习:圆周率前50位的最长子段和

导读

这里是一个有关最长子串的练习题,因为很有意思就记录一下。

题目说明

下面给出圆周率的前 50 50 50位,给出每位数字减掉 5 5 5后的序列,并计算新序列的最大子段和。

3.14159 , 26535 , 89793 , 23846 , 26433 , 83279 , 50288 , 41971 , 69399 , 37510 3.14159,26535,89793,23846,26433,83279,50288,41971,69399,37510 3.14159,26535,89793,23846,26433,83279,50288,41971,69399,37510

读题

题目首先给出了 52 52 52个字符,也就是从 3 3 3开始一直到 0 0 0之间包括小数点。我们要把这些全部放进数组,并且每一位数字减去 5 5 5,最终找出一个最大子段和

前提说明

首先,我们确认环境:Python 3.8.5 3.8.5 3.8.5

然后看看题,既然给出了小数,那我们直接把小数一次次乘以 10 10 10,每次取出整数部分并减掉,不就完了?

答案是不行的

虽然Python中有一个Decimal格式能够保存 50 50 50位甚至更多位的小数,但是不能保存一开始就给出的数字,而只能够保存计算得出的数字

也就是说像这样:

import decimal

if __name__ == '__main__':
  decimal.getcontext().prec = 50
  print(decimal.Decimal(1)/decimal.Decimal(3))  
  pass

才能够显示 50 50 50位小数,也就是

0.33333 , 33333 , 33333 , 33333 , 33333 , 33333 , 33333 , 33333 , 33333 , 33333 0.33333,33333,33333,33333,33333,33333,33333,33333,33333,33333 0.33333,33333,33333,33333,33333,33333,33333,33333,33333,33333

直接给出 50 50 50位小数则会在转换的过程中因为没办法表示直接溢出并丢弃

所以,题目给出的

3.14159 , 26535 , 89793 , 23846 , 26433 , 83279 , 50288 , 41971 , 69399 , 37510 3.14159,26535,89793,23846,26433,83279,50288,41971,69399,37510 3.14159,26535,89793,23846,26433,83279,50288,41971,69399,37510

没有办法作为直接给出的数字计算,甚至都不能使用str()函数进行处理,只能够当作字符串进行下一步处理,也就是:

num = '3.14159265358979323846264338327950288419716939937510'

只有这样才能被完好地处理。

开始

既然我们知道了只能作为字符串传进来,那么我们就开始吧:

分离

# 将数字分离出来,并每个数字减去同样的值
def divide(num, minus):
  # 把字符串变为数组
  array = list(num)
  # 移除不需要的小数点
  array.remove('.')
  # 每个字符转为数字并减去同样的值
  return [int(item) - minus for item in array]
  pass

之后我们就可以通过调用divide('3.1415926...', 5)来获得我们需要计算最大子段和的数组了。

暴力破解子段和

遇事不决不如先来几层循环暴力破解。

既然是子段和,也就是确定子段起始点和子段结束点后全部加起来的大小。于是马上就能出来:

def violate(array):
  # 子段和
  temp = 0
  # 最终确认的子段和
  sum = 0
  # 记录起始位置
  start = 0
  # 记录结束位置
  end = 0
  # 三循环
  for i in range(0, len(array)):
    for j in range(i, len(array)):
      temp = 0
      for k in range(i, j):
        temp += array[k]
        pass
      # 因为要求最大的子段,所以我们和相等的情况会拉长子段
      if temp >= sum:
        # 记录起始位置
        start = i
        # 由于第三层循环中的range(i,j)中最大只有j-1,所以end取j-1
        end = j - 1
        # 记录和
        sum = temp
      pass
    pass
  # 输出,其中array[start:end]中又最多只能输出到array[end-1],所以最后还得加上去
  print(f'start: {start}, end: {end}, length: {end - start}, array: {array[start : end + 1]}, sum: {sum}')
  pass

看着还不错。

我们把完整的代码放上去跑一下:

import math
# 分割字符串
def divide(num, minus):
  array = list(num)
  array.remove('.')
  return [int(item) - minus for item in array]
  pass
# 暴力破解
def violate(array):
  temp = 0
  sum = 0
  start = 0
  end = 0
  for i in range(0, len(array)):
    for j in range(i, len(array)):
      temp = 0
      for k in range(i, j):
        temp += array[k]
        pass
      if temp >= sum:
        start = i
        end = j - 1
        sum = temp
      pass
    pass
  print(f'start: {start}, end: {end}, length: {end - start}, array: {array[start : end + 1]}, sum: {sum}')
  pass
# 主函数
if __name__ == '__main__':
  raw = divide('3.14159265358979323846264338327950288419716939937510', 5)
  violate(raw)
  pass

于是输出了:start: 34, end: 48, length: 14, array: [3, 3, -1, -4, 4, 2, -4, 1, 4, -2, 4, 4, -2, 2, 0], sum: 14

没毛病。

我们来肉眼估计一下时间复杂度:三层for循环,大概是 O ( n 3 ) O(n^3) O(n3)

思考优化

可是这三层循环也太累赘了。打个比方的话,就相当于初中时候的相遇问题中加了一个像疯狗一样在两个相遇的个体间不停地来回折返跑的个体。脑子里除了为这只疯狗感到担忧,就只剩下对自己的担忧了。

那么,问题就在于:怎么无视掉这只疯狗那就是坐在旁边看着

# 暴力破解(优化版)
def violation(array):
  temp = 0
  sum = 0
  start = 0
  end = 0
  for i in range(0, 50):
    temp = 0
    for j in range(i, 50):
      temp += array[j]
      if (temp >= sum):
        sum = temp
        start = i
        end = j
        pass
      pass
    pass
  print(f'start: {start}, end: {end}, length: {end - start}, array: {array[start : end + 1]}, sum: {sum}')
  pass

于是就有了输出:

start: 34, end: 48, length: 14, array: [3, 3, -1, -4, 4, 2, -4, 1, 4, -2, 4, 4, -2, 2, 0], sum: 14

一样的输出诶!

看起来还不错。再来个肉眼估计,两层for循环,大概是 O ( n 2 ) O(n^2) O(n2)。这可比刚刚的循环好多了。现在是 50 50 50个数据,差距大概是 5 0 3 − 5 0 2 = 12250 50^3-50^2=12250 503502=12250次运算。如果是万级数据,差距会更加明显。

“非暴力不合作”

这个标题可能有点搞事情,不过别在意这些细节。

说实在的,暴力破解还是有点累赘。有没有再优化一点的方法?那当然是动态规划

动态规划

怎么说?核心思想,就是一遍过完所有的数据

怎么做?我们不如画个图。

开始 节点1 + int 值 > 0 sum() : void 节点2 + int 值 < 0 sum() : void 剪枝 节点3 + int 值 > 0 sum() : void 节点4 + int 值 < 0 sum() : void 节点5 + int 值 > 0 sum() : void 节点6 + int 值 > 0 sum() : void 输出存档点 开始累加,记录存档点 累加为正,覆盖存档点 累加为负 开始累加 累加为正,覆盖存档点 累加为负 累加为正,覆盖存档点 累加为正,覆盖早点起 遍历结束

为什么这么做?因为我们需要的是最大,而且给出的数据集中存在非负数,所以小于零的我们就不关注了。一旦检测到负数,我们就直接剪枝,只记录一下直到这之前的最大子段。然后,接下来就是用新的非负数代替新的子段起始位置。

举个例子,我们在这里拿到的数据是这样的(截取部分举例):

− 2 , − 4 , − 1 , − 4 , 0 , 4 , − 3 , 1 , 0 , − 2 , 0 , 3 , 4 , 2 , 4 , . . . -2, -4, -1, -4, 0, 4, -3, 1, 0, -2, 0, 3, 4, 2, 4, ... 2,4,1,4,0,4,3,1,0,2,0,3,4,2,4,...

  • 首先找一个非负数,也就是第 5 5 5位的 0 0 0
  • 然后从这里开始接着往后一边走一边累加,也就是:
    • 0 , 4 0, 4 0,4 累加得 4 4 4,记录存档点;
    • 0 , 4 , − 3 0, 4, -3 0,4,3累加得 1 1 1,未能高过前存档点,不覆盖;
    • 0 , 4 , − 3 , 1 0, 4, -3, 1 0,4,3,1累加得 2 2 2,未能高过存档点,不覆盖;
    • 0 , 4 , − 3 , 1 , 0 0, 4, -3, 1, 0 0,4,3,1,0累加得 2 2 2,未能高过存档点,不覆盖;
    • 0 , 4 , − 3 , 1 , 0 , − 2 0, 4, -3, 1, 0, -2 0,4,3,1,0,2累加得 0 0 0,未能高过存档点,不覆盖;
    • 0 , 4 , − 3 , 1 , 0 , − 2 , 0 0, 4, -3, 1, 0, -2, 0 0,4,3,1,0,2,0累加得 0 0 0,未能高过存档点,不覆盖;
    • 0 , 4 , − 3 , 1 , 0 , − 2 , 0 , 3 0, 4, -3, 1, 0, -2, 0, 3 0,4,3,1,0,2,0,3累加得 3 3 3,未能高过存档点,不覆盖;
    • 0 , 4 , − 3 , 1 , 0 , − 2 , 0 , 3 , 4 0, 4, -3, 1, 0, -2, 0, 3, 4 0,4,3,1,0,2,0,3,4累加得 7 7 7,高过存档点,覆盖前存档点;
    • 0 , 4 , − 3 , 1 , 0 , − 2 , 0 , 3 , 4 , 2 0, 4, -3, 1, 0, -2, 0, 3, 4, 2 0,4,3,1,0,2,0,3,4,2累加得 9 9 9,高过存档点,覆盖前存档点;
    • 0 , 4 , − 3 , 1 , 0 , − 2 , 0 , 3 , 4 , 2 , 4 0, 4, -3, 1, 0, -2, 0, 3, 4, 2, 4 0,4,3,1,0,2,0,3,4,2,4累加得 13 13 13,高过存档点,覆盖前存档点;
  • ……

这就是大致的步骤。如果在后面的计算中遇到了负数,我们就直接把这个子段停掉,重新开始子段计算。打个比方的话就像是嗦面嗦到一半用牙齿咬断一样

既然有了思路,我们就开始编码:

def dynamic(array):
  temp = 0
  sum = 0
  start = 0
  end = 0
  for index in range(0, len(array)):
    if temp + array[index] > array[index]:
      temp += array[index]
    else:
      temp = array[index]
      start = index
      pass
    if temp >= sum:
      sum = temp
      end = index
      pass
    pass
  print(f'start: {start}, end: {end}, length: {end - start}, array: {array[start : end + 1]}, sum: {sum}')
  pass

跑一下,输出就成了

start: 34, end: 48, length: 14, array: [3, 3, -1, -4, 4, 2, -4, 1, 4, -2, 4, 4, -2, 2, 0], sum: 14

也是一样的输出。再肉眼观测一下,一个for循环,于是只剩下 O ( n ) O(n) O(n)。这就相当不错了,又是一个质的飞跃。

是不是有点能理解了?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ordinary_brony

代码滞销,救救码农

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值