递归与动态规划 —— Python 实现

1. 递归

1.1 递归三要素

递归 (recursion) 通过自身调用来降低问题的复杂性,利用递归解决问题首先需要确定 3 个子问题:

  • 基本问题:最简单的子问题,或称为初始状态
  • 状态转移方程:如何从复杂问题向更简单的问题过渡
  • 目标问题:需要利用状态转移方程从基本问题到复杂问题

1.2 最小钱币数问题

最小钱币数是一个找零问题,比如目前有若干 [1, 5, 10] 三种硬币,需要找零 37,则在使用最少钱币的情况下需要 10, 10, 10, 5, 1, 1 共 6 枚硬币

使用递归解决该问题:

  • 基本问题:当找零数为某种硬币金额时,返回 1
  • 状态转移方程:尝试先给出 1 枚任意金额小于找零金额的硬币,再找零剩余部分,从中选择硬币数目最少的
  • 目标问题:找到需要的最少硬币数

def recMinCoins(coinValueList, change):
	res = change
	if change in coinValueList:
		return 1
	else:
		for coin in [c for c in coinValueList if c < change]:
			num = minCoins(coinValueList, change - coin) + 1
			if num < res:
				res = num
	return res

s = time.time()
print(recMinCoins([1, 2, 5, 10], 37))	# 5
e = time.time()
print(e - s)

分析:结果正确,但消耗的时间和内存巨大(耗时 26.66s),因为在递归调用自身时,会重复计算很多参数完全相同的函数,比如 37 先给出 10 以及 37 先给出 2 枚 5 的硬币后,接下来的子问题完全一样,即找零 27,但该程序仍然会重复计算该子问题!



2. 动态规划

2.1 方法

动态规划 (dynamic programming) 是优化后的递归算法,它通过存储先前计算过的子问题结果,来避免相同子问题的重复计算,从而提高算法的效率。

算法步骤:

  • 创建表格,假设要计算的问题复杂度为 n,则创建 n 列的表格 minCoins,minCoins[0] = 0,硬币为 coinValue = [1, 2, 5]
    在这里插入图片描述

  • 接着按列从 0 到 n 依次计算结果 minCoins[i],对于 minCoins[i],尝试使用每一种硬币 conValue[j],j = 0, 1, 2;若 minCoins[i] < coinValue[j],则目标硬币数为 minCoins[i - coinValue(j)] + 1,计算全部可能的情况取 最小值

  • 比如说下面这种情况,要计算 minCoins[16],有四种情况:
    minCoins[13] + 1 = 5
    minCoins[12] + 1 = 4
    minCoins[9] + 1 = 4
    最后 minCoin[14] = 4(选择 5 5 2 2 共 4 枚硬币)
    在这里插入图片描述


2.2 动态规划实现最小钱币数
import time
import numpy as np

def dpMinCoins(coinValueList, change):
	minCoins = [0]*(change + 1)
	for money in range(1, change+1):
		options = []
		for val in coinValueList:
			if money >= val:
				options.append(minCoins[money - val] + 1)
		minCoins[money] = np.min(options)

	return minCoins[change]


s = time.time()
print(dpMinCoins([1, 2, 5, 10], 37))	# 5
e = time.time()
print(e - s)

运行结果:5,运行时间接近 0s 🍻

此时创建的数组如下:

[0, 1, 1, 2, 2, 1, 2, 2, 3, 3, 1, 2, 2, 3, 3, 2, 3, 3, 4, 4, 2, 3, 3, 4, 4, 3, 4, 4, 5, 5, 3, 4, 4, 5, 5, 4, 5, 5]

此外,利用 minCoins[money] == minCoins[money - val] + 1 可以倒推使用过的硬币:

import time
import numpy as np

def dpMinCoins(coinValueList, change):
	minCoins = [0]*(change + 1)
	coinUsed = [[0]*(change + 1) for i in range(len(coinValueList))]

	for money in range(1, change+1):
		# calculate coins
		options = []
		for val in coinValueList:
			if money >= val:
				options.append(minCoins[money - val] + 1)
		minCoins[money] = np.min(options)

		# coins record
		for val in coinValueList:
			if money >= val and minCoins[money] == minCoins[money - val] + 1:
				for i in range(len(coinValueList)):
					coinUsed[i][money] = coinUsed[i][money - val]
				coinUsed[coinValueList.index(val)][money] += 1
				break

	print(minCoins)
	for row in coinUsed:
		print(row)
	return minCoins[change]


s = time.time()
print(dpMinCoins([1, 2, 5, 10], 37))	# 5
e = time.time()
print(e - s)

结果为:

[0, 1, 1, 2, 2, 1, 2, 2, 3, 3, 1, 2, 2, 3, 3, 2, 3, 3, 4, 4, 2, 3, 3, 4, 4, 3, 4, 4, 5, 5, 3, 4, 4, 5, 5, 4, 5, 5]
[0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0]
[0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1]
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3]
5
0.0009975433349609375

总结:

  • 所有递归算法都必须回归到基本问题。
  • 递归算法必须改变其状态并向基本问题靠近。
  • 递归在某些情况下可以替代循环。
  • 递归有时比其他算法的计算复杂度更高。

完结 🍻

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值