使用python有趣地解决一道动态币值规划方案

使用python有趣地解决一道动态币值规划方案


大家好,最近碰到一道动态币值规划问题,通过几种方法求解,深得其奥妙,也十分有趣,特将我的心路历程分享出来。

题目

币值题目

币值规划目的

设计一种在给定的币值类型里规划出币值数量最少,币值总和与给定值一致的方案

解法一

改解法为此题已给定的方法,以下仅是我自己的理解。

# bz[i] = min{bz[i - bizhi[j]] + 1}, 且 其中 i >= bizhi[j], 0 <= j < bizhi.length
# 输出可找的币值方案
# path[i] 表示经过本次已给币值后所剩下的面值,即 i - path[i] 可得到本次所需的币值。
def paperNum(bizhi, n):#定义币值以及输入金额n
    bz, path = [0] * (n + 1), [0] * (n + 1)  # 初始化
    for i in range(1, n + 1):
        minNum = i  # 初始化当前币值最优值
        for c in bizhi:  # 扫描一遍币值列表,选择一个最优值
            if i >= c and minNum > bz[i - c] + 1:
                minNum, path[i] = bz[i - c] + 1, i - c
        bz[i] = minNum  # 更新当前币值最优值
    print('bz:   ', bz)
    print('path: ', path)
    print('最少币值数:', bz[-1])#读取数组最后一个值
    print('所需的币值', end=': ')
    while path[n] != 0:
        print(n - path[n], end=' ')
        n = path[n]
    print(n, end=' ')

if __name__ == '__main__':
    bizhi = [100,70,50,20,10,5,2,1]# 输入可换的币值种类,总金额n
    while True:
        n = input("输入总金额: ")
        n = int(n)
        if n < 0:
            print('输入数值无效,请重新输入')
        if n>=0:
            break
    paperNum(bizhi, n)

输出结果:
输出结果1
这个解法初看有点难以理解,其思路是什么,为什么要这样写,当尝试输出一遍结果后再细看,解法相当之妙,其输出的bzpath不仅仅是针对输入的总金额"16",而是把"16"作为该序列的最大值进行遍历求解,即求出了小于等于16的所有金额的币值最优组合。

我们从输出的结果来试看问题,输入的总金额是16,则设置bz和path的列表长度为16+1,但我们现在不推算出16的结果,来推算15试试,bz[15]为2,所以用bizhi中的币值种类,所需最少币值数为2个,可想而知币值为[10, 5],使用path中的元素推算过程为:path[15]为5,第一个值为n-path[n]即10,将path[n]赋值给n,此时path[n]为0,则将n最为结果输出,结果为:[10, 5]

不得感叹下,求解方法上是动态化对1-n上的币值规划最少币值数,但在提取结果上依旧是单个数据提取,当给定数值过于大时,会有较长的等待时间。所以下一种方法是对一个数值进行规划,最后可以套用for循环构造数值映射字典。

解法二

根据已给定的bizhi列表及已有的知识构造bz_dict字典

# 手动构造币值区间字典
bz_dict = {3: {0: []},
           2: {9: [70, 20],
               8: [70, 10],
               7: [70],
               6: [50, 10],
               5: [50],
               4: [20, 20],
               3: [20, 10],
               2: [20],
               1: [10],
               0: [],
               },
           1: {9: [5, 2, 2],
               8: [5, 2, 1],
               7: [5, 2],
               6: [5, 1],
               5: [5],
               4: [2, 2],
               3: [2, 1],
               2: [2],
               1: [1],
               0: []}
           }

修改 解法一 中的paperNum函数:

def paperNum(bizhi, n):  # 定义币值以及输入金额n
    # 手动构造币值区间字典
    bz_dict = {3: {0: []},
               2: {9: [70, 20],
                   8: [70, 10],
                   7: [70],
                   6: [50, 10],
                   5: [50],
                   4: [20, 20],
                   3: [20, 10],
                   2: [20],
                   1: [10],
                   0: [],
                   },
               1: {9: [5, 2, 2],
                   8: [5, 2, 1],
                   7: [5, 2],
                   6: [5, 1],
                   5: [5],
                   4: [2, 2],
                   3: [2, 1],
                   2: [2],
                   1: [1],
                   0: []}
               }

    n_str = '0' * (3 - len(str(n))) + str(n)
    bz = []
    if len(n_str) > 3 or n_str[-3] in list('123456789'):
        bz = [100] * int(n_str[:-2])  # 如果不能使用*号,改用for循环累加
        n_str = n_str[-2:][::-1]
    bz += bz_dict.get(2).get(int(n_str[1]))
    bz += bz_dict.get(1).get(int(n_str[0]))
    print('最少币值数: ', len(bz))
    print('所需币值: ', bz)

乍一看比上一种容易理解多了,主要原因也是这只是针对一个值进行币值规划,而且bz_dict在已知bizhi = [100,70,50,20,10,5,2,1]的情况下容易构建,而当bizhi中的元素发生了改变,paperNum中的一切都需要改变,也没有解法一的通用性,所以得把bz_dict用代码动态构造。

经过一顿瞎操作终于搞定,最终代码:

