数据结构与算法:动态规划

动态规划

三大算法:分治法、动态规划与贪婪算法
分治法与动态规划的区别:
分治法将大问题分成小问题,例如二分法。子问题属性不变,小问题之间互相独立,需要做一个合并的过程(从上到下的方法)

动态规划也是将大问题拆解成小问题,不同之处是他的小问题有很多重复的,我们不需要重复的做,只需要将这些小问题存储起来,便于之后的查询,这种现象叫做记忆法!(从下到上的方法:先解决这些小问题)

斐波那契数列就是典型的动态规划

要做DP的问题需要给出一个公式,这个很重要

一维动态规划

1、给定n,找到不同的将n写成1,3,4相加的方法有多少个,顺序不一样算一种

n = 5,输出6
n = 1+1+1+1+1=1+3+4=1+3+1=3+1+1=1+4=4+1

解析:与斐波那契数一样,f(n) = f(n-1) + f(n-3) + f(n-4)
f(0) = f(1) = f(2) = 1 f(3) = 2 f(4) = 4

def count(n):
    dp = [None] * (n+1)
    dp[0] = dp[1] = dp[2] = 1
    dp[3] = 2
    for i in range(4,n+1):
        dp[i] = dp[i-1] + dp[i-3] + dp[i-4]
    return dp[5]

2、找到不相邻的加和最大数 [网易笔试刚好出这道题了,幸运]

假设你是一个职业抢劫犯,你打算洗劫一个街道,每一个房子中都有一定数量的钱,限制你的唯一条件是相邻的房子的安保系统是相连的,如果你抢劫相邻房子那么保安系统就会报警

给定一个非负整数的列表代表每个房子中的钱,计算在不惊动警察的情况下你可以抢劫到的最多的钱

在这里插入图片描述

def robe(array):
    n = len(array)
    # 生成两行n+1的0,第一列都为0,代表初始值
    # 规定第二行不取当前值所达到的最大值,第一行取当前值所达到的最大值
    dp = [[0 for _ in range(n + 1)] for _ in range(2)]
    for i in range(1,n + 1):
        dp[0][i] = dp[1][i-1] + array[i-1]
        dp[1][i] = max(dp[0][i-1], dp[1][i-1])
    return max(dp[0][n], dp[1][n])

array = [2,7,9,3,1]
robe(array)
> 12

这里是引入了两个空间,因此时间复杂度为N,空间复杂度也为N.但其实我们每次只用到了当前值的前面的那两个数字,因此我们可以做出优化,不用引入空间。

def robe(array):
    n = len(array)
    # 初始值
    yes, no = 0, 0
    for i in range(n):
#         no = max(yes, no) 不对,他们两个应该是同时变化
#         yes = no + array[i]
        yes, no = no + array[i], max(yes, no)
    return max(yes, no)

变形,现在这些银行排成一个圆环该如何做

如果是一个圆环的话,我们可以再上述的情况下,加一下思考:圆环我们肯定是要选择一个起点和终点的,只不过选择了起点就不能选择终点了,f(n)分成两种情况:1、选择起点,剩下的n-1,就和非变形的一样了,f(1, n-1); 2、不选择起点,那么可以选择第二个元素f(2, n-1)

def rob_round(array):
    def robe(array):
        n = len(array)
        # 初始值
        yes, no = 0, 0
        for i in range(n):
            yes, no = no + array[i], max(yes, no)
        return max(yes, no)
    return max(robe(array[:-1]), robe(array[1:]))
def rob(nums):
    if len(nums) == 0:
        return 0

    if len(nums) == 1:
        return nums[0]

    return max(robRange(nums, 0, len(nums) - 1),\
               robRange(nums, 1, len(nums)))

def robRange(nums, start, end):
    yes, no = nums[start], 0
    for i in range(start + 1, end): 
        no, yes = max(no, yes), i + no
    return max(no, yes)

3、给定一块n2的地板,瓷砖的大小为12.计算需要使用的瓷砖数量

瓷砖可以水平放置:12,也可以竖直放置:21
提示:斐波那契

4、有一个楼梯,每次可以走1层或者2层,cost数组表示每一层做需要花费的值

你可以从第0阶或者从第1阶开始走,一旦您交了过路费,您可以上一个或者两个台阶。计算登顶的最少花费
比如,现在每个台阶的cost为
3 5 2 6 7
min: 3 5 5 11 12

