动态规划与递归实例分析

本文收集了一些能用动态规划或递归进行解答的题目,旨在大家能逐渐理解递归和动态规划的思想,所以这里并不会说明其相关定义,由于博主也是刚刚接触,如果有问题请大家一定要指正,谢谢!

问题1

从一个数组中取出几个数,使其和最大,要求取出的数的位置不能相邻。
以如下数组为例进行分析:

index0123456
arr1241783

1 递归
DP(i)表示第i个数之前的最优解, + 表示选这个数, - 表示不选这个数。
这里写图片描述
比如:对于求DP(6),可以分成两种情况:如果你选第6个数,那么只能从第4之前的数中选最优解再加上第6个数的值;如果不选第6个数,就从第5个数之前选择最优解,然后比较这两种情况谁的值大就是DP(6)的解了,后面的分析是一样的。
递归公式如下:

	DP(i) = max(DP(i-2)+arr[i], DP(i-1))

递归的出口:

	DP(0) = arr[0]
	DP(1) = max(arr[0], arr[1])

下面是代码:

def f(arr, i):
    """递归解法"""
    if i == 0:
        return arr[0]
    if i == 1:
        return max(arr[0], arr[1])
    else:
        choose = f(arr, i-2) + arr[i]
        not_choose = f(arr, i-1)
        return max(choose, not_choose)

2 动态规划
一般来说能用递归就可以用动态规划,只不过数据量大时,递归会非常耗时,因为其时间复杂度是呈指数级增长的。动态规划就是拿空间来换时间。递归是从后往前推,而动态规划则是从前往后推,利用递归公式计算出后面的最优解,并将其保存到数组中。从递归的图中可以看到,很多子问题被重复计算了多次(例如DP(3)等),使用动态规划只计算一次,并将这些值保存起来,要使用时直接从数组中取。
代码如下:

def dp(arr):
    """动态规划解法"""
    # 使用了numpy模块,方便创建数组
    opt = np.zeros(len(arr), dtype=int)
    opt[0] = arr[0]
    opt[1] = max(arr[0], arr[1])
    for i in range(2, len(arr)):
        choose = opt[i-2] + arr[i]
        not_choose = opt[i-1]
        opt[i] = max(choose, not_choose)
    return opt

问题2

从一个数组中选择若干个数,看是否有满足和为指定值的这些数。
依然使用问题一的数组分析,假设指定的和是20:

index0123456
arr1241783

