【LeetCode】动态规划相关问题

态规划问题的一般形式就是求最值,求解动态规划的核心问题是穷举。

动态规划三要素:重叠子问题、最优子结构、状态转移方程

转态转移方程:明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。

# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 求最值(选择1,选择2...)
一、斐波那契数列

1、暴力递归

int fib(int N) {
    if (N == 1 || N == 2) return 1;
    return fib(N - 1) + fib(N - 2);
}

2、带备忘录的递归解法

int fib(int N) {
    if (N < 1) return 0;
    // 备忘录全初始化为 0
    vector<int> memo(N + 1, 0);
    // 进行带备忘录的递归
    return helper(memo, N);
}
 
int helper(vector<int>& memo, int n) {
    // base case 
    if (n == 1 || n == 2) return 1;
    // 已经计算过
    if (memo[n] != 0) return memo[n];
    memo[n] = helper(memo, n - 1) + helper(memo, n - 2);
    return memo[n];
}

3、dp 数组的迭代解法

int fib(int N) {
    vector<int> dp(N + 1, 0);
    // base case
    dp[1] = dp[2] = 1;
    for (int i = 3; i <= N; i++)
        dp[i] = dp[i - 1] + dp[i - 2];
    return dp[N];
}

4、细心的读者会发现,根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1)

int fib(int n) {
    if (n == 2 || n == 1) 
        return 1;
    int prev = 1, curr = 1;
    for (int i = 3; i <= n; i++) {
        int sum = prev + curr;
        prev = curr;
        curr = sum;
    }
    return curr;
}
二、凑零钱问题

具体解析参考LeetCode
在这里插入图片描述
在这里插入图片描述

# 伪码框架
def coinChange(coins: List[int], amount: int):

    # 定义:要凑出金额 n,至少要 dp(n)***
    def dp(n):
        # 做选择,选择需要***最少的那个结果
        for coin in coins:
            res = min(res, 1 + dp(n - coin))
        return res

    # 题目要求的最终结果是 dp(amount)
    return dp(amount)

根据伪码,我们加上 base case 即可得到最终的答案。显然目标金额为 0 时,所需***数量为 0;当目标金额小于 0 时,无解,返回 -1:

def coinChange(coins: List[int], amount: int):

    def dp(n):
        # base case
        if n == 0: return 0
        if n < 0: return -1
        # 求最小值,所以初始化为正无穷
        res = float('INF')
        for coin in coins:
            subproblem = dp(n - coin)
            # 子问题无解,跳过
            if subproblem == -1: continue
            res = min(res, 1 + subproblem)

        return res if res != float('INF') else -1
    
    return dp(amount)

状态转移方程
在这里插入图片描述


面试题 08.11. 硬币

在这里插入图片描述

class Solution(object):
    def waysToChange(self, n):
        """
        :type n: int
        :rtype: int
        """
        mod = 10**9 + 7
        coins = [25, 10, 5, 1]

        f = [1] + [0] * n
        for coin in coins:
            for i in range(coin, n + 1):
                f[i] += f[i - coin]  # 记录最优解
        return f[n] % mod
322.兑换零钱

在这里插入图片描述

class Solution:
    def coinChange(self, coins, amount: int) -> int:
        dp = [float('inf')] * (amount + 1)
        dp[0] = 0

        for coin in coins:
            for x in range(coin, amount + 1):
                print('x:',x,'dp[x - coin]:',dp[x - coin])

                dp[x] = min(dp[x], dp[x - coin] + 1)
               
        return dp[amount] if dp[amount] != float('inf') else -1

560. 和为K的子数组

在这里插入图片描述

class Solution(object):
    def subarraySum(self, nums, k):
        """
        :type nums: List[int]
        :type k: int
        :rtype: int
        """
        pre,count = 0,0
        dic = {0:1}
        for i in nums:
            pre += i
            if pre-k in dic:
                count += dic[pre-k]
            dic[pre] = dic.get(pre,0)+1

        return count
64. 最小路径和

在这里插入图片描述