使用动态规划我们需要一个递推公式,到达某一层所需要话费的最小值他的前一步只有两种情况:1、前一步是在前一个台阶;2、前一步是在前两个台阶,因此:dp[i] = cost[i] + dp[i-1]+dp[i-2],dp[i]表示的是上到第i个台阶后在向上走一个或两个台阶的最小花费

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        n = len(cost)
        dp = [0] * (n)
        dp[0] = cost[0]
        dp[1] = cost[1]
        # dp[i]表示的是到达这一个台阶后再网上上一层或者两层所需要的最少cost
        for i in range(2, n):
            dp[i] = cost[i]+min(dp[i-1], dp[i-2])
        # 因为顶层是在cost之外的,所以可以从前一个台阶走或者前两个台阶走,关键是谁小
        return min(dp[n-1],dp[n-2])

5、一段包含着A-Z的短信用以下方式进行编码:

A=1
B=2

Z=26
给定一段编码的短信,计算解码的方式:

比如:123 解码:1,2,3;12,3;1,23
109 解码:10,9没有其他的了,因为0不能单独出去

我们可以换个思路,如果编码方式不是1-26,而是0-99,那么这个其实又是一个斐波那契数列的问题了,12344503 :最后开始,可能是由1个数字或者两个数字,因此f(n) = f(n-1)+f(n-2);但是我们现在是在原本的斐波那契数列上又加上了条件范围,1-26,我们从最后开始,能娶到f(n-2)必须最后两个数字在10-26之间;能取到f(n-1)必须最后一个数字是1-9之间,因此,我们可以用if

def numDecoding(s):
    if s == '' or s[0] == 0:
        return o
    # 初始值,字符串为1位和两位的情况下都只能分解成1种情况
    dp = [1, 1]
    for i in range(2, len(s)+1):
        result = 0
        if int(s[i-1]) != 0:
            result += dp[i-1]
        if 10 <= int(s[i-2:i]) <= 26:
            result += dp[i-2]
        dp.append(result)
    
    return dp[len(s)]

6、独特二叉树搜索路径(卡特兰数)(与斐波那契一样有名,可以变形为很多题)

二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。

就是说左<根<右。
在这里插入图片描述

# 卡特兰数
def numTrees(n):
    sol = [0] * (n+1)
    sol[0] = sol[1] = 1
    if n <= 2:
        return n
    for i in range(2, n+1):
        for left in range(1, i+1):
            sol[i] += sol[left - 1] *sol[i - left]
    return sol[n]

这个题可以变形为好多:
比如:打印括号
1:()
2:(()),()()
3、()()(),(())(),()(()),((())),(()())
另外:栈的进栈与出栈有多少种方式,也是卡特兰数,我们可以将其与打印括号进行对比,左括号代表进栈,右括号代表出栈
1:只有数字1:+1-1
2:数字1,2:+1+2-2-1, +1-1+2-2
3:数字1,2,3:+1-1+2-2+3-3,+1+2-2-1+3-3,+1-1+2+3-3-2,+1+2+3-3-2-1,+1+2-2+3-3-1

7、连续子数组的最大和PK连续子数组的最大乘积

1、连续子数组的最大和
输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
设dp[i]为以nums[i]为结尾的连续子数组的最大和
如果dp[i-1]>0, 则dp[i] = dp[i-1]+nums[i]
如果dp[i-1]<0,则dp[i] = nums[i]

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n = len(nums)
        ret = [0]*n 
        ret[0] = nums[0]
        for i in range(1, n):
            if ret[i-1] >= 0:
                ret[i] = ret[i-1]+nums[i]
            else:
                ret[i] = nums[i]
        return max(ret)

这样做会额外增加了O(N)的空间复杂度,因此问可以不用引入ret列表,可以直接在原数据列表上进行操作:

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        for i in range(1, len(nums)):
            nums[i] = max(nums[i-1],0) + nums[i]
        return max(nums)

2、连续子数组的最大乘积
我刚是写成这样,看来好几遍不知道为什么会报错

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        # max[i]表示以当前节点为终点的最大子乘积
        # min[i]表示以当前节点为终点的最小子乘积
        # max[i] = max(max[i-1]*nums[i], min[i-1]*nums[i], nums[i]) 
        # min[i] = min(max[i-1]*nums[i], min[i-1]*nums[i], nums[i])
        # 因为是整数数组,没有分数,所以除了负数和0之外,不会出现越乘越小的情况
        # 如果nums[0]=0,最大值就是0
        maxinum, mininum = nums[0], nums[0]
        result = nums[0]
        n = len(nums)
        for i in range(1, n):
            maxinum = max(maxinum*nums[i], mininum*nums[i], nums[i])
            mininum = min(maxinum*nums[i], mininum*nums[i], nums[i])
            # 将中间的某个最大数保存起来
            result = max(result,maxinum)

        return result

