作为更大的模拟程序(我很乐意分享,但与问题无关)的一部分,我遇到了以下问题,我正在寻找一个好的算法。
问题: 给出一个长度为n的浮点数组f(带有元素f_1,...,f_n),指定n维空间中的一个点。可以公平地假设f的和为0.0(受浮点精度限制)。
寻求的: 找到长度为n的整数数组i(带有元素i_1,...,i_n),指定n维空间中的网格点,使得i的总和恰好为0和d(f,i),这是一个合适的度量f和i之间的距离最小化。
对于合适的度量,我认为最好的一个是最小化相对误差的一个(即,(i_j / f_j-1)^ 2的j之和,但是最小化普通欧氏距离(即总和)超过j(i_j-f_j)^ 2)也可能有用。
我最容易想到的最佳算法是在i网格上猜测一个合适的点(总和为0),然后以最小距离重复切换到相邻网格点(总和0),直到所有邻居都有一个更大的距离距离。鉴于距离函数的凹度,这应该集中在解决方案上。
但这种算法看起来很笨重。谁能做得更好?
在How to round floats to integers while preserving their sum?进行了相关讨论,但未达到我正在寻找的最优点。
背景附录: 如果感兴趣(也因为我觉得它很酷),让我指出问题出现的背景。
问题出现在交易模拟的一部分(这是更大模拟的一部分)。在每个地点,代理商提供交易许多商品。由于每个地点和商品都是单独处理的,我们可以专注于一个地点和商品,并逐一处理它们。
每个代理商j具有货币量c_j和商品q_j的数量,它们必须保持不变。每个代理人还指定一个实值,连续,非负,非单调递减的需求函数d_j(p),它基本上代表了代理人希望以任何给定价格拥有的商品单位数。
交易按以下步骤执行。
答案 0 :(得分:3)
这很容易。使用下面的算法定义i k 。 i k 的总和将始终为0,欧几里德距离将始终最小。
if (f[k] < 0)
i[k] = int(f[k]-0.5);
else
i[k] = int(f[k]+0.5);
int()是一个返回浮点数的整数部分的函数。它会截断浮点。
让我们定义e k ,使得f k = i k + e k 。
对于n = 2,f 0 = -f 1 。两个f k 具有相同的幅度但符号相反。通过向0舍入,两个误差也具有相同的幅度,但符号相反。因为sum(f)= sum(i)+ sum(e)和sum(e)和sum(f)等于0,sum(i)= 0。
除了平衡两个误差的大小之外,通过舍入到最接近的整数,我们最小化误差。总和(e 2 )将是最小的。
我们如上所述计算i k 。然后,和(i)可以取值-1,0或1.
当sum(i)= -1时,我们必须递增一个i k 。选择具有最大e k 的i k (所有e k 均为正)。
当sum(i)= 1时,我们必须递减i k 中的一个。选择具有最小e k 的i k (所有e k 均为负数)。
当n = 3时,我们有3个错误值| e k | &LT; 0.5。结果|和(e)|永远不会是2或更多。由于sum(i)只能取整数值,sum(e)只能取值-1,0或1,而sum(i)也取值。
|总和(E)|当所有e k 具有相同的符号时,= 1。这是因为| e k | &LT; 0.5。您总是需要三个相同符号的错误才能达到1或-1。注意,这在统计上不如它们具有不同符号和sum(i)= 0的情况那么频繁。
我们如何决定选择哪个 k ?
当sum(i)= 1时,则sum(e)= -1并且所有e k 因此为负。 我们必须减少一个i k 。递减一个i k 将通过递增其e k 来平衡,因为sum(i)+ sum(e)= 0.因此我们应该选择e k < / sub>以便递增它,产生最小幅度的错误。这是最接近-0.5的e k ,因此是最小的e k 。这确保总和(e 2 )最小。
当sum(i)= -1时,同样的逻辑适用。在这种情况下,sum(e)= 1并且所有e k 都是正的。递增一个i k 通过递减其e k 来平衡。通过选择最接近0.5的e k ,我们得到最小的和(e 2 )。
在这种情况下,sum(i)仍然限于值-1,0和1.
当sum(i)= -1时,选择最大的e k 。
当sum(i)= 1时,选择最小的e k 。
| sum(e)|无法达到2.这就是sum(i)限于值-1,0和1的原因。
与n = 3的差异是现在我们可能有3或4个e k 具有相同的符号。但是通过应用上述规则,总和(e 2 )仍然是最小的。
当n> 4 |总和(e)|可以大于1.在这种情况下,我们必须修改多个i k 。
一般算法如下
sum(i) -> m
when m = 0,
we are done.
when m < 0,
increment the m i<sub>k</sub> with biggest e<sub>k</sub>.
when m > 0,
decrement the m i<sub>k</sub> with smallest e<sub>k</sub>.
这是python 2.7代码
def solve(pf):
n = len(pf)
# construct array pi from pf
pi = [round(val) for val in pf]
print "pi~:", pi
# compute the sum of the integers
m = sum(val for val in pi)
print "m :", m
# if the sum is zero, we are done
if m == 0:
return pi
# compute the errors
pe = [pf[k]-pi[k] for k in xrange(n)]
print "pe :", pe
# correct pi when m is negative
while m < 0:
# locate the pi with biggest error
biggest = 0
for k in xrange(1,n):
if pe[k] > pe[biggest]:
biggest = k
# adjust this integer i
pi[biggest] += 1
pe[biggest] -= 1
m += 1
# correct pi when m is positive
while m > 0:
# locate the pi with smallest error
smallest = 0
for k in xrange(1,n):
if pe[k] < pe[smallest]:
smallest = k
# adjust this integer i
pi[smallest] -= 1
pe[smallest] += 1
m -= 1
return pi
if __name__ == '__main__':
print "Example case when m is 0"
pf = [1.1, 2.2, -3.3]
print "pf :", pf
pi = solve( pf )
print "pi :", pi
print "Example case when m is 1"
pf = [0.6, 0.7, -1.3]
print "pf :", pf
pi = solve( pf )
print "pi :", pi
print "Example case when m is -1"
pf = [0.4, 1.4, -1.8]
print "pf :", pf
pi = solve( pf )
print "pi :", pi
答案 1 :(得分:2)
这是一个整数编程问题。分支和边界是一种简单的方法,在实践中具有良好的边界条件,可以非常有效。
我实施了一个简单的分支定界算法。主要思想是为数组的每个成员尝试下一个更高和更低的整数。在每一步,我们都会尝试先减少损失。一旦我们找到了潜在的解决方案,如果解决方案比我们迄今为止发现的最佳解决方案更好,我们会保留它,然后我们回溯。如果在任何时候我们发现我们的部分解决方案的损失比我们发现的最佳总损失更糟,那么我们就可以修剪那个分支。
这是一个基本的品牌和绑定解决方案的Python实现。有许多方法可以进一步优化,但这显示了基本的想法:
from sys import stderr, stdout
from math import floor, ceil
import random
def debug(msg):
#stderr.write(msg)
pass
def loss(pf,pi):
return sum((pf[i]-pi[i])**2 for i in range(0,len(pf)))
class Solver:
def __init__(self,pf):
n = len(pf)
self.pi = [0]*n
self.pf = pf
self.min_loss = n
self.min_pi = None
self.attempt_count = 0
def test(self):
"""Test a full solution"""
pi = self.pi
pf = self.pf
assert sum(pi)==0
l = loss(pf,pi)
debug('%s: %s\n'%(l,pi))
if l<self.min_loss:
self.min_loss = l
self.min_pi = pi[:]
def attempt(self,i,value):
"""Try adding value to the partial solution"""
self.pi[i] = int(value)
self.extend(i+1)
self.attempt_count += 1
def extend(self,i):
"""Extend the partial solution"""
partial = self.pi[:i]
loss_so_far = loss(self.pf[:i],partial)
debug('%s: pi=%s\n'%(loss_so_far,partial))
if loss_so_far>=self.min_loss:
return
if i==len(self.pf)-1:
self.pi[i] = -sum(partial)
self.test()
return
value = self.pf[i]
d = value-floor(value)
if d<0.5:
# The the next lower integer first, since that causes less loss
self.attempt(i,floor(value))
self.attempt(i,ceil(value))
else:
# Try the next higher integer first
self.attempt(i,ceil(value))
self.attempt(i,floor(value))
def exampleInput(seed):
random.seed(seed)
n = 10
p = [random.uniform(-100,100) for i in range(0,n)]
average = sum(p)/n
pf = [x-average for x in p]
return pf
input = exampleInput(42)
stdout.write('input=%s\n'%input)
stdout.write('sum(input)=%s\n'%sum(input))
solver=Solver(input)
solver.extend(0)
stdout.write('best solution: %s\n'%solver.min_pi)
stdout.write('sum(best): %s\n'%sum(solver.min_pi))
stdout.write('attempts: %s\n'%solver.attempt_count)
stdout.write('loss: %s\n'%loss(input,solver.min_pi))
assert sum(solver.min_pi)==0