稍微說點錢的事吧 - 分錢的方法 (3)

  • 1903
  • 0
  • C#
  • 2012-09-08

稍微說點錢的事吧 - 分錢的方法 (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