本文收集了一些能用动态规划或递归进行解答的题目,旨在大家能逐渐理解递归和动态规划的思想,所以这里并不会说明其相关定义,由于博主也是刚刚接触,如果有问题请大家一定要指正,谢谢!
问题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 的情况:
那么递归公式很明显了:
递归的出口:
首先肯定是当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所求得的最大乘积,
那么
树形状的第2层(7,6,5,4)表示将绳子分成2段,第3层则表示将绳子分成3段,例如第3层的树节点6,则表示f(8)=f(1)*f(1)*f(6),将绳子分成了3段;依次类推。
递归公式:
这里为什么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(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(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年网易笔试题)