从多张优惠券中,挑出合适组合,使其最接近手续费 —— 失效的贪心算法到改装的动态规划

背景

有这么一个需求,后台会给客户发优惠券。用这些券可以抵扣买基金时的手续费。

客户有多张优惠券可使用,手续费是确定的。

系统自动给算出来,要使用哪几张券,客户最划算。

规则有以下三条:

  1. 半月内过期的券优先使用
  2. 优先使用大额的优惠券
  3. 优惠券充足时,选出的组要不低于手续费,尽可能接近。

需求拆分

这样的要求很合理,对客户来讲也很人性化。

不太好实现,比较复杂,可对于合情合理的需要,理应努力实现其需求。

于是乎,开始构想应对方法,这个过程很有意思。

我提出了初步方案。

  • 第一步分组,将快过期的券单独拿出来,优先选择
    在这里插入图片描述
  • 第二步,问题转化为从一堆券中,挑出组合,使其接近目标值。

解释下:

比如手续费是100元,如果快过期的券总面值120元,那就从快过期的券中,挑选近似100元的组合即可。

如果快过期券总面值是70元。那就是快过期券全用,在正常券中,挑选近似30元的组合。

总之,问题都转化为从一堆券中,挑出组合,使其接近某个目标值。

  • 第三步,目标值加入后排序,问题转化为三种可能在这里插入图片描述
    将优惠券从大到小排序,那手续费与之相比,有三种可能,排在最后,排在最前,排在中间

若排在最后,那不用证明,取面值最小的优惠券即可,将其定义为方案A

若排在最前,那定义这方案B,待会再讨论。

若排在中间,那就是两种方案取其一, 一种方案是排在它前面的,方案A解决,另一种是排在它后面的方案B解决。优先其中一种方案

最后一句是什么意思?

比如有10张券,手续费排在第3张与第4张券之前,

那相当于,前3张券看做一个整体,手续费排最后。

第4张券到第10张券看做一个整体,手续费用排在最前。

最终都会转化是排在最前,或排在最后的问题。

  • 第四步,讨论方案B怎么实现。

方案B 对应的场景是,一堆优惠券,每张面值都比手续费小。

解决分为两种情况

所有优惠券面值总额小于等于手续费用,不用想,优惠券全部使用。

优惠券面值总额大于手续费用,那挑选合适的组合。

至此,将一个现实中需要一步一步抽象,问题逐步转化,变成一个单纯的算法问题。

有一组数中,每个都小于目标值,总和大于目标值。

挑选合适的组合,使其总和大于或等于目标值。

在所有符合条件的组合中,选择总和最小的那个组合。

算法选择

在这里插入图片描述
我首先想到的是,类似于贪心算法:

将所有的数,从大到小排列,逐个累加。

首次出现大于目标值时,停止 。
如上图,到第4个数停止,
取第1到第3的总和,计为SUM,(一定小于目标值)。

从第5个数开始与SUB相加,其和与目标值比较,
大于目标值,舍弃,试下一个。
直到首次出现,小于目标值时停止。

比如上图中,假设 SUM加上第8个数,
首次小于目标值,那取第7个数即可。
所选出的组合就是 1 2 3 7

智力所及,我能想到最好的方案,到此就结束了。

可回头想想,这是一种方案,有可能不是最优的。举个反例,暂时也没想出来。

比如上面那个例子中,首次出现大于目标值,舍弃第4个,那舍弃第3个,有时也是可行的,甚至能找出更优解。

我无法从数学上证明,给出的算法是最优解,但反例确实一时也没想出来

这就算是初步的方案,先完成需求,以后再优化。

没有完美的方案,本身这个需要条件就很多,
而且这三个条件有时还互斥。

想了好久,我还真的写出了个反例来。

优惠券金额 70, 40, 30, 21, 5, 手续费 120

按我给出的方案会选出 70, 40, 21 , 总计131

但如果选 70 , 30, 21, 总计 121,显然这个更合理

贪心算法失效了……

我曾想到用动态规划,可动态转移方程没想好。

更不好想的是,整个过程要记录路径,最终要输出,选了哪几张券。

晚上下班,继续想,看相关文章,最后,终于找到了更合理的方案。

用动态转移表法,更容易实现。直接上代码。

如果看代码吃力,请参考我之前写的这篇文章:初识动态规划


public class DpTest {
   
    public static void main(String[] args) {
   
        int expectValue = 90; // 目标值,即手续费是多少
        int n = 20; // 优惠券数量
        List<Node> list = getCouponList(n);
        dp(expectValue,list,n);
    }

    /**
     * 随机获取一组优惠券
     * 
     * @param n
     * @return
     */
    private static List<Node> getCouponList(int n) {
   
        List<Node> list = new ArrayList<>(n)
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值