贪心法(greedy method)
贪心法在解决问题的策略上目光短浅,只根据当前已有的信息就做出选择,而且一旦做出了选择,不管将来有什么结果,这个选择都不会改变。换言之,贪心法并不是从整体最优考虑,它所做出的选择只是在某种意义上的局部最优。这种局部最优选择并不总能获得整体最优解(Optimal Solution),但通常能获得近似最优解.
动态规划法(dynamic programing method)
是将待求解问题分解成若干个相互重叠的子问题,每个子问题对应决策过程的一个阶段,一般来说,子问题的重叠关系表现在对给定问题求解的递推关系(也就是动态规划函数)中,将子问题的解求解一次并填入表中,当需要再次求解此子问题时,可以通过查表获得该子问题的解而不用再次求解,从而避免了大量重复计算。
个人理解他们的区别贪心算法对于解是否是当前问题的最优解的关心程度不是很高,只要能获得近似最优解即可,而动态规划是需要求出对于整体来说的最优解,所以贪心算法相对动态规划来说,对处理求解不是很严格的场景适用,也避免了许多的计算。
最近在做一个发票项目:
其中有一个需求:开具发票时开具的金额可能会超过限额,这个时候需要拆分发票。
拆分规则:
1.优先按明细拆分。
2.当一行明细行超过限额时才能拆分该行明细,未超过不允许拆分。
3.尽量做到最少的发票张数(有纸票需求,拆分出来的票越少越好)
了解需求后,捋了一遍思路,初步想出来的算法是
1.首先遍历明细行,将所有单行明细超过限额的明细行拆分
2.经过第一次的遍历现在剩下的明细行都是未超过限额的明细
3.运用贪心算法思想对剩下的明细行进行拆分
算法实现如下:
/**
* 自动拆分工具类
* @author longw
* @since 190326
*
*/
public class AutoSplitUtil {
//BIZItemInfo 是发票明细的实体类 里面存储的大概就是商品名称,和明细金额
//自动拆分
public static List<List<BIZItemInfo>> autoSplit(List<BIZItemInfo> items,Double limit) {
if (limit<=0) {
return null;
}
List<List<BIZItemInfo>> result = new ArrayList<List<BIZItemInfo>>();
//先将所有大于限制的明细拆分成小于限制的明细(必须拆分时才拆分明细行)
for (BIZItemInfo list : items) {
while (list.getJe()>=limit) {
result.add(getLimitList(list, limit));
list.setJe(list.getJe()-limit);
}
}
//清理金额为0的明细
items = resetList(items);
//降序排序
Collections.sort(items);
//贪心算法每次都返回一个尽量接近发票限额的明细行列表(不一定是最优解)
while (!items.isEmpty()) {
List<BIZItemInfo> list = getSatisfyList(items, limit);
result.add(list);
items.removeAll(list);
}
return result;
}
//返回一条金额为限制金额的行的list
private static List<BIZItemInfo> getLimitList(BIZItemInfo item,Double limit){
List<BIZItemInfo> result = new ArrayList<BIZItemInfo>();
BIZItemInfo data = new BIZItemInfo();
data.setDj(item.getDj());
data.setDw(item.getDw());
data.setFphxz(item.getFphxz());
data.setGgxh(item.getGgxh());
data.setHsbz(item.getHsbz());
data.setJe(limit);//金额设置为限额大小
data.setSpmc(item.getSpmc());
//data.setSpsl(item.getSpsl());
data.setSelectid(0);
result.add(data);
return result;
}
//遍历传入的数据返回一个总金额不大于限制金额的list
private static List<BIZItemInfo> getSatisfyList(List<BIZItemInfo> data,Double limit){
List<BIZItemInfo> result = new ArrayList<BIZItemInfo>();
for (BIZItemInfo item : data) {
if (item.getJe()<=limit) {
result.add(item);
limit -= item.getJe();
}
}
return result;
}
关于降序排序的问题,BIZItemInfo实体类需要实现Comparable接口,然后在类中重写compareTo方法,我的compareTo方法为:
//根据金额降序排序
@Override
public int compareTo(BIZItemInfo o) {
return this.je < o.je ? 1 : -1;
}
从实现可以看出我在后半段使用的是贪心算法的思想(求近似最优解而不是最优解,这也是贪心算法和动态规划最大的区别)
下面用main方法测试下效果:
public static void main(String[] args) {
List<BIZItemInfo> s = new ArrayList<BIZItemInfo>();
BIZItemInfo item = new BIZItemInfo();
item.setDj(100d);
item.setJe(15001d);
item.setSpmc("图书");
s.add(item);
BIZItemInfo item1 = new BIZItemInfo();
item1.setDj(100d);
item1.setJe(15001d);
item1.setSpmc("图书111");
s.add(item1);
BIZItemInfo item2 = new BIZItemInfo();
item2.setDj(100d);
item2.setJe(15001d);
item2.setSpmc("图书222");
s.add(item2);
List<List<BIZItemInfo>> a = new ArrayList<List<BIZItemInfo>>();
Double d = 0d;
for (BIZItemInfo list : s) {
d += list.getJe();
}
Double limit = 20000d;//限额大小
a = autoSplit(s, limit);
System.out.println("限额大小:"+limit);
System.out.println("预计最少发票数:"+Math.ceil(d/limit));
System.out.println("拆分后发票数:"+a.size());
System.out.println("---------------------------------");
}
以下是我分别将限额设置为1000,10000,20000时候的输出
限额大小:1000.0
预计最少发票数:46.0
拆分后发票数:46
---------------------------------
限额大小:10000.0
预计最少发票数:5.0
拆分后发票数:6
---------------------------------
限额大小:20000.0
预计最少发票数:3.0
拆分后发票数:3
---------------------------------
总结:总体上来说发票张数是等于预计张数的,在限额10000的情况下,比预计多了一张的原因是需求中的 2.当一行明细行超过限额时才能拆分该行明细,未超过不允许拆分。 如果可以拆分,可以将发票拆成:三张1w,一张1w(两条明细行5000)一张5003(一条5001,两条1)来达到最少票的要求。
3.29测试后发现有的发票中存在金额为0的明细行,写了个方法过滤了下金额为0的明细。如下
//返回有效明细行
private static List<BIZItemInfo> resetList(List<BIZItemInfo> items){
List<BIZItemInfo> result = new ArrayList<BIZItemInfo>();
for (BIZItemInfo item : items) {
if (!(item.getJe()==0d)) {
result.add(item);
}
}
return result;
}
然后在排序之前过滤下明细行
//清理金额为0的明细
items = resetList(items);
//降序排序
Collections.sort(items);
现在用贪心算法实现了本需求,如何用动态规划实现该需求呢?请路过的大神留下您宝贵的意见。谢谢。
待更新!!!