zoukankan      html  css  js  c++  java
  • Golang实现高性能凑单工具:给定<金额列表>计算<目标金额>所有组合

    一、需求

    公司有一个比较坑爹的报销方案,需要根据一堆零碎的发票中,凑出一个目标金额,要求误差在1块钱以内。
    例如:你有一堆发票[100, 101, 103, 105, 106, 132, 129, 292, 182, 188, 224.3, 40.5, 35.9, 32.5, 39, 12, 17.5, 28, 35, 34, 26.32, 28, 35, 39, 25, 1, 24, 35, 45, 47, 32.11, 45, 32, 38.88, 44, 36.5, 35.8, 45, 26.5, 33, 25, 364, 27.3, 39.2, 180, 279, 282, 281, 285, 275, 277, 278, 200, 201, 1959.12, 929.53, 1037.03, 1033.9],让你从这堆票中凑出5000块来,然后最多不能超过1块钱。你瞧瞧这是人干的事么!

    缺点:每次人肉去对比,浪费大量的时间。
    操作过程大概是这样的:新建一个excel表格,将所有的金额录入,然后自己勾选发票,直到目标金额出现,如下图

    人品大爆发的时候一下就凑出来,运气不好的时候凑着凑着一两个小时就过去了!
    因此,我们急需一个程序自己去干这个事。

    二、实现思路

    1. 最差方案:全组合
      使用全组合,搜索所有组合方案,遍历满足的结果输出,时间复杂度为O(n!),原先调用了python的排列组合函数实现,结果卡得不行,有时候能把程序跑挂了

    2. 中等方案:回溯暴力破解
      利用回溯输出,存在重复递归,时间复杂度为O(2^n),一般来说已经满足正常需求,但是如果n很大,还是影响性能

    3. 最优方案:动态规划
      时间复杂度为O(n*w),为最快方案,提升气质指数,5颗星!

    三、最终方案:动态规划

    最终用动态规划思想实现,空间换时间,200个碎票匹配1万的金额秒出结果,大概使用800M内存,

    代码已经贴到github:chenqionghe/amount-calculator

    核心代码如下

    package main
    
    import (
    	"fmt"
    	"github.com/shopspring/decimal"
    	"strconv"
    )
    
    type AmountCalculator struct {
    	maxValue int   //期望值(单元为分)
    	items    []int //发票金额(单元为分)
    	overflow int   //允许的误差值(单元为分)
    }
    
    //items:所有发票 maxValue:目标金额 overflow:允许误差金额
    func New(items []float64, maxValue float64, overflow float64) *AmountCalculator {
    	obj := &AmountCalculator{}
    	obj.maxValue = obj.dollarToCent(maxValue)
    	obj.overflow = obj.dollarToCent(overflow)
    	centItems := make([]int, len(items))
    	for i, v := range items {
    		centItems[i] = obj.dollarToCent(v)
    	}
    	obj.items = centItems
    	return obj
    }
    
    //元转分
    func (this *AmountCalculator) dollarToCent(value float64) int {
    	value, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", value), 64)
    
    	decimalValue := decimal.NewFromFloat(value)
    	decimalValue = decimalValue.Mul(decimal.NewFromInt(100))
    
    	res, _ := decimalValue.Float64()
    	return int(res)
    }
    
    //分转元
    func (this *AmountCalculator) centToDollar(v int) float64 {
    	value := float64(v)
    	value, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", value/100), 64)
    	return value
    }
    
    //执行计算,返回所有方案
    func (this *AmountCalculator) GetCombinations() [][]float64 {
    	items := this.items
    	n := len(this.items)
    	max := this.maxValue + this.overflow
    	states := this.createStates(len(this.items), max+1)
    
    	states[0][0] = true
    	if items[0] <= max {
    		states[0][items[0]] = true
    	}
    
    	for i := 1; i < n; i++ {
    		//不选
    		for j := 0; j <= max; j++ {
    			if states[i-1][j] {
    				states[i][j] = states[i-1][j]
    			}
    		}
    		//选中
    		for j := 0; j <= max-items[i]; j++ {
    			if states[i-1][j] {
    				states[i][j+items[i]] = true
    			}
    		}
    	}
    	//获取最终所有满足的方案
    	res := make([][]float64, 0)
    	for j := this.maxValue; j <= max; j++ {
    		for i := 0; i < n; i++ {
    			if states[i][j] {
    				//判断必须最后一个选中才算,要不区间有重合 比如前5个元素已经满足目标金额了,state[5][w]=true,然后state[6][w]也是true,存在重复的方案
    				if i == 0 {
    					//第一个元素已经满足
    					res = append(res, this.getSelected(states, items, i, j))
    				} else if j-items[i] >= 0 && states[i-1][j-items[i]] == true {
    					res = append(res, this.getSelected(states, items, i, j))
    				}
    			}
    		}
    	}
    	return res
    }
    
    //获取所有选中的元素(倒推)
    func (this *AmountCalculator) getSelected(states [][]bool, items []int, n, max int) []float64 {
    	var selected = make([]int, 0)
    	for i := n; i >= 1; i-- {
    		//元素被选中
    		if max-items[i] >= 0 && states[i-1][max-items[i]] == true {
    			selected = append([]int{items[i]}, selected...)
    			max = max - items[i]
    		} else {
    			//没选,max重量不变,直接进入下一次
    		}
    	}
    
    	//如果max不为0,说明还需要追加第一个元素
    	if max != 0 {
    		selected = append([]int{items[0]}, selected...)
    	}
    
    	dollarItems := make([]float64, len(selected))
    	for i, v := range selected {
    		dollarItems[i] = this.centToDollar(v)
    	}
    	return dollarItems
    }
    
    //初始化所有状态
    func (this *AmountCalculator) createStates(n, max int) [][]bool {
    	states := make([][]bool, n)
    	for i, _ := range states {
    		states[i] = make([]bool, max)
    	}
    	return states
    }
    

    四、使用方式

    1.直接调用代码(适合用来开发自己的软件)

    package main
    
    import (
    	"fmt"
    	"github.com/chenqionghe/amount-calculator"
    	"time"
    )
    
    func main() {
    	//所有碎票
    	items := []float64{100, 101, 103, 105, 106, 132, 129, 292, 182, 188, 224.3, 40.5, 35.9, 32.5, 39, 12, 17.5, 28, 35, 34, 26.32, 28, 35, 39, 25, 1, 24, 35, 45, 47, 32.11, 45, 32, 38.88, 44, 36.5, 35.8, 45, 26.5, 33, 25, 364, 27.3, 39.2, 180, 279, 282, 281, 285, 275, 277, 278, 200, 201, 1959.12, 929.53, 1037.03, 1033.9}
    	//目标金额
    	target := float64(5000)
    	//允许超出
    	overflow := float64(1)
    	obj := amountcalculator.New(items, target, overflow)
    
    	startTime := time.Now()
    
    	//获取所有的组合
    	res := obj.GetCombinations()
    	for _, v := range res {
    		fmt.Println(v)
    	}
    	fmt.Printf("total:%d used time:%s
    ", len(res), time.Now().Sub(startTime))
    }
    

    运行结果

    [100 101 103 105 106 132 129 292 182 188 224.3 40.5 12 17.5 35 34 26.32 28 35 39 25 1 24 35 45 47 45 32 38.88 44 36.5 45 26.5 33 25 364 27.3 39.2 180 279 282 281 285 275 277 278]
    [100 101 103 105 132 129 292 182 188 35.9 39 12 17.5 28 35 34 26.32 28 35 39 25 1 24 35 45 47 32.11 45 32 38.88 44 36.5 35.79 45 26.5 33 25 364 27.3 39.2 180 279 282 281 285 275 277 278 200]
    ...
    [35.9 25 24 38.88 36.5 35.79 45 26.5 33 25 27.3 39.2 180 279 282 281 285 275 277 278 200 201 1037.03 1033.9]
    total:577 used time:97.048224ms
    

    耗时97毫秒,共计算出577种方案,性能高到令人忍不住想鼓掌!

    这种方式适合在自己开发的程序中使用,比如要出一个web界面,给前端提供数据

    2.命令行模式(适合不会编程的人使用)

    这种方式适合不会go语言,或者不会编程的人使用,只需编译出对应平台的版本就行。
    一次编译多次分发,相当于copy了个绿色版软件到电脑上直接使用

    编译过程如下

    • 新建一个go文件:main.go
    package main
    
    import (
    	"github.com/chenqionghe/amount-calculator"
    )
    
    func main() {
    	amountcalculator.RunCliMode()
    }
    
    • 编译
    go build -o amount-calculator
    
    • 运行该工具
     ./amount-calculator -max=156 -overflow=1 -items=12,135,11,12,15,16,18,32,64,76,50
    156 [11 15 16 18 32 64]
    156 [16 64 76]
    156 [12 18 76 50]
    157 [12 15 16 18 32 64]
    157 [15 16 18 32 76]
    157 [15 16 76 50]
    

    可以看到,命令行直接输出了所有匹配的组合,还给出了每个组合的金额,对于使用者来说,不会编程语言也完全没有关系,只需要自己执行一下即可,相当方便。

    另外,命令行模式省了一个编译的过程,效率更高,推荐!

    五、总结

    技术解放生产力,这种重复且费时的劳动完全应该由程序去做,可惜的是

    1. 一般的产品提不出这样的需求
    2. 产品即使能提这样的需求,开发也不一定能实现出来
    3. 开发即使能实现出来,程序也不一定能跑得动,因为很可能太耗内存或太耗CPU,还没等结果运行出来程序就挂了

    哈哈,稍微有点吹牛逼了,不管怎么样,就一句话:yeah buddy! light weight baby! 让我听到你们的尖叫声!

  • 相关阅读:
    Ros学习——Cmakelists.txt文件解读
    Ros学习——Movebase源码解读
    C++——STL之vector, list, deque容器对比与常用函数
    Ros学习——移动机器人Ros导航详解及源码解析
    C++——多线程
    C++——STL容器
    PHP对图片按照一定比例缩放并生成图片文件
    PHP二维数组排序
    PHP裁剪图片并上传完整demo
    [PHP] php实现文件下载
  • 原文地址:https://www.cnblogs.com/chenqionghe/p/12145928.html
Copyright © 2011-2022 走看看