class Solution(object):
    def minPathSum(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """ 
        dp = [[0 for i in range(len(grid[0]))] for j in range(len(grid))]

        for i in range(len(grid)):
            for j in range(len(grid[0])):

                if i==0 and j==0:
                    dp[i][j] = grid[0][0]
                
                elif j == 0 and i != 0:
                    dp[i][j] = dp[i-1][j] + grid[i][j]
                
                elif i == 0 and j != 0:
                    dp[i][j] = dp[i][j-1] + grid[i][j]
                
                else:
                    dp[i][j] = min(dp[i][j-1] ,dp[i-1][j]) + grid[i][j]  # 较小的加上当前的单元格

        return dp[len(grid)-1][len(grid[0])-1]

63. 不同路径 II

在这里插入图片描述

class Solution(object):
    def uniquePathsWithObstacles(self, obstacleGrid):
        """
        :type obstacleGrid: List[List[int]]
        :rtype: int
        """
        if not obstacleGrid or obstacleGrid[0][0] ==1:
            return 0
        n = len(obstacleGrid[0])
        m = len(obstacleGrid)
        dp = [[0 for i in range(len(obstacleGrid[0]))] for j in range(len(obstacleGrid))]

        for i in range(m):
            if obstacleGrid[i][0] == 1:
                break
            
            dp[i][0] = 1

        for j in range(n):
            if obstacleGrid[0][j] == 1:
                break
            dp[0][j] = 1
       
        for i in range(1,len(obstacleGrid)):
            for j in range(1,len(obstacleGrid[0])):
    
                if obstacleGrid[i][j] == 1:
                    dp[i][j] = 0    #遇到障碍物
                else:
                    dp[i][j] = dp[i-1][j] + dp[i][j-1]
        return dp[m-1][n-1]

62. 不同路径

在这里插入图片描述

class Solution(object):
    def uniquePaths(self, m, n):
        res = [[1 for i in range(n)] for j in range(m) ]
        count = 0
        for i in range(m):
            for j in range(n):
                if i ==0 or j ==0:
                    continue
                res[i][j] = res[i-1][j] + res[i][j-1]
        return res[m-1][n-1]

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

'''
递归法:剪一刀下去,就会产生1,2,3...n-1个值,因此只需要求出f(i)*f(n-i)的最大值即可
'''
class Solution:
    def cutRope(self, number):
        # write code here
        if number < 2:
            return 0
        if number == 2:
            return 1
        if number == 3:
            return 2
        return self.cutRopelong(number)

    def cutRopelong(self, number):
        if number < 4:
            return number
        maxnum = 0
        for i in range(1, number // 2 + 1):
            maxnum = max(self.cutRopelong(i) * self.cutRopelong(number - i), maxnum)
        return maxnum

300.最长上升子序列
'''
用dp[i] 表示 从下标0 到下标i 的最长上升子序列的长度,
例如对于样例输入[10,9,2,5,3,7,101,18], 有 dp = [ 1, 1, 1, 2, 2, 3, 4, 4]。
显然dp[0] = 1
对于任意的i 不为零的情况,应该在 i 的左侧找一个下标 j ,其满足两个条件:

1. nums[ j ]比 nums[ i ] 小
2. 它是所有满足条件1里 dp [j]  最大的那个
dp[i] = max(dp[j]) + 1 , j < i and nums[ j ] < nums[ i ]

如果不存在这样的下标j,说明在0 ~ i - 1 的范围内,所有元素都比nums[i] 大,即无法找到一个能和 nums[i] 组成上升子序列的元素,
所以dp[i] = 1, 表示为nums[i] 自身成为一个长度为1 的上升子序列。

'''

class Solution(object):
    def lengthOfLIS(self, nums):
        if not nums:
            return 0
        a = [1 for i in range(len(nums))]  #单个元素的时候,最长序列为1,保存序列的长度
        for i in range(1,len(nums)):
            for j in range(0,i):
                if nums[i] > nums[j]:
                    a[i] = max(a[j] + 1, a[i])  # i前边有j个元素的时候,需要确保a[i]的值能取到最大

        return max(a)

最长公共字符串/子序列

# 最长公共字符串
def LCstring(string1,string2):
    len1 = len(string1)
    len2 = len(string2)
    res = [[0 for i in range(len1+1)] for j in range(len2+1)]
    result = 0
    for i in range(1,len2+1):
        for j in range(1,len1+1):
            if string2[i-1] == string1[j-1]:
                res[i][j] = res[i-1][j-1]+1
                result = max(result,res[i][j])
    return result
# print(LCstring("helloworld","looper"))
# 输出结果为:2

'''
最长公共子序列的长度,比如 hello 和 eillok 的最长公共子序列是4,即ello
'''
'''
用二维数组res[i][j]来记录公共序列的长度,共有三种可能:
当i=0或j=0时,res[i][j] = 0
当A[i] = B[j]的时候 res[i][j]= res[i-1][j-1] + 1
当A[i] != B[j]的时候,res[i][j] = max(res[i-1][j],res[i][j-1])
'''

def LCS2(string1,string2):
    len1 = len(string1)
    len2 = len(string2)
    res = [[0 for i in range(len1+1)] for j in range(len2+1)]
    for i in range(1,len2+1):
        for j in range(1,len1+1):
            if string2[i-1] == string1[j-1]:
                res[i][j] = res[i-1][j-1]+1
            else:
                res[i][j] = max(res[i-1][j],res[i][j-1])
    return res[-1][-1]
print(LCS2("helloworld","loop"))

数组中不相邻元素最大和

'''
给定一个只含正数的数组,找到数组满足条件的元素的最大和,条件是:组成最大和的所有元素不能相邻,比如数组 [3,2,7,10] 返回 13(3+10),数组 [3,2,5,10,7] 返回(3+5+7)

分析,这是一道典型的使用动态规划求解的题目(当然能够使用万能的回溯法,但显然不是很棒),最优子结构为:f(i)=max{f(i - 2) + A[i], f(i - 1)}

解释一下,对于第i位,如果选择A[i],则不能选择i-1位,因此有f(i - 2) + A[i],如果不选择A[i],则将该问题放缩到i-1,因此有f(i-1)。
对于边界情况,f(0) = A[0],f(1) = max{A[0], A[1]}
'''
def no_f(B):
    opt = [0 for i in range(len(B))]
    opt[0] = B[0]
    opt[1] = max(B[1],B[0])
    for i in range(2,len(B)):
        opt[i] = max(B[i]+opt[i-2],opt[i-1])
    return opt[len(B)-1]

编辑距离

def edit_distance(str1, str2):
    # 初始化矩阵
    matrix = [[i+j for j in range(len(str2) + 1)] for i in range(len(str1) + 1)]
    for i in range(1,len(str1)+1):
        for j in range(1,len(str2)+1):
            if str1[i-1] == str2[j-1]:
                d = 0
            else:
                d = 1
            # 用d[i-1,j]+1表示增加操作
            # d[i,j-1]+1 表示我们的删除操作
            # d[i-1,j-1]+temp表示我们的替换操作
            matrix[i][j] = min(matrix[i-1][j]+1,matrix[i][j-1]+1,matrix[i-1][j-1]+d)

    return matrix[len(str1)][len(str2)]

print(edit_distance('','xwrs'))

120、三角形最小和路径

在这里插入图片描述

'''
时间复杂度:O(n^2),其中n是三角形的行数。
空间复杂度:O(n^2)我们需要一个n*n的二维数组存放所有的状态。

'''

class Solution:
    def minimumTotal(self, triangle):
        n = len(triangle)
        f = [[0] * n for _ in range(n)]
        f[0][0] = triangle[0][0]

        for i in range(1, n):

            f[i][0] = f[i - 1][0] + triangle[i][0]

            for j in range(1, i):
                f[i][j] = min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j]  #

            f[i][i] = f[i - 1][i - 1] + triangle[i][i]  # 每行的最后一个元素

        return min(f[n - 1])

在这里插入图片描述


53、最大子序和

在这里插入图片描述

def maxSubArray(nums):
    sum = 0
    max = nums[0]

    for index in range(len(nums)):
        if sum < 0:
            sum = 0
        sum += nums[index]
        if sum > max:
            max = sum
    return max

print(maxSubArray([-2,-1,-3,-4,-1,-2,-1,-5,-4]))


def maxSubArray1(nums):
    for i in range(1, len(nums)):
        nums[i] = nums[i] + max(nums[i - 1], 0)
    return max(nums)

def maxSubArray2(num):
    dp = [0] * len(num)
    dp[0] = num[0]

    for i in range(1, len(num)):
        dp[i] = max(num[i], dp[i - 1] + num[i])
        # print(dp[i-1]+dp[i])

    return max(dp)

121、买卖股票最佳时期

在这里插入图片描述

def maxProfit(self, prices):
    profit = 0
    mindex = 0
    for i in range(len(prices)):
        if prices[i] < prices[mindex]:
            mindex = i
            
        profit = max(profit,prices[i]-prices[mindex])
    return profit

122、买卖股票最佳时期II

在这里插入图片描述

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        profit = 0
        for i in range(1,len(prices)):
            if prices[i] > prices[i-1]:
                profit += prices[i]-prices[i-1]
        return profit

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值