在一组数中寻找加和最接近某个值的数组合

在一组数中寻找加和最接近某个值的数组合

今天碰到个小问题,就是需要在一组数中,找到加和数最接近某个值的一系列数。

比如:
[8.05, 6.98, 6.19, 5, 22.96,4.71,4.74,4.25,6.34,2.77,7.31,3.59,18.28,19.55]
中找到最接近84.01的一组数。

这个问题,所有的可能元素的加和组合数为16383,即:

Binomial[14, 1] + Binomial[14, 2] + Binomial[14, 3] + 
 Binomial[14, 4] + Binomial[14, 5] + Binomial[14, 6] + 
 Binomial[14, 7] + Binomial[14, 8] + Binomial[14, 9] + 
 Binomial[14, 10] + Binomial[14, 11] + Binomial[14, 12] + 
 Binomial[14, 13] + Binomial[14, 14]

用mathematica软件

如果求加和等于给定值的计算在mathematica软件中是很方便的,两句话就能搞定了。

array = Subsets[{8.05, 6.98, 6.19, 5, 22.96, 4.71, 4.74, 4.25, 6.34, 2.77, 7.31, 3.59, 18.28, 19.55}, 13];
Do[sumt = Total[array[[i]]];  If[sumt == 84.01, Print[array[[i]]]], {i, 1, Length[array]}]

结果为:

{6.98,6.19,5,22.96,4.71,4.74,4.25,7.31,3.59,18.28}

其中用到的函数是Subsets用于抽取list中元素的组合,13不带花括号则组合包括对所有数量小于等于13的情况。

如果要计算接近的情况,比如相差0.02的情况:

array = Subsets[{8.05, 6.98, 6.19, 5, 22.96, 4.71, 4.74, 4.25, 6.34, 
    2.77, 7.31, 3.59, 18.28, 19.55}, 13];
Do[err = Abs[Total[array[[i]]] - 84.01];
 If[err < 0.02, 
  If[err == 0, Print[err, array[[i]]]; Break[], 
   Print[err, array[[i]]]]], {i, 1, Length[array]}]

结果为:

0.02{8.05,6.19,22.96,4.71,4.25,18.28,19.55}
0.01{8.05,6.19,22.96,4.74,4.25,18.28,19.55}
0.02{8.05,22.96,4.25,7.31,3.59,18.28,19.55}
0.01{6.19,5,22.96,4.71,7.31,18.28,19.55}
0.02{6.19,5,22.96,4.74,7.31,18.28,19.55}
0.02{8.05,6.98,6.19,5,22.96,4.71,4.25,6.34,19.55}
0.02{8.05,6.98,5,22.96,4.25,6.34,7.31,3.59,19.55}
0.{6.98,6.19,5,22.96,4.71,4.74,4.25,7.31,3.59,18.28}

但实际上用mathematica软件做数值计算并不是非常合适,如果语句写的不好,那么很容易卡死。

使用python进行计算

其实最新想到的,并实践的是利用python来解决。这可能跟当前的知识应用状态相关,第一反应就是试图联系线性规划,如果有现成的求解器,那么就不用算了,但略一思索,似乎是不能用的。反而只是一些简单的加和,应该就可以解决。然后第二反应是怎么来解决,没有精确等于解的情况,我想这应该是一个范围,既能得到临界大于,也能得到临界小于的解。所以很自然想到排列,给出一个排列,那么从头计算到尾,自然能够得到最接近解的小于和大于的值。

所以首先实现了一个基于排列的蒙特卡洛方法解:

def MC_permutation():
	data1=[]
	res_data=[]
	res_j=0
	res_jp=0
	res_s1=0
	res_s2=0
	res_err=100.0
	outflag=False

	for i in range(200000):
		data1[:]=data[:]
		random.shuffle(data1) 
		#print("data1=",data1)
		sumt=0
		for j in range(len(data1)):
			sumts=sumt
			sumt=sumt+data1[j]
			if(sumt>suma):
				ds1=abs(sumts-suma)
				ds2=abs(sumt-suma)
				ds=min(ds1,ds2)
				if (ds < res_err):
					#print('')
					#print('ds=',ds)
					#print('i=',i,'data1=',data1)
					#print('j=',j,data1[j])
					#print('sum=',sumts,sumt)
					res_err=ds
					res_data[:]=data1[:]
					res_jp=j
					if (ds1 < ds2):
						res_j=j
					else:
						res_j=j+1
					#print(ds1,ds2)
					#print(j,res_j)
					res_s1=sumts
					res_s2=sumt
				if(ds<0.000001): outflag=True
				break
		if outflag: break

	print("")
	print('permutes=',res_data)
	print(res_jp,res_s1,res_s2)
	print('res_data=',sorted(res_data[:res_j]),' sum=',sum(res_data[:res_j]))
	print(res_s1,res_s2,res_err)

