动态规划与递归实例分析

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

问题1:

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

index 0 1 2 3 4 5 6
arr 1 2 4 1 7 8 3

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:

index 0 1 2 3 4 5 6
arr 1 2 4 1 7 8 3

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

递归的出口:
首先肯定是当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的值。动态规划过程从头开始根据递归公式将计算的中间值保存到数组中。

0 1 2 3 4 5 6 7 8
0 T T F F F F F F F
1 T
2 T
3 T
4 T
5 T
6 T

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

问题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 )

树形状的第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 ]

这里为什么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(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(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年网易笔试题)

猜你喜欢

转载自blog.csdn.net/qq_35556064/article/details/82503076
今日推荐