1. 程式人生 > >基於概率的公平抽獎、公開開獎演算法

基於概率的公平抽獎、公開開獎演算法

背景

最近,由於專案需要,在產品同事的合作下專門設計並開發了一個基於概率的抽獎、開獎程式。我們先看下需求:

需求

基於現有的使用者積分資訊開發一套世界盃抽獎、開獎程式。首先,每個使用者每天完成日常任務後可參與現金紅包抽獎,中獎概率為隨機的,不做人為隱形設定。但是系統可以配置現金獎池大小,根據獎池大小,每天最多發出相應金額的紅包。其次,現金紅包有不同的金額範圍限制,不同的範圍中獎概率不同。另外,根據使用者操作等,積分榜是實時變動的。活動結束後積分榜前三名和積分榜4-50中隨機抽取的一名,共4名小夥伴可獲得系統大獎。設計一個公開、公正的演算法從第4名至第50名中隨機抽取一個幸運兒。

設計

由於獎池和紅包部分相對複雜,所以編寫程式處理;隨機抽取幸運兒相對簡單,利用第三方資料來源和獨立公開演算法計算出幸運兒名次即可。

幸運的寶寶

這部分相對簡單,先說下規則:

LuckyNumber = Parameter % (50 - 3) + 4

Parameter取值為 中國銀行美元外匯牌價 2018/07/16 12:00前(世界盃結束後,平臺於07/16中午公開開獎)最後一條記錄的現匯買入價去小數點後的值;為了保證公平性或防止意外,可從多平臺取值,即LuckyNumber = (Parameter1 + Parameter2 + Parameter3) % (50 - 3) + 4

50 - 3的意思是隻有47人蔘與抽獎

餘數是從0開始的,而中獎者序號從4開始,所以+4

  • 舉例
    假如2018/07/16查詢得到如下外匯資訊:
    這裡寫圖片描述

    則:LuckyNumber = Parameter % (50 - 3) + 4 = 66132 % 47 + 4 = 3 + 4 = 7
    榜單第7名即為幸運使用者。

紅包抽獎

配置

  • level=N # 0-9
  • level.prize=p0, p1, p2, … , pn
  • level.rate=r0, r1 … , rn-1

配置示例

  • level=3
  • level.amount=100, 1000, 9000, 10000
  • level.rate=100, 20, 10

示例含義

紅包分為三個等級,[1.00, 10.00)的概率是1%(100/10000),[10.00-90.00)的概率是0.2%,[90.00-100.00]的概率是0.1%。

示例程式

package test;

import java.util.Random;

import com.alibaba.fastjson.JSONObject;

public class T {
    private static final int LEVEL = 3;
    private static final int[] LEVEL_AMOUNT = new int[] { 100, 1000, 9000, 10000 };
    private static final int[] LEVEL_RATE = new int[] { 100, 20, 10 };

    public static void main(String[] args) {
        System.out.println(prize());
    }

    /**
     * {"lucky":9282,"prize":false}
     * {"lucky":50,"amount":980,"level":0,"prize":true}
     * lucky:幸運數
     * prize:中獎標識,true-中獎,false-沒中獎
     * level:中獎等級
     * amount:中獎金額
     * @return
     */
    public static JSONObject prize() {
        JSONObject result = new JSONObject();
        Random r = new Random();

        // 伺服器每天第一次收到抽獎請求或相關請求時初始化當日獎池資訊,
        // 或定時器每天自動初始化獎池資訊(凌晨高併發請求時可能查不到當日獎池資訊)

        // 生成幸運數
        int lucky = r.nextInt(10000);
        result.put("lucky", lucky);

        // 宣告中獎等級,預設不中獎,值為-1;或中獎值為0,1,2
        int level = -1;

        // 若lucky < level.rate0,則level=0;
        // 否則,若lucky < level.rate0 + level.rate1,則level=1
        // ...
        int rate = 0;
        for (int i = 0; i < LEVEL_RATE.length; i++) {
            rate += LEVEL_RATE[i];
            if (lucky < rate) {
                level = i;
                break;
            }
        }

        if (level > -1) {
            result.put("prize", true);
            result.put("level", level);

            int amount = 0;
            // 最後一箇中獎等級的獎金範圍是閉區間,其他都是左閉右開
            if (level + 1 == LEVEL) {
                // nextInt(N)的範圍是[0,N),nextInt(N * 10) / 10的範圍是[0,N]
                int target = (int) (LEVEL_AMOUNT[level + 1] - LEVEL_AMOUNT[level] + 1) * 10;
                amount = r.nextInt(target) / 10 + (int) LEVEL_AMOUNT[level];
            } else {
                // nextInt(N-M)的範圍是[0, N-M),nextInt(N-M) + M的範圍是[M,N)
                int target = (int) (LEVEL_AMOUNT[level + 1] - LEVEL_AMOUNT[level]);
                amount = r.nextInt(target) + (int) LEVEL_AMOUNT[level];
            }

            int pool_unusedAmount = 1000000; // 查詢當日獎池可用金額

            // 獎池可用金額不足時,按獎池剩餘金額支付
            if (amount > pool_unusedAmount) {
                amount = pool_unusedAmount;
            }

            // 更新當日獎池資訊,儲存使用者中獎資料
            result.put("amount", amount);
        } else {
            result.put("prize", false);
        }

        return result;
    }
}