剪绳子 算法_[校招-算法题]动态规划

动态规划(Dynamic Programming)是面试中非常常见的一种算法,可以解决很多复杂问题。

核心算法:

  1. 定义状态:dp[i],一个数组,具体几维根据问题定。
  2. 状态转移方程:dp[i] = best(dp[i-1], dp[i-2], ...)。
  3. 最优子结构。
  4. 递归+记忆化。

动态规划的适用条件

  • 最优子结构性质。一个最优化策略的子策略一定是最优的。
  • 无后向性。可以理解为每个状态都是过去历史状态的完整总结。
  • 子问题的重叠性。这不是DP的必要条件,但是如果不满足,则DP相对其他算法没有优势。因为DP的关键在于解决冗余,存储过程中的各种状态,用空间换时间。

斐波那契数列

f(0) = 0, f(1) = 1, 当n>=2时, f(n) = f(n-1) + f(n-2) 输入n,求f(n)。

比较常见的问题,因此四种做法都能顺利写出来,并分析时间复杂度很重要。

朴素的递推, 时间复杂度O(2^n),空间复杂度?

def fibo(n):
    if n <= 1:
        return n
    return fibo(n-1) + fibo(n-2)

动态规划, 时间复杂度O(n),空间复杂度O(n)

def fibo(n):
    if n <= 1:
        return n
    dp = [0] * (n+1)
    dp[0], dp[1] = 0, 1
    for i in range(2, n+1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]

改进动态规划,基于循环, 时间复杂度O(n),空间复杂度O(1)

def fibo(n):
    if n <= 1:
        return n
    first_number = 0
    second_number = 1
    for _ in range(n):
        first_number, second_number = second_number, first_number+second_number
    return first_number

基于矩阵乘法,时间复杂度O(logn)。

