高性能凑单工具:给定<金额列表>计算满足<目标金额>的所有组合
雪山飞猪 人气:1目录
- 一、需求
- 二、实现思路
- 三、如何使用
- 1.直接调用类和函数
- 2.命令行模式(无需会编程)
一、需求
公司有一个比较坑爹的报销方案,需要根据一堆碎的发票中,凑出一个目标金额,要求误差在1块钱以内
缺点:每次人肉去对比,浪费大量的时间。
大概是这样的,新建一个excel表格,将所有的金额录入,然后自己勾选发票,直到目标金额出现,如下图
二、实现思路
最差方案:全组和
使用全组合,搜索所有组合方案,遍历满足的结果输出,时间复杂度为O(n!),原先调用了python的排列组合函数实现,结果卡得不行,有时候能把程序跑挂了中等方案:回溯暴力破解
利用回溯输出,存在重复递归,时间复杂度为O(2^n),一般来说已经满足正常需求,但是如果n很大,还是影响性能最优方案:动态规划
时间复杂度为O(n*w),为最快的方案
通过动态规划思想实现,空间换时间,200个碎票匹配1万的金额秒出结果,大概使用800M内存,
代码已经贴到github:chenqionghe/amount-calculator
核心代码如下
package main
import (
"fmt"
"github.com/shopspringhttps://img.qb5200.com/download-x/decimal"
"strconv"
"time"
)
type InvoiceCounter struct {
maxValue int //期望值(单元为分)
items []int //发票金额(单元为分)
overflow int //允许的误差值(单元为分)
}
//items:所有发票 maxValue:目标金额 overflow:允许误差金额
func NewInvoiceCounter(items []float64, maxValue float64, overflow float64) *InvoiceCounter {
obj := &InvoiceCounter{}
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 *InvoiceCounter) 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 *InvoiceCounter) centToDollar(v int) float64 {
value := float64(v)
value, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", value/100), 64)
return value
}
//执行计算,返回所有方案
func (this *InvoiceCounter) Run() [][]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 j-items[i] >= 0 && states[i-1][j-items[i]] == true {
res = append(res, this.getSelected(states, items, i, j))
}
}
}
}
return res
}
//获取所有选中的元素(倒推)
func (this *InvoiceCounter) 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重量不变,直接进入下一次
}
}
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 *InvoiceCounter) 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\n", 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界面,返回给前端,例如以下
这种方式适合在自己开发的程序中使用,比如要出一个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]
输出了所有匹配的组合,和组合金额的总和。
命令行模式其实更方便,不用每次修改函数再编译使用,也无需知道编程语言如何使用,只需编译出对应平台的版本就行。
使用简单效率更高,如果经常使用,建议编译成自己的工具使用。
加载全部内容