LeetCode之分治算法实战之最大子序和(53)、多数元素(169)、汉诺塔问题

分治算法思想

1、最大子序和(53)

题目描述:

【简单题】

在这里插入图片描述

题目链接
思路分析

题解一:分治方法

【分解】连续子序列的最大和主要可由这三部分子区间里最大子序和的最大值得到,因此,可将原问题分解为在以下三个子区间分别求最大子序和的问题。当然,这些三个区间又可以以这样的方式不断的划分,直到子子区间的长度为1.

  • 第 1 部分:左子区间 [left, mid]
  • 第 2 部分:右子区间 [mid + 1, right]
  • 第 3 部分:包含/跨越子区间 [mid , mid + 1] 的子区间,即 nums[mid] 与 nums[mid + 1] 一定会被选取
    在这里插入图片描述

【解决】

  • 主要的还是求解跨越左右区间划分点的中间区域的最大子序和:此时需计算左子区间中从右端点开始(包含右端点)从右向左的最大子序和和右子区间中从最左端点开始(包含左端点)从左向右的最大子序和,再将这两个最大子序和相加。
  • 递归求解左子区间的最大子序和
  • 递归求解右子区间的最大子序和

【合并】

  • 返回三大区间解的最大值。

【代码实现】

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n = len(nums)
        #递归终止条件
        if n == 1:
            return nums[0]
        else:
            #递归计算左半边最大子序和
            max_left = self.maxSubArray(nums[0:len(nums) // 2])
            #递归计算右半边最大子序和
            max_right = self.maxSubArray(nums[len(nums) // 2:len(nums)])
        
        #关建一步:计算中间的最大子序和:从右到左计算左边的最大子序和,从左到右计算右边的最大子序和,然后两值再相加。
        max_l = nums[len(nums) // 2 - 1]
        tmp = 0
        for i in range(len(nums) // 2 - 1, -1, -1):
            tmp += nums[i]
            max_l = max(tmp, max_l)
        max_r = nums[len(nums) // 2]
        tmp = 0
        for i in range(len(nums) // 2, len(nums)):
            tmp += nums[i]
            max_r = max(tmp, max_r)
        #返回三个中的最大值
        return max(max_right,max_left,max_l+max_r)
  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度:递归会使用 O ( log ⁡ n ) O(\log n) O(logn) 的栈空间,故渐进空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

题解二:动态规划

第1步:定义状态

既然一个连续子数组一定要以一个数作为结尾,那么我们可以将状态定义成如下:

dp[i]:表示以num[i]结尾的连续子数组的最大和

第2步:状态转移方程

根据状态的定义,由于 nums[i] 一定会被选取,并且 dp[i] 所表示的连续子序列与 dp[i - 1] 所表示的连续子序列(有可能)就差一个 nums[i]

假设数组 nums 全是正数,那么一定有 dp[i] = dp[i - 1] + nums[i],但是搞不好 dp[i - 1] 是负数也是有可能的。例如前几个数都是负数,突然来了一个正数。

于是分类讨论:

  • 如果 dp[i - 1]>= 0,那么可以把 nums[i] 直接接在 dp[i - 1] 表示的那个数组的后面。

  • 如果 dp[i - 1] < 0,那么加上前面的数反而越来越小了,于是“另起炉灶”,单独的一个 nums[i],就是 dp[i]

以上两种情况的最大值就是 dp[i] 的值,写出如下状态转移方程:
d p [ i ] = { d p [ i − 1 ] + n u m s [ i ] ,   i f   d p [ i − 1 ] ≥ 0 n u m s [ i ] ,   i f   d p [ i − 1 ] < 0 dp[i]=\left\{ \begin{aligned} dp[i-1]+nums[i],\ if\ dp[i-1]\geq0 \\ nums[i],\ if\ dp[i-1]<0 \end{aligned} \right. dp[i]={dp[i1]+nums[i], if dp[i1]0nums[i], if dp[i1]<0

记为“状态转移方程 1”。

状态转移方程还可以这样写,反正求的是最大值,也不用分类讨论了,就这两种情况,取最大即可,因此还可以写出状态转移方程如下:

d p [ i ] = m a x { n u m s [ i ] , d p [ i − 1 ] + n u m s [ i ] } dp[i]=max\{nums[i],dp[i−1]+nums[i]\} dp[i]=max{nums[i],dp[i1]+nums[i]}

记为“状态转移方程 2”。

动态规划的问题经常要分类讨论,这是因为动态规划的问题本来就有最优子结构的特征,即大问题的最优解通常由小问题的最优解得到,那么我们就需要通过分类讨论,得到大问题的小问题究竟是哪些。

第3步:初始值

dp[0] 根据定义,一定以 nums[0] 结尾,因此 dp[0] = nums[0]

第4步:思考输出

这里状态的定义不是题目中的问题的定义,不能直接将最后一个状态返回回去。

输出应该是把所有的 dp[0]、dp[1]、……、dp[n - 1] 都看一遍,取最大值。

【代码实现】

#状态转移方程1
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n=len(nums)
        if n==0:
            return
        dp=[0 for _ in range(n)]
        dp[0]=nums[0]
        for i in range(1,n):
            if dp[i-1]>=0:
                dp[i]=dp[i-1]+nums[i]
            else:
                dp[i]=nums[i]
        return max(dp)
#状态转移方程2
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n=len(nums)
        if n==0:
            return
        dp=[0 for _ in range(n)]
        dp[0]=nums[0]
        for i in range(1,n):
            dp[i]=max(dp[i-1]+nums[i],nums[i])
           
        return max(dp)

  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( N ) O(N) O(N)

第5步:思考状态压缩

既然当前状态只与上一个状态有关,我们可以将每一个状态的值保存在一个表中,就可以将空间复杂度压缩到 O ( 1 ) O(1) O(1)

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n=len(nums)
        if n==0:
            return
        pre=nums[0]#pre移动指针,表示上一个状态的值
        res=pre
        for i in range(1,n):
            pre=max(nums[i],pre+nums[i])
            res=max(res,pre)
        return res
  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( 1 ) O(1) O(1)

2、多数元素(169)

题目描述:

【简单题】
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

在这里插入图片描述

题目链接

思路分析

题解一:哈希表

遍历整个数组,记录所有值出现的次数,则可以用到哈希表:

  • 特判,若数组中只有一个元素,直接返回
  • 初试化 m a x _ c o u n t = n / / 2 max\_count=n//2 max_count=n//2,表示若出现次数大于 n / / 2 n//2 n//2,则为众数。初始化哈希表hash={}
  • 遍历数组:
    • nums[i]不在hash中,则存下来,键为nums[i],值为1。
    • nums[i]在hash中,则令nums[i]对应值加一。
      • nums[i]对应值大于 m a x _ c o u n t max\_count max_count,则返回nums[i],为众数。

【代码实现1】

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        n=len(nums)
        max_count=n//2#初始化最大次数
        if n==1:
            return nums[0]
        hash={}# 字典的形式
        for i in nums:
            if i not in hash:
                hash[i]=1
            else:
                hash[i]+=1
                if hash[i]>max_count:
                    return i

观察上述代码,python中字典若没有关键字,则会出现错误,必须的先在字典加入此键。下面利用python中的defaultdict创建字典,就不会出现错误,而是当遇见没有键时,值会返回默认值0,不影响结果,而且也比较简便。

【代码实现2】

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        n=len(nums)
        max_count=n//2#初始化最大次数
        if n==1:
            return nums[0]
        hash=defaultdict(int)# 字典的形式
        for i in nums:
            hash[i]+=1
            if hash[i]>max_count:
                return i

[理解 Python 语言中的 defaultdict]

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)

题解二:分治方法

定义一个辅助函数,找众数。
【分解】

  • 将数组逐次分成左右两部分,分解的终止条件为数组的长度为1

【求解】

  • 递归求解左半部分的众数left_num
  • 递归求解右半部分的众数right_num
  • 递归终止条件:直到所有的子问题都是长度为 1 的数组。长度为 1 的子数组中唯一的数显然是众数,直接返回即可。

【合并】:求原问题解

  • 如果回溯后某区间的长度大于 1,我们必须将左右子区间的值合并。
  • 如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。否则,我们需要比较两个众数在整个区间内出现的次数来决定该区间的众数。

【代码实现】

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        return self.findnum(nums)
    def findnum(self,nums):
        if len(nums)==1:
            return nums[0]
        mid=len(nums)//2
        left_num=self.findnum(nums[0:mid])
        right_num=self.findnum(nums[mid:])
        if left_num != right_num:
            if nums.count(left_num)>=nums.count(right_num):
                return left_num
            else:
                return right_num
        else:
            return left_num

在这里插入图片描述
主定理

3、汉诺塔问题

题目描述:

【简单题】

在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
(1) 每次只能移动一个盘子;
(2) 盘子只能从柱子顶端滑出移到下一根柱子;
(3) 盘子只能叠在比它大的盘子上。

请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。

你需要原地修改栈。

在这里插入图片描述
题目链接

思路分析

1、假设盘子总数 n = 1,只有一个盘子,很简单,直接把它从 A 中拿出来,移到 C 上;
2、假设盘子总数 n > 1,由于盘子只能从柱子顶端滑出移到下一根柱子且盘子只能叠在比它大的盘子上,因此需要借助柱子B,先将上面n-1个盘子从A移到B,再将最大的盘子从A移到C,再将B上n-1个盘子按照上述方式移到C。

总体流程:

  • n = 1 时,直接把盘子从 A 移到 C;

  • n > 1 时,

    • 先把上面 n - 1 个盘子从 A 移到 B(子问题,递归);
    • 再将最大的盘子从 A 移到 C;
    • 再将 B 上 n - 1 个盘子从 B 移到 C(子问题,递归)。

【代码实现】

class Solution:
    def hanota(self, A: List[int], B: List[int], C: List[int]) -> None:
        """
        Do not return anything, modify C in-place instead.
        """
        n=len(A)
        self.move(n,A,B,C)
    def move(self,n,A,B,C):
        if n==1:
            C.append(A[-1])
            A.pop()
            return
        else:
            self.move(n-1,A,C,B)
            C.append(A[-1])
            A.pop()
            self.move(n-1,B,A,C)
  • 时间复杂度: O ( 2 n − 1 ) O(2^n-1) O(2n1)

  • 空间复杂度: O ( 1 ) O(1) O(1)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值