将斐波那契数列公式写成矩阵相乘格式:[[f(n), f(n-1)], [f(n-1), f(n-2)] = ([[1,1], [1,0]]) ^ (n-1)。直接求乘方也是O(n)的时间复杂度,但是可以用递归的思路简化。

零钱兑换1

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

解法:

状态方程dp[i]代表总金额为i所对应的最少硬币个数,初始状态dp[0] = 0

状态转移方程:

; 不存在的为-1。
def coin_change(coins, amount):
    dp = [0xffffff] * (amount + 1)
    dp[0] = 0 # 初始状态,总金额为0,硬币数为0
    for i in range(amount + 1): # 外层循环是总金额
        for coin in coins:
            if i >= coin and dp[i - coin] < dp[i] - 1:
                dp[i] = dp[i - coin] + 1
    if dp[amount] != 0xffffff:
        return dp[amount]
    else:
        return -1

零钱兑换2

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

解法:

状态方程dp[i]代表总金额为i所对应的硬币组合数;

状态转移方程:

,k为硬币面值个数,注意
i要大于等于j
def change(amount, coins):
    dp = [0] * (amount + 1)
    dp[0] = 1 # 初始状态,总金额为零,组合数为1(所有硬币数都为0)
    for coin in coins: # 外层循环是硬币,用前面面值的硬币组成i的组合数
        for i in range(1, amount + 1): 
            if i >= coin:
                dp[i] = dp[i] + dp[i - coin]
    return dp[amount]

二维矩阵求路径次数

简单版

一个
的二维矩阵,从左上角走到右下角,每次只能往右走或往左走,那么到达右下角总共有多少种走法?

解法1:数学排列组合问题。总共有m次向下走,n次向右走,无论哪种路径都是m+n步到达,总的走法有

种。

解法2:动态规划。到达一个点的路径总数=到达上面一个点的路径总和+到达左边一个点的路径总和。

def path_number(m,n):
    """
    m+1行,n=1列
    """
    dp = [0] * (m+1)
    for i in range(m+1):
        dp[i] = [0] * (n+1)
    dp[0][0] = 1
    for i in range(0, m+1):
        for j in range(0, n+1):
            if i == 0 and j == 0:
                continue
            dp[i][j] = dp[i-1][j] * (i-1 >= 0) + dp[i][j-1] * (j-1 >= 0)
    #print(dp)
    return dp[m][n]

print(path_number(3,3)) # 20

稍微复杂一点

在上面的基础上加上一个条件,矩阵中某些地方不能经过。

只需要在状态转移的时候判断一下,如果当前格点处是不能经过的,那么dp[i][j] = 0;否者,dp[i][j]= dp[i-1][j]*(i-1>=0)+ dp[i][j-1]*(j-1>=0)。

剪绳子

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

解法一:动态规划。时间复杂度O(n^2),空间复杂度O(n)。灵活运用动态规划的关键是具备从上到下分析问题,并且从下到上解决问题的能力。

def max_product( n):
    """动态规划"""
    if n < 2:
        return 0
    elif n == 2: # 注意,至少要剪成两段
        return 1
    elif n == 3:
        return 2

    products = [0] * (n + 1)
    for i in range(4):
        products[i] = i

    for i in range(4, n + 1):
        max_val = 0
        for j in range(1, i // 2 + 1):
            product = products[j] * products[i - j]
            if max_val < product:
                max_val = product
                products[i] = max_val
    #print(products)
    return products[n]

解法二:贪婪算法。时间复杂度和空间复杂度均为O(n)。

数学上可以证明,当n>=5时,尽可能多的剪长度为3的绳子,且最后一段绳子如果是4的话,把它剪成2+2的两段。

证明:当n >= 5时,下列不等式恒成立 3(n-3) >= 2(n-2) > n。 因此,当n大于等于5时,尽可能剪成长度为3或2的小段,并且尽可能剪成长度为3的小段。并且当n=4时,剪成2+2比1+3更好。证毕!

def max_product2(n):
    """贪婪算法"""
    if n < 2:
        return 0
    elif n == 2:
        return 1
    elif n == 3:
        return 2

    times_of_2 = 0
    times_of_3 = 0

    times_of_3 = n // 3
    if n % 3 == 1:
        times_of_3 -= 1
    times_of_2 = (n - times_of_3 * 3) / 2

    return int(3 ** times_of_3 * 2 ** times_of_2)

三角形的最小路径和

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

从后往前递推,也就是从三角形的最下面向上递推。

状态方程定义:dp[i,j],指的是从底部递推到[i,j]位置的最小路径值。

状态转移方程:dp[i,j] = min(dp[i+1, j], dp[i=1, j+1]) + triangle[i, j]。

def minimumTotal(triangle):
    n = len(triangle)
    if n == 1:
        return max(triangle[0])
    dp = triangle
    for i in range(n-2, -1, -1):
        for j, val in enumerate(triangle[i]):
            dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + val
    return dp[0][0]

连续子数组最大和

解法见这篇文章

Jack Stark:[校招-算法题]子序列问题1​zhuanlan.zhihu.com

最长递增子序列

解法见这篇文章

Jack Stark:[校招-算法题]子序列问题1​zhuanlan.zhihu.com

乘积最大子序列

给定一个整数数组 nums ,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。

解法:这道题需要两个一维的状态数组(或者一个二维的状态数组),

状态定义:

  • dp_max[i]:nums[i]对应的最大的连续子数组乘积,第0个元素为nums[0]
  • dp_min[i]:nums[i]对应的最小的连续子数组乘积,第0个元素为nums[0]

状态转移方程:

  • dp_max[i]=max(dp_max[i -1]* nums[i], dp_min[i -1]* nums[i], nums[i])
  • dp_min[i]=min(dp_max[i -1]* nums[i], dp_min[i -1]* nums[i], nums[i])

最后返回dp_max的最大值

def max_product(nums):
    if len(nums) == 1:
        return nums[0]
    dp_max = [0] * len(nums)
    dp_min = [0] * len(nums)
    dp_max[0], dp_min[0], res = nums[0], nums[0], nums[0]
    for i in range(1, len(nums)):
        dp_max[i] = max(dp_max[i - 1] * nums[i], dp_min[i - 1] * nums[i], nums[i])
        dp_min[i] = min(dp_max[i - 1] * nums[i], dp_min[i - 1] * nums[i], nums[i])
        res = max(res, dp_max[i])
    return res

优化空间复杂度后代码为:

def max_product(nums):
    if len(nums) == 1:
        return nums[0]
    cur_max, cur_min, res = nums[0], nums[0], nums[0]
    for num in nums[1:]:
        temp_max, temp_min = cur_max * num, cur_min * num
        cur_max = max(temp_max, temp_min, num)
        cur_min = min(temp_max, temp_min, num)
        res = max(res, cur_max)
        print(cur_max, cur_min)
    return res

编辑距离问题

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。可以对word1进行如下三种操作:插入一个字符、删除一个字符、替换一个字符。

解法:这个问题的难点在于定义状态数组。下面的i, j是从1开始数的。定义dp[i][j]为word1前i个字符转换成word2前j个字符的最少步数。然后状态转移方程有四种情况,一种是当前word1的第i个元素等于word2的第j个元素,那么这一步不需要任何操作;或者等于进行删除、插入和替换这三种情况的最小值然后加一(加1是因为做操作了)。

def min_distance(word1, word2):
    m = len(word1)
    n = len(word2)
    dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]  # dp[i][j]表示word1前i个字符转换成word2前j个字符的最少步数
    for i in range(m + 1): dp[i][0] = i  # word1前i个字符转换为长度为0的字符,全部删除即可
    for j in range(n + 1): dp[0][j] = j  # word1是空字符,转换为长度为j的字符,逐个插入即可
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if word1[i - 1] == word2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]
            else:
                dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])  # delete, insert, replace
    return dp[m][n]
print(min_distance("horse", "ros")) # 3
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值