面試題:如何實現紅包算法


題目

例如一個人在群里發了100塊錢的紅包,群里有10個人一起來搶紅包,每人搶到的金額隨機分配。
紅包功能需要滿足哪些具體規則呢?
1. 所有人搶到的金額之和要等於紅包金額,不能多也不能少。
2. 每個人至少搶到1分錢。
3. 要保證紅包拆分的金額盡可能分布均衡,不要出現兩極分化太嚴重的情況。

解決方案

解決方法一

思路

二倍均值法:假設剩余紅包金額為m元,剩余人數為n,那么有如下公式:
每次搶到的金額 = 隨機區間 [0.01,m /n × 2 - 0.01]元
這個公式,保證了每次隨機金額的平均值是相等的,不會因為搶紅包的先后順序而造成不公平。
舉個例子如下:
假設有5個人,紅包總額100元。100÷5×2 = 40,所以第1個人搶到的金額隨機范圍是[0.01,39.99]元,在正常情況下,平均可以搶到20元。假設第1個人隨機搶到了20元,那么剩余金額是80元。80÷4×2 = 40,所以第2個人搶到的金額的隨機范圍同樣是[0.01,
39.99]元,在正常的情況下,還是平均可以搶到20元。假設第2個人隨機搶到了20元,那么剩余金額是60元。60÷3×2 = 40,所以第3個人搶到的金額的隨機范圍同樣是[0.01,39.99]元,平均可以搶到20元。以此類推,每一次搶到金額隨機范圍的均值是相等的。

代碼實現

package arithmetic.com.ty.binary;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class RedEnvelope {
    /**
     * 拆分紅包
     * 
     * @param totalAmount    總金額(以分為單位)
     * @param totalPeopleNum 總人數
     */
    public static List<Integer> divideRedPackage(Integer totalAmount, Integer totalPeopleNum) {
        List<Integer> amountList = new ArrayList<Integer>();
        Integer restAmount = totalAmount;
        Integer restPeopleNum = totalPeopleNum;
        Random random = new Random();
        for (int i = 0; i < totalPeopleNum - 1; i++) {
            //隨機范圍:[1,剩余人均金額的2倍-1] 分 
            int amount = random.nextInt(restAmount / restPeopleNum * 2 - 1) + 1;
            restAmount = restAmount - amount;
            restPeopleNum--;
            amountList.add(amount);
        }
        amountList.add(restAmount);

        return amountList;
    }

    public static void main(String[] args) {
        List<Integer> amountList = divideRedPackage(1000, 10);
        for (Integer amount : amountList) {
            System.out.println(" 搶到金額:" + new BigDecimal(amount).divide(new BigDecimal(100)));
        }
    }
}

缺點:這個方法雖然公平,但也存在局限性,即除最后一次外,其他每次搶到的金額都要小於剩余人均金額的2倍,並不是完全自由地隨機搶紅包。

解決方法二

思路

線段切割法:何謂線段切割法?我們可以把紅包總金額想象成一條很長的線段,而每個人搶到的金額,則是這條主線段所拆分出的若干子線段。

 

當N個人一起搶紅包的時候,就需要確定N-1個切割點。

因此,當N個人一起搶總金額為M的紅包時,我們需要做N-1次隨機運算,以此確定N-1個切割點。

隨機的范圍區間是(1, M)。當所有切割點確定以后,子線段的長度也隨之確定。這樣每個人來搶紅包的時候,只需要順次領取與子線段長度等價的紅包金額即可。

這就是線段切割法的思路。在這里需要注意以下兩點:

(1)當隨機切割點出現重復,如何處理   --- 重復了就重新切唄
(2)如何盡可能降低時間復雜度和空間復雜度 --- 這里我用鏈表,犧牲時間換取空間(排了個序),也可以犧牲空間節省時間(大數組)

代碼 

package arithmetic.com.ty.binary;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;

public class RedEnvelope {
    
    public static List<Integer> hongbao(int totalAmount, int totalNumber) {
        List<Integer> list = new ArrayList<>();
        if (totalAmount <= 0 || totalNumber <= 0) {
            return list;
        }
        
        Set<Integer> set = new HashSet<>();
        while (set.size() < totalNumber - 1) {
            //生成一個1~totalAmount的隨機數
            int random = ThreadLocalRandom.current().nextInt(1, totalAmount);
            set.add(random);
        }

        //使用set.toArray(new Integer[0])是為了保證轉成數組后不用轉型。因為不帶Integer[0]的話,轉過后是Object[]
        Integer[] amounts = set.toArray(new Integer[0]);
        //排序之后首先把數組中的第一位數放入List中
        Arrays.sort(amounts);
        list.add(amounts[0]);
        
        /**
         * 對排序后的數組進行如下操作。假如排序后的數組為{x1, x2, x3, x4, x5, x6}
         * 下面的規則就相當於是x2-x1+x3-x2+x4-x3+x5-x4+x6-x5=x6-x1。而x1已經在上面被添加到list中,因此現在list中數據總大小為x6。
         * 因此最后list.add(totalAmount - amounts[amounts.length - 1])時,也就=list.add(totalAmount - x6),總數為totalAmount
         */
        for (int i = 1; i < amounts.length; i++) {
            list.add(amounts[i] - amounts[i - 1]);
        }
        
        list.add(totalAmount - amounts[amounts.length - 1]);
        return list;
    }

    public static void main(String[] args) {
        List<Integer> list = hongbao(200, 20);
        System.out.println(list);
        System.out.println(list.stream().mapToInt(x -> x).sum());
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM