稍微說點錢的事吧 - 分錢的方法 (3)
| MyDivider1 | |||
| id | factor | value | Diff. | 
| 0 | 40 | 0 | -0.4 | 
| 1 | 20 | 1 | 0.8 | 
| 2 | 20 | 0 | -0.2 | 
| 3 | 20 | 0 | -0.2 | 
| 
 | 
 | 
 | 
 | 
| SUM: | 100 | 1 | 0 | 
| Sum( | Diff | ) = 1.6 | |||
繼續上次的題目,先來看看上次結束時的表格。
第一位(id=0)出了40%的錢,卻少拿0.4元。
第二位(id=1)出了20%的錢,卻多拿0.8元。
這是不是不太公平呢?
那個Diff.,給他一個工程一點的名字叫做"誤差"好了。
不知道有沒有人注意到,上次的Print程式碼,算出表格內Diff^2 Sum這個值。我叫他"誤差絕對值和" (其實通常會用平方和,在(2)裡面是用平方和的)
大家對誤差這東西有什麼看法沒? 它應該是不可避免、但是愈接近零愈好的東西。
在整個系統中,誤差絕對值和也是不可避免、但愈接近零愈好的數值。
(2)的算法是完全無法解決這個問題的,這邊我要換個算法,其實非常單純:
1. 先發給所有人、發到夠,但不發到超過。這時所有人的Diff會是負值,而且會符合 | Diff | < 最小單位 p。
2. 所有人依Diff做排序,少最多的先拿一份最小單位 p,他會變成多最少的人。依序直到發完。
以下為證明,不想看可跳過:
證明法1
共多了N分最小單位p
排序後所有可拿到那N份的誤差分類到集合Q,
拿不到的誤差分類成集合R。
因為排序,q∈∀Q、r∈∀R , q< r< (q + p) 成立。
假設q不拿多的一份p,而讓 r 去拿,會使誤差平方和變小。
(q+p)2 + r2 > q2 + (r+p)2
q2 + 2qp + p2 + r2 > q2 + r2 + 2rp + p2
2qp > 2rp
q > r 與 q< r< (q + p) 矛盾,不成立。
故不存在任何Q、R間的交換可使誤差平方和變小,證明此分配法誤差平方和是最小的。
證明法2
若一個系統在分完錢後,可以找到一個多拿的人,將他多拿的一個刻度p,轉給一個少拿的人,並使他們兩個的誤差絕對值和減少,代表系統現在的狀態誤差絕對值和不是系統可能的最小值。
誤差為集合D。
某筆正的誤差dm,將一個刻度p轉給某筆負的誤差dn。
兩者原始誤差絕對值和 = dm + (-dn) = dm – dn
轉移後的誤差絕對值和 = (p – dm) + (dn + p) = 2p – (dm – dn)
誤差減少代表
dm – dn > 2p – (dm – dn)
2(dm – dn) > 2p
dm – dn > p
當dm = Dmax, dn = Dmin時,dm – dn 有最大值。
故,當 Dmax – Dmin >p 成立時,代表系統現在的狀態誤差絕對值和不是系統可能的最小值。
※至此可以作為單元測試的測試式。
又,dm – dn >p
dm – p > dn
dm – p 為拿到p 之前的誤差,若是排序後才分配,必定(dm – p ) < dn,故
dm – p >dn 不成立。
可反證排序後再分配的算法,誤差絕對值和是可能的最小值。
以下依照新的方式設計算法,看起來很長但實際有一半是註解。順便因應新的規則修改一下單元測試。
這個算法裡面有一些有趣的細節。另外,只能傳陣列、拿陣列也不太好用。故、待續!
不過在那之前,我應該會先寫寫另一個主題、敬請期待!
public class MyDivider2 : BaseDivider
{
    sealed protected override decimal[] divide(decimal[] factors, decimal totalValue, int digits)
    {
        return Divide(factors, totalValue, (decimal)Math.Pow(0.1, digits));
    }
    /// <summary>依照輸入的比例、總值、與最小單位,傳回分配的結果。</summary>
    /// <param name="totalValue">被分配的總值。</param>
    /// <param name="factors">各單位的權重。</param>
    /// <param name="graduation">刻度,就是最小單位</param>
    /// <returns>依照權重分配總值的結果。</returns>
    public decimal[] Divide(decimal[] factors, decimal totalValue, decimal graduation)
    {
        // 特殊狀況
        if (factors == null)
            throw new NullReferenceException();
        if (factors.Length == 0)
            return new decimal[0];
        if (factors.Length == 1)
            return new decimal[] { totalValue };
        int count = factors.Length;
        decimal[] values = new decimal[count];
        decimal fSum = factors.Sum();
        // 每股(factor)能分配到的錢(value)
        decimal valPerFact = totalValue / fSum;
        // 每股(factor)能分配到的刻度份數(graduation)
        decimal grdPerFact = valPerFact / graduation;
        // 函數: 計算某index的factor,可以分配到幾份的理論刻度(含小數點下)
        Func<int, decimal> getGraduationCount = i => grdPerFact * factors[i];
        // 函數: 計算某index的factor,小數點以下、領不到的刻度差
        Func<int, decimal> computeDiff =
            i =>
            {
                var _grd = getGraduationCount(i);
                // 實際領到的整數刻度 - 可領到的理論刻度 = 沒領到的差額
                return Math.Truncate(_grd) - _grd;
            };
        // 紀錄已分發的總值
        var currValueShared = 0M;
        for (int i = 0; i < count; i++)
        {
            values[i] = Math.Truncate(getGraduationCount(i)) * graduation;
            currValueShared += values[i];
        }
        // 還有幾個刻度能分
        int partCount = (int)Math.Truncate((totalValue - currValueShared) / graduation);
        // 如果在這邊就結束、當然很讚
        if (partCount == 0)
            return values;
        // 建立索引表,供排序
        int[] idxTable = new int[count];
        for (int i = 0; i < count; i++)
            idxTable[i] = i;
        // 依差作排序(很慢), 少最多的每人分得一單位
        // *排序(OrderBy)是最慢的部分
        var idxEnumerator = idxTable
            .OrderBy(i => computeDiff(i))
            .Take(partCount);
        // 多拿一個刻度後,會變成"多最少"的人
        foreach (int i in idxEnumerator)
            values[i] += graduation;
        return values;
    }
}
結果:
| MyDivider2 | |||
| id | factor | value | Diff. | 
| 0 | 40 | 1 | 0.6 | 
| 1 | 20 | 0 | -0.2 | 
| 2 | 20 | 0 | -0.2 | 
| 3 | 20 | 0 | -0.2 | 
| 
 | 
 | 
 | 
 | 
| SUM: | 100 | 1 | 0 | 
| Sum( | Diff | ) = 1.20 | |||