题目
文章目录
- 题目
- 递归方法
- 动态规划
- 首先确定状态
- 最后一步
- 分解成子问题
- 其次确定转移方程
- 最后确定初始条件和边界情况
- 两种方法的区别
- 参考文章
你有三种硬币,面值分别为2元,5元,7元,每种硬币都足够多,买一本书需要27元。问:如何用最少的硬币组合正好付清,不需要对方找钱。
递归方法
递归方法的思路是判断最后一步,如果最后一步的数不能被2、5、7整除的话,那么返回无穷大,只有钱刚好凑齐的情况下,返回0
a,b,c = map(int,input('输入a,b空格隔开:').split())
def coin(X):
res = float('inf') ##初始情况,将其赋予无穷大
if(X == 0):
return 0 ##钱刚好凑齐
if(X>=2):
res = min(coin(X-2)+1,res)
if(X>=5):
res = min(coin(X-5)+1,res)
if(X>=7):
res = min(coin(X-7)+1,res)
return res
print(coin(27))
但是用递归的话浪费的空间太多了,而且时间复杂度是2的N此方,如下图所示:
动态规划
所以我们可以选择用动态规划来做。
首先确定状态
最后一步
最优策略必定是K枚硬币a1, a2,…, aK 面值加起来是27。
找出不影响最优策略的最后一个独立角色,这道问题中,那枚最后的硬币“aK”就是最后一步。把aK提取出来,硬币aK之前的所有硬币面值加总是27- aK
因为总体求最硬币数量最小策略,所以拼出27- aK 的硬币数也一定最少(重要设定)。
分解成子问题
最后一步aK提出来之后,我们只要求出“最少用多少枚硬币可以拼出27- aK”就可以了。
这种与原问题内核一致,但是规模变小的问题,叫做子问题。
为简化定义,我们设状态f(X)=最少用多少枚硬币拼出总面值X。
我们目前还不知道最后的硬币aK面额多少,但它的面额一定只可能是2/5/7之一。
如果aK是2,f(27)应该是f(27-2) + 1 (加上最后这一枚面值2的硬币)
如果aK是5,f(27)应该是f(27-5) + 1 (加上最后这一枚面值5的硬币)
如果aK是7,f(27)应该是f(27-7) + 1 (加上最后这一枚面值7的硬币)
除此以外,没有其他的可能了。
至此,通过找到原问题最后一步,并将其转化为子问题。
为求面值总额27的最小的硬币组合数的状态就形成了,用以下函数表示:
f(27) = min{f(27-2)+1, f(27-5)+1, f(27-7)+1}
其次确定转移方程
为求面值总额27的最小的硬币组合数的状态就形成了,用以下函数表示:
f(27) = min{f(27-2)+1, f(27-5)+1, f(27-7)+1}
最后确定初始条件和边界情况
故对边界情况设定如下:
如果硬币面值不能组合出Y,就定义f[Y]=正无穷
例如f[-1]=f[-2]=…=正无穷;
f[1] =min{f[-1]+1, f[-4]+1,f[-6]+1}=正无穷,
特殊情况:本题的F[0]对应的情况为F[-2]、F[-5]、F[-7],按照上文的边界情况设定结果是正无穷。
但是实际上F[0]的结果是存在的(即使用0个硬币的情况下),F[0]=0。
可是按照我们刚刚的设定,F[0]=F[0-2]+1= F[-2]+1=正无穷。
岂不是矛盾?
这种用转移方程无法计算,但是又实际存在的情况,就必须通过手动定义。
这里手动强制定义初始条件为:F[0]=0.
而从0之后的数值是没矛盾的,比如F[1]= F[1-2]+1= F[-1]+1=正无穷(正无穷加任何数结果还是正无穷);F[2]= F[2-2]+1= F[0]+1=1……
实际面试中求解动态规划类问题,正确列出转移方程正确基本上就解决一半了。
##有三枚不同的硬币2、5、7凑成27,求最少的硬币组合
nums = [2,5,7]
S = 27
first_list = [float('inf') for k in range(S+1)] ##赋予初值
first_list[0] = 0 ##初始条件
for i in range(1,S+1): #由小到大F[0]、F[1]、F[2]...
for j in range(len(nums)): #F[0]-2、F[0]-5、F[0]-7 ....
if((i-nums[j]>=0) and (first_list[i-nums[j]]!=float('inf'))): ##如果(剩下的钱够凑&&F[i]-j是不是无穷大)
first_list[i] = min(first_list[i-nums[j]]+1,first_list[i]) ##比较次数
if first_list[S] == float('inf'): #类似爬楼梯算法,最后计算的是S的值(如图所示)
print("没有这种硬币组合")
else:
print(first_list[S]) #因为每一步都已经是最小的组合了,到最后一步只需要直接输出结果
两种方法的区别
递归不会保存每一初始值,所以重复的计算量很多。时间复杂度=2的N此方
而动态规划类似于爬山算法,是从小到大将每一个数保存在列表中从而进行计算,不需要进行重复计算,时间复杂度=硬币种类*凑成的总数字。
参考文章
java实现的凑硬币版本