我需要根据列表中“基础”值的相对权重编写将在列表中按比例分配值的代码。简单地将“基础”值除以“基础”值的总和,然后将因子乘以原始值以按比例分配工作:
proratedValue = (basis / basisTotal) * prorationAmount;
但是,必须将此计算的结果舍入为整数值。舍入的效果意味着列表中所有项目的proratedValue总和可能与原始prorationAmount不同。
任何人都可以解释如何应用“无损”比例算法,该算法尽可能准确地在列表中按比例分配值,而不会出现舍入误差吗?
答案 0 :(得分:16)
这里的简单算法草图......
保证按比例分配的总金额等于输入的比例金额,因为您实际上从未实际修改过运行总计(您只需将其舍入值用于其他计算,不要将其写回)。现在处理整数舍入的问题,因为舍入误差将在运行总计中随时间累加,并最终在另一个方向上跨越舍入阈值。
基本示例:
Input basis: [0.2, 0.3, 0.3, 0.2]
Total prorate: 47
----
R used to indicate running total here:
R = 0
First basis:
oldR = R [0]
R += (0.2 / 1.0 * 47) [= 9.4]
results[0] = int(R) - int(oldR) [= 9]
Second basis:
oldR = R [9.4]
R += (0.3 / 1.0 * 47) [+ 14.1, = 23.5 total]
results[1] = int(R) - int(oldR) [23-9, = 14]
Third basis:
oldR = R [23.5]
R += (0.3 / 1.0 * 47) [+ 14.1, = 37.6 total]
results[1] = int(R) - int(oldR) [38-23, = 15]
Fourth basis:
oldR = R [37.6]
R += (0.2 / 1.0 * 47) [+ 9.4, = 47 total]
results[1] = int(R) - int(oldR) [47-38, = 9]
9+14+15+9 = 47
答案 1 :(得分:11)
TL; DR 算法,准确率最高(+ 20%),速度减慢70%。
在已接受的答案here以及answer中提出的类似性质的蟒蛇问题的推测算法。
测试结果(10,000次迭代)
Algorithm | Avg Abs Diff (x lowest) | Time (x lowest)
------------------------------------------------------------------
Distribute 1 | 0.5282 (1.1992) | 00:00:00.0906921 (1.0000)
Distribute 2 | 0.4526 (1.0275) | 00:00:00.0963136 (1.0620)
Distribute 3 | 0.4405 (1.0000) | 00:00:01.1689239 (12.8889)
Distribute 4 | 0.4405 (1.0000) | 00:00:00.1548484 (1.7074)
方法3的准确率提高了19.9%,执行时间比预期慢70.7%。
尽最大努力在分发金额方面尽可能准确。
通过在循环中进行多次传递,牺牲了准确性。
public static IEnumerable<int> Distribute3(IEnumerable<double> weights, int amount)
{
var totalWeight = weights.Sum();
var query = from w in weights
let fraction = amount * (w / totalWeight)
let integral = (int)Math.Floor(fraction)
select Tuple.Create(integral, fraction);
var result = query.ToList();
var added = result.Sum(x => x.Item1);
while (added < amount)
{
var maxError = result.Max(x => x.Item2 - x.Item1);
var index = result.FindIndex(x => (x.Item2 - x.Item1) == maxError);
result[index] = Tuple.Create(result[index].Item1 + 1, result[index].Item2);
added += 1;
}
return result.Select(x => x.Item1);
}
public static IEnumerable<int> Distribute4(IEnumerable<double> weights, int amount)
{
var totalWeight = weights.Sum();
var length = weights.Count();
var actual = new double[length];
var error = new double[length];
var rounded = new int[length];
var added = 0;
var i = 0;
foreach (var w in weights)
{
actual[i] = amount * (w / totalWeight);
rounded[i] = (int)Math.Floor(actual[i]);
error[i] = actual[i] - rounded[i];
added += rounded[i];
i += 1;
}
while (added < amount)
{
var maxError = 0.0;
var maxErrorIndex = -1;
for(var e = 0; e < length; ++e)
{
if (error[e] > maxError)
{
maxError = error[e];
maxErrorIndex = e;
}
}
rounded[maxErrorIndex] += 1;
error[maxErrorIndex] -= 1;
added += 1;
}
return rounded;
}
static void Main(string[] args)
{
Random r = new Random();
Stopwatch[] time = new[] { new Stopwatch(), new Stopwatch(), new Stopwatch(), new Stopwatch() };
double[][] results = new[] { new double[Iterations], new double[Iterations], new double[Iterations], new double[Iterations] };
for (var i = 0; i < Iterations; ++i)
{
double[] weights = new double[r.Next(MinimumWeights, MaximumWeights)];
for (var w = 0; w < weights.Length; ++w)
{
weights[w] = (r.NextDouble() * (MaximumWeight - MinimumWeight)) + MinimumWeight;
}
var amount = r.Next(MinimumAmount, MaximumAmount);
var totalWeight = weights.Sum();
var expected = weights.Select(w => (w / totalWeight) * amount).ToArray();
Action<int, DistributeDelgate> runTest = (resultIndex, func) =>
{
time[resultIndex].Start();
var result = func(weights, amount).ToArray();
time[resultIndex].Stop();
var total = result.Sum();
if (total != amount)
throw new Exception("Invalid total");
var diff = expected.Zip(result, (e, a) => Math.Abs(e - a)).Sum() / amount;
results[resultIndex][i] = diff;
};
runTest(0, Distribute1);
runTest(1, Distribute2);
runTest(2, Distribute3);
runTest(3, Distribute4);
}
}
答案 2 :(得分:2)
您遇到的问题是定义“可接受的”舍入策略是什么,或者换句话说,您要尝试最小化的是什么。首先考虑这种情况:列表中只有2个相同的项目,并且正在尝试分配3个单元。理想情况下,您希望为每个项目分配相同的金额(1.5),但这显然不会发生。你能做的“最好的”可能是分配1和2,或2和1.所以
然后,我选择1和2而不是0和3因为我认为你想要的是最小化完美分配和整数分配之间的差异。这可能不是你认为“一个好的分配”,这是一个你需要考虑的问题:什么会使分配比另一个好??
一个可能的值函数可以是最小化“总误差”,即分配与“完美”,无约束分配之间差异的绝对值之和。
听起来,Branch and Bound启发的东西可以发挥作用,但这并不重要
假设Dav解决方案总是产生满足约束条件的分配(我信任的情况就是这种情况),我认为不能保证给你“最佳”解决方案,“最好”由任何距离/拟合度量定义你最终采用。我的理由是这是一个贪婪的算法,在整数编程中,问题会导致你找到真正偏离最优解的解决方案。但是,如果你可以忍受“有点正确”的分配,那么我说,去吧! “最佳”地做到这一点听起来并不简单
祝你好运!
答案 3 :(得分:2)
确定。我很确定原始算法(如编写的)和发布的代码(如编写的)并不完全回答@Mathias概述的测试用例的邮件。
我对此算法的预期用途是稍微更具体的应用程序。而不是使用原始问题中显示的(@amt / @SumAmt)
计算%。我有一个固定的$金额,需要根据为每个项目定义的%拆分进行拆分或分散。拆分%总和为100%,但是,直接乘法通常会导致小数(当被强制舍入到整数$时)不会累加到我分开的总量。这是问题的核心。
我很确定@Dav的原始答案在(如@Mathias描述的)多个切片的舍入值相等的情况下不起作用。原始算法和代码的这个问题可以用一个测试用例来概括:
花费100美元并将其分为3种方式,使用33.333333%作为百分比。
使用@jtw发布的代码(假设这是原始算法的准确实现),给你一个错误的答案:为每个项目分配33美元(导致总计99美元),所以它没有通过测试。
我认为更准确的算法可能是:
( [Amount to be Split] * [% to Split] )
[Remainder] + ( [UnRounded Amount] - [Rounded Amount] )
Round( [Remainder], 0 ) > 1
或 ,当前项目是列表中的最后一项,则设置项目的分配= [Rounded Amount] + Round( [Remainder], 0 )
[Rounded Amount]
在T-SQL中实现,它看起来像这样:
-- Start of Code --
Drop Table #SplitList
Create Table #SplitList ( idno int , pctsplit decimal(5, 4), amt int , roundedAmt int )
-- Test Case #1
--Insert Into #SplitList Values (1, 0.3333, 100, 0)
--Insert Into #SplitList Values (2, 0.3333, 100, 0)
--Insert Into #SplitList Values (3, 0.3333, 100, 0)
-- Test Case #2
--Insert Into #SplitList Values (1, 0.20, 57, 0)
--Insert Into #SplitList Values (2, 0.20, 57, 0)
--Insert Into #SplitList Values (3, 0.20, 57, 0)
--Insert Into #SplitList Values (4, 0.20, 57, 0)
--Insert Into #SplitList Values (5, 0.20, 57, 0)
-- Test Case #3
--Insert Into #SplitList Values (1, 0.43, 10, 0)
--Insert Into #SplitList Values (2, 0.22, 10, 0)
--Insert Into #SplitList Values (3, 0.11, 10, 0)
--Insert Into #SplitList Values (4, 0.24, 10, 0)
-- Test Case #4
Insert Into #SplitList Values (1, 0.50, 75, 0)
Insert Into #SplitList Values (2, 0.50, 75, 0)
Declare @R Float
Declare @Results Float
Declare @unroundedAmt Float
Declare @idno Int
Declare @roundedAmt Int
Declare @amt Float
Declare @pctsplit Float
declare @rowCnt int
Select @R = 0
select @rowCnt = 0
-- Define the cursor
Declare SplitList Cursor For
Select idno, pctsplit, amt, roundedAmt From #SplitList Order By amt Desc
-- Open the cursor
Open SplitList
-- Assign the values of the first record
Fetch Next From SplitList Into @idno, @pctsplit, @amt, @roundedAmt
-- Loop through the records
While @@FETCH_STATUS = 0
Begin
-- Get derived Amounts from cursor
select @unroundedAmt = ( @amt * @pctsplit )
select @roundedAmt = Round( @unroundedAmt, 0 )
-- Remainder
Select @R = @R + @unroundedAmt - @roundedAmt
select @rowCnt = @rowCnt + 1
-- Magic Happens! (aka Secret Sauce)
if ( round(@R, 0 ) >= 1 ) or ( @@CURSOR_ROWS = @rowCnt ) Begin
select @Results = @roundedAmt + round( @R, 0 )
select @R = @R - round( @R, 0 )
End
else Begin
Select @Results = @roundedAmt
End
If Round(@Results, 0) <> 0
Begin
Update #SplitList Set roundedAmt = @Results Where idno = @idno
End
-- Assign the values of the next record
Fetch Next From SplitList Into @idno, @pctsplit, @amt, @roundedAmt
End
-- Close the cursor
Close SplitList
Deallocate SplitList
-- Now do the check
Select * From #SplitList
Select Sum(roundedAmt), max( amt ),
case when max(amt) <> sum(roundedamt) then 'ERROR' else 'OK' end as Test
From #SplitList
-- End of Code --
为测试用例生成最终结果集:
idno pctsplit amt roundedAmt
1 0.3333 100 33
2 0.3333 100 34
3 0.3333 100 33
尽可能接近(我在代码中有几个测试用例),这可以非常优雅地处理所有这些情况。
答案 4 :(得分:1)
这是apportionment问题,有许多已知方法。所有人都有一定的病态:阿拉巴马州悖论,人口悖论,或配额规则的失败。 (Balinski和Young证明没有任何方法可以避免这三种方法。)你可能想要一个遵循引用规则并避免阿拉巴马悖论的方法;人口悖论并不是一个问题,因为不同年份之间每月的天数差别不大。
答案 5 :(得分:0)