LeetCode 剑指Offer 数据结构之 动态规划 总结 Part1

LeetCode 剑指Offer 数据结构之 动态规划 总结 Part1

动态规划问题有着鲜明的特点,往往以求最大值,搜索最优结果为目的。在剑指Offer中有6道题。构建问题时,可以先确定问题的动态规划状态空间的维度,是一维,还是二维。然后确定状态空间的定义,更新保留是全局最优,还是更新保留一端,最后确定边界条件和状态转移方程,状态转移方程往往是多条件分支的,条件的判断涉及到一些其他的数据结构知识。多数情况下以上基础的动态规划建立后,可以进行状态空间和转移方程上的优化,尽量将状态空间优化为常量进行更新,转移方程尽量将多条件进行归并。

剑指 Offer 63:

股票的最大利润
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

这和求子串长度比较一致,涉及到一个序列中一个字符串的起点,终点。一般将状态空间定义为终点相关,起点用于状态转移的条件判断:

首先这是一维序列,就不要想去做二维的dp空间,买入点相对好确定着去更新,卖出点是要dp更新的

dp[i]=max(dp[i-1],i天卖出的最大利益) 

dp空间内的值总表示过去到现在的最xxx值 本题可以设为全局的当前最大利润
i天卖出的最大利益 的表征:

price[i]-min(price[:i])

所以得出状态转移方程:

df[i]=max(df[i-1],price[i]-min(price[:i]))

空间优化:一维的dp 如果递推公式只和自己i-1有关,可以用一个值替代dp空间

pf = max(pf,price[i]-min(price[:i]))

时间优化:因为每次都算price[:i],可以用一个动态更新的维护的mincost 来代替每次计算过去所有,只要每次更新用当前值和mincost做更新就可以

pf = max(pf,max(price - cost))
def maxProfit(self, prices):
   cost, profit = float("+inf"), 0
   for i in range(len(prices)):
       cost = min(cost, prices[i])
       profit = max(profit, prices[i] - cost)
   return profit
剑指 Offer 47:

礼物的最大价值
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

示例 1:

输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物

问题是二维的,所以建立二维dp空间,dp[i][j]表示走到(i,j)能拿到的最值,全局量,去不断更新,状态转移方程分为边界和中间两部分

def maxValue0(self, grid):
    if not grid:
        return 0
    row_num = len(grid)
    col_num = len(grid[0])
    df = [[0] * col_num for i in range(row_num)]
    for i in range(row_num):
        for j in range(col_num):
            if i == 0 and j == 0:
                df[i][j] = grid[i][j]
            elif i == 0:
                df[i][j] = df[i][j - 1] + grid[i][j]
            elif j == 0:
                df[i][j] = df[i - 1][j] + grid[i][j]
            else:
                df[i][j] = max(df[i - 1][j], df[i][j - 1]) + grid[i][j]
    return df[row_num - 1][col_num - 1]

空间优化:如果dp空间的递推公式只与自身,以及同维度的入参空间相关,则可以不新建dp空间,就在入参的数据上更新
时间优化:边界的循环可以提出来,双层循环里的边界可以拿出来

## 空间优化
def maxValue1(self, gr
id):
    for i in range(len(grid)):
        for j in range(len(grid[0])):
            if i == 0 and j == 0: continue
            if i == 0:
                grid[i][j] += grid[i][j - 1]
            elif j == 0:
                grid[i][j] += grid[i - 1][j]
            else:
                grid[i][j] += max(grid[i][j - 1], grid[i - 1][j])
    return grid[-1][-1]

## 时间优化
def maxValue2(self, grid):
    m, n = len(grid), len(grid[0])
    for j in range(1, n):  # 初始化第一行
        grid[0][j] += grid[0][j - 1]
    for i in range(1, m):  # 初始化第一列
        grid[i][0] += grid[i - 1][0]
    for i in range(1, m):
        for j in range(1, n):
            grid[i][j] += max(grid[i][j - 1], grid[i - 1][j])
    return grid[-1][-1]
剑指 Offer 42:

连续子数组的最大和
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

示例1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

状态空间如果定义为子数组右侧结尾的全局最大,显然dp[j]与dp[j-1]没有联系。所以dp[j]定义为子数组右侧为num[j]时的和最大子数组值
这样dp[j]与dp[j-1]的关系为:
当dp[j-1]<0 显然dp[j]应该是nums[j]
当dp[j-1]>=0 显然dp[j]应该是nums[j]+dp[j-1]

又因为状态转移只和本身j-1有关,可以在原序列上更新

def maxSubArray(self, nums):
    '''
    f(n+1) 只与 f(n) 与 nums[n+1]有关
    只要nums[n+1]>0 则f(n)必然比f(n+1)小
    :param nums:
    :return:
    '''
    for i in range(1, len(nums)):
        nums[i] += max(nums[i - 1], 0)# 就地累加修改!不再占用空间了!
    return max(nums)# 最后返回全局!
剑指 Offer 14- II:

剪绳子 II
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m - 1] 。请问 k[0]k[1]…*k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

dp[i]表示的是长度为i的绳子能得到的最大乘积
我们先把长度为i的绳子拆成两部分,一部分是j,另一部分是i-j,那么会有下面4种情况
1,最优情况下:j和i-j都不能再拆了

dp[i]=j*(i-j);
2,最优情况下:j能拆,i-j不能拆

dp[i]=dp[j]*(i-j);
3,最优情况下:j不能拆,i-j能拆

dp[i]=j*dp[i-j];
4,最优情况下:j和i-j都能拆

dp[i]=dp[j]*dp[i-j];
我们取上面4种情况的最大值即可。我们把它整理一下,得到递推公式如下

dp[i] = max(dp[i], (max(j, dp[j])) * (max(i - j, dp[i - j])));

注意这个取最优的归并

def cuttingRope(self, n: int) -> int:
    dp = [0 for _ in range(n + 1)]  # dp[0] dp[1]其实没用
    dp[2] = 1  # 初始化
    for i in range(3, n + 1):
        for j in range(i):
            dp[i] = max(dp[i], max((i - j) * j, j * dp[i - j]))
    return dp[n]%1000000007# 防止大数越界

总结:动态规划问题的状态转移 需要进行很多的条件判断与套路,最后还需要进行归并与优化。常见的有条件判断,多种可能max取最优归并等……
下面part2将对一道hard难度的动态规划进行总结,主要是状态转移讨论的部分需要仔细梳理……

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值