把一筆錢依特定的比例分給幾個人是我工作上常要處理的需求。由於金額必須四捨五入到元或分,因此常需面對除不盡的錢要設法攤掉的問題。例如100元平分給三個人,每人33元後,最後的1元要發給三人之一的幸運兒,變成一人34, 兩人33的分配結果。
以前年紀小不懂事,很直覺的想法是先用100*1/3四捨五入得到33把錢分一分,之後再跑一個迴圈(沒辦法,總不能打電話請這三個人過來猜拳吧?)把分剩的錢(總金額大、人數多時餘下數十上百元也是有可能滴)每次一元地發下去,直到發光為止。
說實在說,當初並不覺得這個寫法有什麼不對,直到有前輩指點了另一種更精巧的演算法,一口氣就能把錢攤到一毛不剩,省去分完一輪後處理餘數的麻煩。相形之下,原本的寫法挺笨拙的...
using System;
using System.IO;
using System.Threading;
public class CSharpLab
{
public static void Test()
{
//100萬元要依比例分給3個人(四拾五入計算到元)
int totalAmt = 1000000;
int[] amt = new int[3];
//三個人勢均力敵,100萬除以3會有餘1元的問題
//且看以下的邏輯演法如何避免事後分攤的困擾
decimal[] fact = new decimal[] { 1.2M, 1.2M, 1.2M };
//先加總分配權重
decimal factSum = 0;
for (int i = 0; i < fact.Length; i++)
factSum += fact[i];
//使用以下邏輯分配, 會自然吸收掉四捨五入差額
for (int i = 0; i < fact.Length; i++)
{
amt[i] = Convert.ToInt32(
Math.Round(totalAmt * fact[i] / factSum, MidpointRounding.AwayFromZero)
);
Console.WriteLine("{0} * {1} / {2} = {3}", totalAmt, fact[i], factSum, amt[i]);
totalAmt -= amt[i];
factSum -= fact[i];
}
}
}
var factor = [ 1.2, 1.2, 1.2 ], factorSum = 0;
var totalAmount = 1000000, amount = [];
for (var i = 0; i < factor.length; i++) factorSum += factor[i];
var r = "";
for (var i = 0; i < factor.length; i++)
{
amount[i] = parseInt(Math.round(totalAmount * factor[i] / factorSum))
r += totalAmount + " * " + factor[i] + " / " + factorSum + " = " + amount[i] + "\n";
totalAmount -= amount[i];
factorSum -= factor[i];
}
alert(r);
數字是對的,但明眼人已可看到其中暗藏殺機... 3.5999999999999996? 明明應該是3.6,因為Javascript裡只有浮點數,沒有像.NET decimal一樣分亳不差的精準數字型別,所以數字都是用逼近的。
var factor = [1.2, 1.2, 1.2], factorSum = 0;
var totalAmount = 1000000, amount = [];
function r2(n) { return parseFloat(n.toFixed(2)); }
for (var i = 0; i < factor.length; i++)
factorSum = r2(factorSum + factor[i]);
var r = "";
for (var i = 0; i < factor.length; i++)
{
amount[i] = parseInt(Math.round(totalAmount * factor[i] / factorSum))
r += totalAmount + " * " + factor[i] + " / " + factorSum + " = " + amount[i] + "\n";
totalAmount -= amount[i];
factorSum = r2(factorSum - factor[i]);
}
alert(r);