目錄python
公司有一個比較坑爹的報銷方案,須要根據一堆碎的發票中,湊出一個目標金額,要求偏差在1塊錢之內
缺點:每次人肉去對比,浪費大量的時間。
大概是這樣的,新建一個excel表格,將全部的金額錄入,而後本身勾選發票,直到目標金額出現,以下圖
git
最差方案:全組和
使用全組合,搜索全部組合方案,遍歷知足的結果輸出,時間複雜度爲O(n!),原先調用了python的排列組合函數實現,結果卡得不行,有時候能把程序跑掛了github
中等方案:回溯暴力破解
利用回溯輸出,存在重複遞歸,時間複雜度爲O(2^n),通常來講已經知足正常需求,可是若是n很大,仍是影響性能spring
最優方案:動態規劃
時間複雜度爲O(n*w),爲最快的方案編程
經過動態規劃思想實現,空間換時間,200個碎票匹配1萬的金額秒出結果,大概使用800M內存,
代碼已經貼到github:chenqionghe/amount-calculatorapp
核心代碼以下編程語言
package main import ( "fmt" "github.com/shopspring/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 }
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種方案,就是這麼爽!工具
新建一個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]
輸出了全部匹配的組合,和組合金額的總和。 命令行模式其實更方便,不用每次修改函數再編譯使用,也無需知道編程語言如何使用,只需編譯出對應平臺的版本就行。 使用簡單效率更高,若是常常使用,建議編譯成本身的工具使用。