其核心是利用shuffle函数来进行排列的蒙特卡洛采样。

解决了问题以后,有点时间,我想看看利用组合其实也是一样的,那么遍历也是一样的,其实上面的mathematcia软件的计算就是遍历的方法。

基于组合的的蒙特卡洛方法解:

#利用组合的蒙特卡洛方法
def MC_combination():

	lendata=len(data)
	data1=[]
	res_data=[]
	res_st=0.0
	data1[:]=data[:]
	res_err=1000.0
	for i in range(20000):
		k=random.randrange(2,lendata+1) 
		data2=random.sample(data1, k)
		#print('i=',i,'k=',k)
		#print('data2=',data2)
		sumt=sum(data2)
		ds=abs(sumt-suma)
		if (ds < res_err):
			#print('')
			#print('ds=',ds)
			#print('i=',i,'data2=',data2)
			#print('k=',k)
			#print('sum=',sumt)
			res_err=ds
			res_data=data2
			res_st=sumt
			if( ds < 0.0000001): break
	
	print("")
	print('res_data=',sorted(res_data))
	print(res_st,res_err)

其核心是利用random.sample函数来进行组合的蒙特卡洛采样。

而遍历的方法为:

#利用遍历的方法
def sch_emuration():
	lendata=len(data)
	res_data=[]
	res_st=0.0
	res_err=1000.0
	i=0
	outflag=False
	for k in range(1,lendata+1):
		for da in itertools.combinations(data,k):
			i=i+1
			sumt=sum(da)
			#print('i=',i,' k=',k)
			#print('data=',da)
			#print('sumt=',sumt)
			ds=abs(sumt-suma)
			if (ds < res_err):
				#print('')
				#print('ds=',ds)
				#print('data=',da)
				#print('sum=',sumt)
				res_err=ds
				res_data[:]=da[:]
				res_st=sumt
				if(ds<0.0000001): 
					outflag=True
					break
					#pass
		if outflag: break
	
	print("")
	print('res_data=',sorted(res_data))
	print(res_st,res_err)

其核心是利用itertools.combinations遍历所有的组合。

运行结论:

三种方法都能快速的得到结果。

小结

运用mathematica和python对在一组数中寻找加和最接近某个值的数组合问题进行了求解。由于组合数不大,所以利用遍历的方法是不错的。
如果问题更大,那么可能用蒙特卡洛方法也许会在时间上取得优势。

附完整程序

#!/usr/bin/env python3
#_∗_coding: utf-8 _∗_


"""
在一组数中,找到加和数最接近某个值得一系列数。
比如在:
[8.05, 6.98, 6.19, 5, 22.96,4.71,4.74,4.25,6.34,2.77,7.31,3.59,18.28,19.55]
中找到最接近84.01的一组数
"""
import os
import string
import re
import sys
import datetime
import time
import copy
import json
import operator #数学计算操作符
import random
import itertools

data=[8.05, 6.98, 6.19, 5, 22.96,4.71,4.74,4.25,6.34,2.77,7.31,3.59,19.55] #18.28,
suma=84.01

#利用遍历的方法-列出附近的所有解
def sch_emuration_all(de):
	lendata=len(data)
	res_data=[]
	res_st=0.0
	res_err=1000.0
	i=0
	for k in range(1,lendata+1):
		for da in itertools.combinations(data,k):
			i=i+1
			sumt=sum(da)
			#print('i=',i,' k=',k)
			#print('data=',da)
			#print('sumt=',sumt)
			ds=abs(sumt-suma)
			if (ds < res_err):
				#print('')
				#print('ds=',ds)
				#print('data=',da)
				#print('sum=',sumt)
				res_err=ds
				res_data[:]=da[:]
				res_st=sumt
			if (ds <= de):
				print('err=%.2f'%ds,' sum=%.2f'%sumt,' data=',sorted(da))

	print("")
	print('res_data=',sorted(res_data))
	print(res_st,res_err)