# 单一金额币值规划
def paperNum(bizhi, n):#定义币值以及输入金额n
    def get_bizhi_dict(bizhi):
        """
        获取各分区币值组合,对于此题而言该步骤是多余的,完全可以用先验知识构造字典,
        但这段可以使总体更加普适化,即当币值列表中币值发生了变化不用再次修改字典中的值。
        :param bizhi: 币值列表
        :return: 币值分区字典
        """
        bizhi_dict = dict()
        for i in bizhi:
            N = len(str(i))
            if N not in bizhi_dict.keys():
                bizhi_dict[N] = [i]
            else:
                bizhi_dict[N].append(i)

        for key, value in bizhi_dict.items():
            bz_dict = dict()
            for num in range(1, 10):
                j = num * 10 ** (key - 1)
                bz_list = []
                for i in value:
                    quotient, j = divmod(j, i)
                    bz_list += [i] * quotient
                bz_dict[num] = bz_list
            bizhi_dict[key] = bz_dict
        return bizhi_dict

    bizhi_max = max(bizhi)
    bz_dict = get_bizhi_dict(bizhi)  # 获取币值区间字典
    bz_keys_max = max(bz_dict.keys())   # bizhi最高位
    n_str = '0' * (bz_keys_max - len(str(n))) + str(n)   # 填补字符串长度,保证与bizhi最高位一致
    bz = []
    if len(n_str) > bz_keys_max:
        bz = [bizhi_max] * len(range(bizhi_max, n, bizhi_max))  # 如果不能使用*号,改用for循环累加
        n_str = str(n - sum(bz))
    for i in range(1, bz_keys_max + 1):  # 逐位获取币值
        bz2_dict = bz_dict.get(i)
        if not bz2_dict:
            continue
        bz += bz2_dict.get(int(n_str[::-1][i - 1]), [])
    print('数据验证: ', sum(bz) == n)
    print('最少币值数: ', len(bz))
    print('所需币值: ', bz)

if __name__ == '__main__':
    bizhi = [500,100,70,50,20,10,5,2,1]# 输入可换的币值种类,总金额n
    while True:
        n = input("输入总金额: ")
        n = int(n)
        if n < 0:
            print('输入数值无效,请重新输入')
            #continue
        if n>=0:
            break
    paperNum(bizhi, n)

通过动态构造币值区间字典,再通过字符串化的数值,对逐个位数上的值进行币值规划,而且在bizhi列表发生变化时也不用更改paperNum中的代码,在实际测试中也是只规划单个数值,所以当数值很大时,耗时也在心理层面上能够接受范围之内。
解法3

转念一想,毕竟对单个数值进行规划,与其一通乱搞瞎构造字典,不如直接计算更是妙哉啊。

解法三

使用求商取余的方式规化币值数

# 求商取余对单一金额进行币值规划
def paperNum(bizhi, n):#定义币值以及输入金额n
    n_orig = n
    bz = []
    for i in bizhi:
        bz_num, n = divmod(n, i)  # 求商取余函数
        bz += [i] * bz_num
    print('数据验证: ', sum(bz) == n_orig)
    print('最少币值数: ', len(bz))
    print('所需币值: ', bz)

if __name__ == '__main__':
    bizhi = [500,100,70,50,20,10,5,2,1]# 输入可换的币值种类,总金额n
    while True:
        n = input("输入总金额: ")
        n = int(n)
        if n < 0:
            print('输入数值无效,请重新输入')
        if n>=0:
            break
    paperNum(bizhi, n)

这段代码极大的简化解法二的繁琐步骤,且bizhi发生改变也不用更改paperNum函数中的代码。


更新部分

后续计算中发现bizhi列表中有个值70,在实际生活中会考虑换算效率大概率不会有70面值的币,但在此题就需要解决,如140,正确输入为:最少币值数:2,所需币值:[70, 70],除解法一外另外两种解法均是[100, 20, 20],不满足题意,正当我着手解决时,转念一想210呢,规划方案有两种,分别为[100, 100, 20]和[70, 70, 70],应当把两种方案均输出,可见所有解法中均不满足,故接下仅更改解法三。

  • 解法三_改
# 求商取余对单一金额进行币值规划(多方案归类)
def paperNum(bizhi, n):  # 定义币值以及输入金额n
    minNum = n_orig = n  # 初始化币值数量
    bizhi.sort(reverse=True)  # 按币值从大至小排序
    bizhi_length = len(bizhi)
    result = []
    for _ in range(bizhi_length):
        bz = []
        n = n_orig
        for i in bizhi:
            bz_num, n = divmod(n, i)
            bz += [i] * bz_num
            if not n:  # 除数被整除中断循环
                break
        if len(bz) > minNum:  # 币值数大于最优币值数跳出总循环
            break
        elif len(bz) == minNum and bz not in result:  # 币值数等于最优币值数,且方案不存在增加方案
            result.append(bz)
        else:  # 币值数小于最优币值数,更换方案
            result = [bz]
        minNum = len(bz)
        bizhi = bizhi[1:]  # 剔除最大币值
    print('最少币值数: ', minNum)
    print('所需币值: ', result)


if __name__ == '__main__':
    bizhi = [100, 70, 50, 20, 10, 5, 2, 1]  # 输入可换的币值种类,总金额n
    while True:
        n = input("输入总金额: ")
        n = int(n)
        if n < 0:
            print('输入数值无效,请重新输入')
        if n >= 0:
            break
    paperNum(bizhi, n)

思路:对逐个最大不币值不同的bizhi进行求解,并找出币值数最少的方案,虽然代码相较解法三多了不少,但在考虑条件上更加丰满,也消除了一些不必要的币值规划方案。


总结

一道题目求解方式有很多,计算的过程也各有不同,奈何我学识尚浅,今后的学习之路还很漫长。


二零二一年十一月二十五日作

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值