C# 在一组值之间按比例分配(按比例分配)一个值
我需要编写代码,根据列表中“基”值的相对权重,在列表中按比例分配值。简单地将“基准”值除以“基准”值之和,然后将系数乘以原始值,在一定程度上按比例工作:C# 在一组值之间按比例分配(按比例分配)一个值,c#,math,C#,Math,我需要编写代码,根据列表中“基”值的相对权重,在列表中按比例分配值。简单地将“基准”值除以“基准”值之和,然后将系数乘以原始值,在一定程度上按比例工作: proratedValue = (basis / basisTotal) * prorationAmount; 但是,此计算的结果必须四舍五入为整数值。四舍五入的影响意味着列表中所有项目的按比例价值之和可能不同于原始的按比例金额 有谁能解释一下如何应用一种“无损”的按比例分配算法,在列表中尽可能准确地分配一个值,而不会出现舍入错误吗?这里是简
proratedValue = (basis / basisTotal) * prorationAmount;
但是,此计算的结果必须四舍五入为整数值。四舍五入的影响意味着列表中所有项目的按比例价值之和可能不同于原始的按比例金额
有谁能解释一下如何应用一种“无损”的按比例分配算法,在列表中尽可能准确地分配一个值,而不会出现舍入错误吗?这里是简单的算法示意图
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
您的问题是定义什么是“可接受的”舍入策略,或者换句话说,您试图最小化的是什么。首先考虑这种情况:你的列表中只有2个相同的项目,并试图分配3个单位。理想情况下,您希望为每个项目分配相同的金额(1.5),但这显然不会发生。您所能做的“最佳”可能是分配1和2,或2和1。所以
- 每个分配可能有多个解决方案
- 相同的项目可能不会收到相同的分配
对我来说,灵感来源的东西听起来是可行的,但这不是小事。
假设Dav解决方案总是生成满足约束的分配(我相信就是这种情况),我假设它不能保证为您提供“最佳”解决方案,“最佳”由您最终采用的距离/拟合度量定义。我这样做的原因是,这是一个贪婪算法,在整数规划问题中,它可以引导你找到真正偏离最优解的解。但如果你能接受“某种程度上正确”的分配,那么我说,去吧!“以最佳方式”做这件事听起来并不琐碎。
祝你好运 好的。我非常确定,原始算法(如编写的)和发布的代码(如编写的)并不完全符合@Mathias所概述的测试用例的要求 我打算使用这个算法是一个稍微更具体的应用程序。而不是使用原始问题中所示的
(@amt/@summant)
计算百分比。我有一个固定的$amount,需要根据为每个项目定义的百分比分割,在多个项目之间进行分割或分摊。但是,拆分%和为100%,直接乘法通常会产生小数(当被迫四舍五入为整$)加起来不等于我拆分的总数。这是问题的核心
我相当肯定@Dav的原始答案在多个切片的四舍五入值相等的情况下(如@Mathias所述)不起作用。原始算法和代码的这个问题可以用一个测试用例来总结:
拿100美元,按33.333%的百分比分成3份
使用@jtw发布的代码(假设这是原始算法的准确实现),您会得到错误的答案,即为每个项目分配33美元(总金额为99美元),因此测试失败
我认为更准确的算法可能是:
- 有一个从0开始的运行总数
- 对于组中的每个项目:
- 将未取整的分配金额计算为
([待拆分金额]*[%待拆分])
- 将累计余数计算为
[余数]+([未取整金额]-[取整金额])
- 如果
或当前项目是列表中的最后一项,则设置项目的分配=四舍五入([余数],0)>1
[四舍五入金额]+四舍五入([余数],0)
- 否则设置项目的分配=
[四舍五入金额]
- 对下一个项目重复上述操作
-- 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
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)
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);
}
}