#利用遍历的方法
def sch_emuration():
	lendata=len(data)
	res_data=[]
	res_st=0.0
	res_err=1000.0
	i=0
	outflag=False
	for k in range(1,lendata+1):
		for da in itertools.combinations(data,k):
			i=i+1
			sumt=sum(da)
			#print('i=',i,' k=',k)
			#print('data=',da)
			#print('sumt=',sumt)
			ds=abs(sumt-suma)
			if (ds < res_err):
				#print('')
				#print('ds=',ds)
				#print('data=',da)
				#print('sum=',sumt)
				res_err=ds
				res_data[:]=da[:]
				res_st=sumt
				if(ds<0.0000001): 
					outflag=True
					break
					#pass
		if outflag: break
	
	print("")
	print('res_data=',sorted(res_data),' sum=',sum(res_data))
	print(res_st,res_err)


#利用组合的蒙特卡洛方法
def MC_combination():

	lendata=len(data)
	data1=[]
	res_data=[]
	res_st=0.0
	data1[:]=data[:]
	res_err=1000.0
	for i in range(20000):
		k=random.randrange(2,lendata+1) 
		data2=random.sample(data1, k)
		#print('i=',i,'k=',k)
		#print('data2=',data2)
		sumt=sum(data2)
		ds=abs(sumt-suma)
		if (ds < res_err):
			#print('')
			#print('ds=',ds)
			#print('i=',i,'data2=',data2)
			#print('k=',k)
			#print('sum=',sumt)
			res_err=ds
			res_data=data2
			res_st=sumt
			if( ds < 0.0000001): break
	
	print("")
	print('res_data=',sorted(res_data),' sum=',sum(res_data))
	print(res_st,res_err)
	
	



#利用排列的蒙特卡洛方法
def MC_permutation():
	data1=[]
	res_data=[]
	res_j=0
	res_jp=0
	res_s1=0
	res_s2=0
	res_err=100.0
	outflag=False

	for i in range(200000):
		data1[:]=data[:]
		random.shuffle(data1) 
		#print("data1=",data1)
		sumt=0
		for j in range(len(data1)):
			sumts=sumt
			sumt=sumt+data1[j]
			if(sumt>suma):
				ds1=abs(sumts-suma)
				ds2=abs(sumt-suma)
				ds=min(ds1,ds2)
				if (ds < res_err):
					#print('')
					#print('ds=',ds)
					#print('i=',i,'data1=',data1)
					#print('j=',j,data1[j])
					#print('sum=',sumts,sumt)
					res_err=ds
					res_data[:]=data1[:]
					res_jp=j
					if (ds1 < ds2):
						res_j=j
					else:
						res_j=j+1
					#print(ds1,ds2)
					#print(j,res_j)
					res_s1=sumts
					res_s2=sumt
				if(ds<0.000001): outflag=True
				break
		if outflag: break

	print("")
	print('permutes=',res_data)
	print(res_jp,res_s1,res_s2)
	print('res_data=',sorted(res_data[:res_j]),' sum=',sum(res_data[:res_j]))
	print(res_s1,res_s2,res_err)


				
if __name__ == "__main__":

	#遍历的方法
	t1=time.process_time()
	sch_emuration()
	t2=time.process_time()
	print('time elapsed',t2-t1)

	#排列的MC方法
	t1=time.process_time()
	MC_combination()
	t2=time.process_time()
	print('time elapsed',t2-t1)
	

	#组合的MC方法
	t1=time.process_time()
	MC_permutation()
	t2=time.process_time()
	print('time elapsed',t2-t1)

	'''
	datan=sorted([3.59, 6.19, 7.31, 4.25, 18.28, 22.96, 4.71, 6.98, 5, 4.74])
	print(datan)
	print(sum(datan))
	'''

	print("")
	sch_emuration_all(0.05)
	'''

  • 6
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值