1 递归
递归的思路同问题一是一样的,还是选不选的问题。
用 f(i, s) 表示第 i 个数,和为 s 的情况:
这里写图片描述
那么递归公式很明显了:
f ( a r r , i , s ) = { f ( a r r , i − 1 , s ) , 选 第 i 个 数 f ( a r r , i − 1 , s − a r r [ i ] ) , 不 选 第 i 个 数 f(arr, i, s)=\begin{cases} f(arr, i-1, s), 选第i个数\\ f(arr, i-1, s-arr[i]) ,不选第i个数 \end{cases} f(arr,i,s)={f(arr,i1,s),if(arr,i1,sarr[i]),i
递归的出口:
首先肯定是当s=0的情况,s=0说明有符合条件的数,直接返回True就行了,然后是当i=0的情况,表示这是最后一个数,如果最后一个数等于s说明符合条件,不等就不符合了。这里还有一种情况大家可能不容易发现,就是arr[i]>s,直接跳过这个数据。
出口如下:

 s = 0时, return Ture
 i = 0时, return arr[i] == s
 arr[i] > s时, return f(arr, i-1, s)

递归代码如下:

def f2(arr, i, s):
    """递归解法"""
    # 注意s==0这个条件先于i==0进行判断
    if s == 0:
        return True
    elif i == 0:
        return arr[0] == s
    elif arr[i] > s:
        return f2(arr, i-1, s)
    else:
        choose = f2(arr, i-1, s-arr[i])
        not_choose = f2(arr, i-1, s)
        return choose or not_choose

2 动态规划

def dp2(arr, tag):
    """问题二的动态规划解法"""
    opt = np.zeros((len(arr), tag+1), dtype=bool)
    opt[0, :] = False  
    opt[0, arr[0]] = True  # i = 0时的情况
    opt[:, 0] = True       # s = 0的情况
    for i in range(1, len(arr)):
        for s in range(1, tag+1):
            if arr[i] > s:
                opt[i, s] = opt[i-1, s] 
            choose = opt[i-1, s-arr[i]]
            not_choose = opt[i-1, s]
            opt[i, s] = choose or not_choose
    r, c = opt.shape
    return opt[r-1, c-1]

由于表格太大不方便画出来,下面是求和为8(即tag=8)的数组opt初始值,列表示求和s的值,行表示i的值。动态规划过程从头开始根据递归公式将计算的中间值保存到数组中。

012345678
0TTFFFFFFF
1T
2T
3T
4T
5T
6T

大家可以自己计算,将相应的值填入表格,慢慢理解动态规划这一过程的思想,也可以将函数直接返回数组,看看数组里面的值,如下图所示:
这里写图片描述

问题3:

一根长度为n的绳子,请把绳子剪成m段(m,n都是整数,m>=2),每段绳子的
长度记为k[0],k[1],k[2]…. 请问如何剪绳子使得k[0],k[1],k[2]的乘积最大。
1 递归
以绳长8为例进行分析,因为至少要分成2段,所以可以有如下的分法:
假设f(n)表示绳长为n所求得的最大乘积,
这里写图片描述
那么
f ( 8 ) = m a x { f ( 1 ) ∗ f ( 7 ) f ( 2 ) ∗ f ( 6 ) f ( 3 ) ∗ f ( 5 ) f ( 4 ) ∗ f ( 4 ) f(8)=max\begin{cases} f(1)*f(7)\\ f(2)*f(6)\\ f(3)*f(5)\\ f(4)*f(4)\\ \end{cases} f(8)=maxf(1)f(7)f(2)f(6)f(3)f(5)f(4)f(4)
树形状的第2层(7,6,5,4)表示将绳子分成2段,第3层则表示将绳子分成3段,例如第3层的树节点6,则表示f(8)=f(1)*f(1)*f(6),将绳子分成了3段;依次类推。
递归公式:
f ( n ) = m a x ( f ( i ) ∗ f ( n − i ) ) , n > = 4 , i = [ 1 , 2 , 3 , . . . , n / / 2 ] f(n)=max( f(i)*f(n-i)), n>=4, i=[1, 2, 3,..., n//2] f(n)=max(f(i)f(ni)),n>=4,i=[1,2,3,...,n//2]
这里为什么n>=4看后面就清楚了。
递归出口:

当n=1时,f(1)=0,不能分
当n=2时,f(2)=1*1=1
当n=3时,f(3)=1*2=2

上面的出口相信大家都能理解,但是当使用递归公式时,就不是这个结果了:
使用递归公式的时候,我们假设绳子已经分成了至少2段了,所以f(3)的值应该是3,为什么呢?因为此时绳子已经分成两段了,所以对于f(3)这一段绳子,是可以不用再分割的,也就是说,此时
f ( 3 ) = m a x { 3 , 不 再 分 割 1 ∗ 2 , 再 次 分 割 成 1 和 2 的 两 段 f(3)=max\begin{cases} 3, 不再分割\\ 1*2,再次分割成1和2的两段\\ \end{cases} f(3)=max{3,1212
同理,我们可以得到f(2)=2,f(1)=1
递归代码如下:

def f(n):
    """递归解法"""
    if n <= 3:
        return n
    else:
        return max([f(j)*f(n-j) for j in range(1, n//2+1)])

if __name__ == '__main__':
    n = int(input())
    # 这里要分情况讨论,只有当n>=4时才能使用递归
    if n == 1:
        print(0)
    elif n == 2:
        print(1)
    elif n == 3:
        print(2)
    else:
        print(f(n))

2 动态规划

def dp(n):
    """问题三的动态规划解法"""
    arr = [0] * (n+1)
    arr[0] = 0
    arr[1] = 1
    arr[2] = 2
    arr[3] = 3
    # 该循环用来得到n之前的每一个n的最大乘积
    for i in range(4, n+1):
        mx = 0
        # 对每一个n,求所有不同分割中的最大乘积
        for j in range(1, i//2+1):
            mul = arr[j]*arr[i-j]
            if mul > mx:
                mx = mul
        arr[i] = mx
    # print(arr)
    return arr[n-1]

问题4

假设有面值为1元、3元、5元的硬币若干枚,凑够11元所用的最少硬币数量是多少?(这题比较简单,我就不过多阐述了)
1 递归
假设f(n)表示凑够n元钱的最少硬币数量:
这里写图片描述
其实可以理解为求这棵树的叶子节点出现的最早的层次,递归公式如下:
f ( n ) = m i n { f ( n − 1 ) f ( n − 3 ) f ( n − 5 ) + 1 f(n)=min\begin{cases} f(n-1)\\ f(n-3)\\ f(n-5)\\ \end{cases} + 1 f(n)=minf(n1)f(n3)f(n5)+1
递归的出口:

f(1),f(3),f(5) = 1
f(2), f(4) = 2

代码如下:

def f(n):
    """递归解法"""
    if n == 1 or n == 3 or n == 5:
        return 1
    elif n == 2 or n == 4:
        return 2
    else:
        return min(f(n-1), f(n-3), f(n-5)) + 1

2 动态规划

def dp(n):
    """动态规划"""
    arr = [0] * (n+1)
    arr[0] = 0
    arr[1] = 1
    arr[2] = 2
    arr[3] = 1
    arr[4] = 2
    arr[5] = 1
    for i in range(6, n+1):
        arr[i] = min(arr[i-1], arr[i-3], arr[i-5]) + 1
    return arr[1:]

问题5

从人民币面值1, 5, 10, 20, 50, 100中拼凑出N元,假设每种币值的数量足够多,求拼凑的不同组合的个数。

问题6

一只袋鼠要从河这边跳到河对岸,河很宽,但是河中间打了很多桩子,每隔一米就有一个,每个桩子上都有一个弹簧,袋鼠跳到弹簧上就可以跳的更远。每个弹簧力量不同,用一个数字代表它的力量,如果弹簧力量为5,就代表袋鼠下一跳最多能够跳5米,如果为0,就会陷进去无法继续跳跃。河流一共N米宽,袋鼠初始位置就在第一个弹簧上面,要跳到最后一个弹簧之后就算过河了,给定每个弹簧的力量,求袋鼠最少需要多少跳能够到达对岸。如果无法到达输出-1(2017年搜狐笔试题)

问题7

求两个字符串的最长公共子序列个数和最长公共子串个数(子序列不要求连续,子串要求连续)

问题8

有 n 个学生站成一排,每个学生有一个能力值,牛牛想从这 n 个学生中按照顺序选取 k 名学生,要求相邻两个学生的位置编号的差不超过 d,使得这 k 个学生的能力值的乘积最大,你能返回最大的乘积吗? (2017年网易笔试题)

问题9

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
示例 :
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", “bwfi”, “bczi”, “mcfi"和"mzi”

问题 10

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
实例:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

  1. 递归
    这题比较简单,假设f(i)表示从下标0到i结尾的数组中的所要求的最大和,那么不难得出:
    f(i) = max( f(i-1) + nums[i], nums[i])
    那代码就很容易写了:
    nums=[-2,1,-3,4,-1,2,1,-5,4]
    def dp(i):
        if i == 0:
            return nums[i]
        return max(dp(i-1)+nums[i], nums[i)
    
    然而这算出来居然不等于6,而是5.
    原因是下标=6的最大和为6,这就是所求的最大和,而最终得到的结果是下标=8的结果5,显然需要用变量保存下之前求得的最大值:
    from functools import lru_cache
    
    nums = [-2, 1, -3, 4, -1, 2, 1, -5]
    pre_max = nums[0]
    
    @lru_cache(maxsize=None)
    def dp(i):
        if i == 0:
            return nums[i]
    
        ret = max(dp(i - 1) + nums[i], nums[i])
        global pre_max 
        pre_max = max(pre_max , ret)
    
        return ret
    
    dp(len(nums) - 1)
    print(pre_max)
    
    lru_cache可以说是一个神器,能有效解决递归过程中重复计算的问题,提高效率。它内部用一个字典保存了函数参数对应的函数值。
  2. 动态规划
    用动态规划更简单了,我们只需要用一个变量保存上个结果,一个变量保存最大值即可:
    nums = [-2, 1, -3, 4, -1, 2, 1, -5]
    pre_max = nums[0]
    last = 0
    for n in nums:
        last = max(last + n, n)
        pre_max = max(pre_max, last)
    
    
    print(pre_max)
    
    本题还可以用分治算法和贪心算法求解,有兴趣的可以自己研究研究,这题是这些算法中比较基础的题。

问题11

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值