这样写会报错,原因是max与min应该同时变化的,你先将max变了,你的min里面有max的计算公式的

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        # max[i]表示以当前节点为终点的最大子乘积
        # min[i]表示以当前节点为终点的最小子乘积
        # max[i] = max(max[i-1]*nums[i], min[i-1]*nums[i], nums[i]) 
        # min[i] = min(max[i-1]*nums[i], min[i-1]*nums[i], nums[i])
        # 因为是整数数组,没有分数,所以除了负数和0之外,不会出现越乘越小的情况
        # 如果nums[0]=0,最大值就是0
        maxinum, mininum = nums[0], nums[0]
        result = nums[0]
        n = len(nums)
        for i in range(1, n):
            maxinum, mininum = max(maxinum*nums[i], mininum*nums[i], nums[i]), min(maxinum*nums[i], mininum*nums[i], nums[i])
            # 将中间的某个最大数保存起来
            result = max(result,maxinum)
        return result

8 买卖股票1(找到一个数组中相差的最大数,小的在前面,大的在后面)

给定一个数组,表示每天的股票价格
你可以进行一次交易(先买后卖),问如何能够得到最大利润
8 1 3 4 7 3 4
最大利润为6,买1卖7

方法1:常规方法

记录一个列表中的最小值,不断更新
记录列表中的最大利润,不断更新
初始化:将第一个数作为最小值,将0设为最大利润
第二步:for循环,如果后面的一个数比记录的最小值小,更新最小值;如果更新完最小值,最大利润比0大,更新最大利润

# 最大利润
def maxProfit(array):
    if len(array) < 2:
        return 0
    minPrice = array[0]
    maxProfit = 0
    for price in array:
        if price < minPrice:
            minPrice = price
        if price - minPrice > maxProfit:
            maxProfit = price - minPrice
    return maxProfit
方法2:动态规划

设dp[i]为遍历到当前数字时候的最大利润,那么
dp[i] = max(dp[i-1], array[i] - minPrice)
minPrice = min(minPrice, dp[i])

def maxProfit(array):
    n = len(array)
    if n<2:
        return 0
    dp = [0] * n
    minPrice = array[0]
    for i in range(1, n):
        dp[i] = max(dp[i-1], array[i] - minPrice)
        minPrice = min(minPrice, array[i])
    return dp[-1]

但是这样他的空间复杂度会比较高,为O(n)因为引入了一个数组dp,为了降低复杂度,课有不要那个数组,如下所示

def maxProfit(array):
    n = len(array)
    if n<2:
        return 0
    # dp = [0] * n
    dp = 0
    minPrice = array[0]
    for i in range(1, n):
        dp = max(dp, array[i] - minPrice)
        minPrice = min(minPrice, array[i])
    return dp

9 买卖股票2:现在你可以任意多次的买卖

给定一个数组,表示每天的股票价格
你可以进行一次交易(先买后卖),问如何能够得到最大利润

[7, 1, 5, 6] 第二天买入,第四天卖出,收益最大(6-1),所以一般人可能会想,怎么判断不是第三天就卖出了呢? 这里就把问题复杂化了,根据题目的意思,当天卖出以后,当天还可以买入,所以其实可以第三天卖出,第三天买入,第四天又卖出((5-1)+ (6-5) === 6 - 1)。所以算法可以直接简化为只要今天比昨天大,就卖出。

我原本一直在想,为什么今天比昨天大就可以卖了,原来是因为买卖可以同一天,不要将问题复杂化了

def maxProfit(array):
    if len(array) < 2:
        return 0
    maxProfit = 0
    for i in range(1, len(array)):
        if array[i] > array[i - 1]:
            maxProfit += array[i] - array[i - 1]            
    return maxProfit
def maxProfit(self, array: List[int]) -> int:
    if len(array) < 2:
        return 0
    maxProfit = 0
    for i in range(1, len(array)):
        maxProfit += max(0, array[i] - array[i - 1])           
    return maxProfit

9 买卖股票3:现在你可以任意多次的买卖,但是你在买入的时候需要交手续费

因此你的买卖次数最好尽可能的少

cash:当前手中不持有股票的最大收益,他的前一天可能是cash或者hold,如果前一天是cash,今天要获得最大收益,肯定不会再买股票了,因此cash会继续保留;如果前一天是hold,今天要获得最大收益我需要将股票卖了,因此hold + price[i] - fee

    # hold:当前手中持有股票的最大收益,他的前一天可能是hold,因为今天是hold,所以不可能卖了;如果前一天是cash,今天是hold,所以我得买点股票,cash - price[i]
    # cash[i] = max(cash[i-1], hold[i-1]+price[i]-fee)
    # hold[i] = max(hold[i-1], cash[i-1]-price[i])
class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        cash, hold = 0, -prices[0]
        for i in range(len(prices)):
            cash = max(cash, hold+prices[i]-fee)
            hold = max(hold, cash-prices[i])
        return max(cash, hold)

注意上面的cash和hold可以同时变,也可以不用,因为不同时改变时,即使cash = hold+prices[i]-fee,你带入到第二个式子当中,结果依旧不变

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值