一、背景介紹
前一陣公司業務有一個產生紅包的需求,分為固定紅包和隨機紅包兩種,固定紅包沒什麼好說的了,隨機紅包要求指定最小值,和最大值,必須至少有一個最大值,可以沒有最小值,但任何紅包不能小於最小值。
以前從來沒做過這方面,有點懵B,於是去百度了一番,結果發現能找到的紅包演算法都有各種各樣的bug,要麼會算出負值,要麼超過最大值,所以決定自己擼一套出來。
二、基本思路
在隨機數產生方面,我借鑒了這位博主@悲慘的大爺的思路:
原文:比如要把1個紅包分給N個人,實際上就是相當於要得到N個百分比資料 條件是這N個百分比之和=100/100。這N個百分比的平均值是1/N。 並且這N個百分比資料符合一種常態分佈(多數值比較靠近平均值)。
解讀:比如我有1000塊錢,發50個 紅包,就先隨機出50個數,然後算出這50個數的均值$avg,用$avg/(1/N),就得到了一個基數$mixrand,然後用隨機出的那50個數分別去除以$mixrand,得到每個數相對基數的百分比$randVal,然後用$randVal乘以1000塊錢,就可以得到每個紅包的具體金額了。
還是不太清楚咋回事?沒關係,我們一起擼代碼!
三、Talk is cheap, show me your code!
紅包產生核心演算法:
<?php/* * Author:xx_lufei * Time:2016年9月14日09:55:36 * Note:紅包產生隨機演算法 */class Reward{ public $rewardMoney; #紅包金額、單位元 public $rewardNum; #紅包數量 #執行紅包產生演算法 public function splitReward($rewardMoney, $rewardNum, $max, $min) { #傳入紅包金額和數量,因為小數在計算過程中會出現很大誤差,所以我們直接把金額放大100倍,後面的計算全部用整數進行 $min = $min * 100; $max = $max * 100; #預留出一部分錢作為誤差補償,保證每個紅包至少有一個最小值 $this->rewardMoney = $rewardMoney * 100 - $rewardNum * $min; $this->rewardNum = $rewardNum; #計算出發出紅包的平均機率值、精確到小數4位。 $avgRand = 1 / $this->rewardNum; $randArr = array(); #定義產生的資料總合sum $sum = 0; $t_count = 0; while ($t_count < $rewardNum) { #隨機產出四個區間的額度 $c = rand(1, 100); if ($c < 15) { $t = round(sqrt(mt_rand(1, 1500))); } else if ($c < 65) { $t = round(sqrt(mt_rand(1500, 6500))); } else if ($c < 95) { $t = round(sqrt(mt_rand(6500, 9500))); } else { $t = round(sqrt(mt_rand(9500, 10000))); } ++$t_count; $sum += $t; $randArr[] = $t; } #計算當前產生的隨機數的平均值,保留4位小數 $randAll = round($sum / $rewardNum, 4); #為將產生的隨機數的平均值變成我們要的1/N,計算一下每個隨機數要除以的總基數mixrand。此處可以約等處理,產生的誤差後邊會找齊 #總基數 = 均值/平均機率 $mixrand = round($randAll / $avgRand, 4); #對每一個隨機數進行處理,並乘以總金額數來得出這個紅包的金額。 $rewardArr = array(); foreach ($randArr as $key => $randVal) { #單個紅包所佔比例randVal $randVal = round($randVal / $mixrand, 4); #算出單個紅包金額 $single = floor($this->rewardMoney * $randVal); #小於最小值直接給最小值 if ($single < $min) { $single += $min; } #大於最大值直接給最大值 if ($single > $max) { $single = $max; } #將紅包放入結果數組 $rewardArr[] = $single; } #對比紅包總數的差異、將差值放在第一個紅包上 $rewardAll = array_sum($rewardArr); $rewardArr[0] = $rewardMoney * 100 - ($rewardAll - $rewardArr[0]);#此處應使用真正的總金額rewardMoney,$rewardArr[0]可能小於0 #第一個紅包小於0時,做修正 if ($rewardArr[0] < 0) { rsort($rewardArr); $this->add($rewardArr, $min); } rsort($rewardArr); #隨機產生的最大值大於指定最大值 if ($rewardArr[0] > $max) { #差額 $diff = 0; foreach ($rewardArr as $k => &$v) { if ($v > $max) { $diff += $v - $max; $v = $max; } else { break; } } $transfer = round($diff / ($this->rewardNum - $k + 1)); $this->diff($diff, $rewardArr, $max, $min, $transfer, $k); } return $rewardArr; } #處理所有超過最大值的紅包 public function diff($diff, &$rewardArr, $max, $min, $transfer, $k) { #將多餘的錢均攤給小於最大值的紅包 for ($i = $k; $i < $this->rewardNum; $i++) { #造隨機值 if ($transfer > $min * 20) { $aa = rand($min, $min * 20); if ($i % 2) { $transfer += $aa; } else { $transfer -= $aa; } } if ($rewardArr[$i] + $transfer > $max) continue; if ($diff - $transfer < 0) { $rewardArr[$i] += $diff; $diff = 0; break; } $rewardArr[$i] += $transfer; $diff -= $transfer; } if ($diff > 0) { $i++; $this->diff($diff, $rewardArr, $max, $min, $transfer, $k); } } #第一個紅包小於0,從大紅包上往下減 public function add(&$rewardArr, $min) { foreach ($rewardArr as &$re) { $dev = floor($re / $min); if ($dev > 2) { $transfer = $min * floor($dev / 2); $re -= $transfer; $rewardArr[$this->rewardNum - 1] += $transfer; } elseif ($dev == 2) { $re -= $min; $rewardArr[$this->rewardNum - 1] += $min; } else { break; } } if ($rewardArr[$this->rewardNum - 1] > $min || $rewardArr[$this->rewardNum - 1] == $min) { return; } else { $this->add($rewardArr, $min); } }}
細節考慮:
下邊這段代碼用來控制具體的商務邏輯,按照具體的需求,留出固定的最大值、最小值紅包的金額等;
在代碼中調用產生紅包的方法時splitReward($total, $num,$max - 0.01, $min);,我傳入的最大值減了0.01,這樣就保證了裡面產生的紅包最大值絕對不會超過我們設定的最大值。
<?php class CreateReward{ /* * 產生紅包 * author xx 2016年9月23日13:53:38 * @param int $total 紅包總金額 * @param int $num 紅包總數量 * @param int $max 紅包最大值 * */ public function random_red($total, $num, $max, $min) { #總共要發的紅包金額,留出一個最大值; $total = $total - $max; $reward = new Reward(); $result_merge = $reward->splitReward($total, $num, $max - 0.01, $min); sort($result_merge); $result_merge[1] = $result_merge[1] + $result_merge[0]; $result_merge[0] = $max * 100; foreach ($result_merge as &$v) { $v = floor($v) / 100; } return $result_merge; }}
四、拉出來遛遛
基礎代碼:
設定好各種初始值
<?php/** * Created by PhpStorm. * User: lufei * Date: 2017/1/4 * Time: 22:49 */header('content-type:text/html;charset=utf-8');ini_set('memory_limit', '128M');require_once('CreateReward.php');require_once('Reward.php');$total = 50000;$num = 300000;$max = 50;$min = 0.01;$create_reward = new CreateReward();
效能測試:
因為memory_limit的限制,所以只測了5次的均值,結果都在1.6s左右。
for($i=0; $i<5; $i++) { $time_start = microtime_float(); $reward_arr = $create_reward->random_red($total, $num, $max, $min); $time_end = microtime_float(); $time[] = $time_end - $time_start;}echo array_sum($time)/5;function microtime_float(){ list($usec, $sec) = explode(" ", microtime()); return ((float)$usec + (float)$sec);}
運行結果:
資料檢查:
檢測有沒有負值,有沒有最大值,最大值有多少個,有沒有小於最小值的值;
$reward_arr = $create_reward->random_red($total, $num, $max, $min);sort($reward_arr);//正序,最小的在前面$sum = 0;$min_count = 0;$max_count = 0;foreach($reward_arr as $i => $val) { if ($i<3) { echo "<br />第".($i+1)."個紅包,金額為:".$val."<br />"; } if ($val == $max) { $max_count++; } if ($val < $min) { $min_count++; } $val = $val*100; $sum += $val;}//檢測錢是否全部發完echo '<hr>已產生紅包總金額為:'.($sum/100).';總個數為:'.count($reward_arr).'<hr>';//檢測有沒有小於0的值echo "<br />最大值:".($val/100).',共有'.$max_count.'個最大值,共有'.$min_count.'個值比最小值小';
運行結果:
常態分佈圖:
注意,出圖的時候,紅包的數量不要給的太大,不然頁面渲染不出來,會崩
$reward_arr = $create_reward->random_red($total, $num, $max, $min);$show = array();rsort($reward_arr);//為了更直觀的顯示常態分佈效果,需要將數組重新排序foreach($reward_arr as $k=>$value){ $t=$k%2; if(!$t) $show[]=$value;; else array_unshift($show,$value);}echo "設定最大值為:".$max.',最小值為:'.$min.'<hr />';echo "<table style='font-size:12px;width:600px;border:1px solid #ccc;text-align:left;'><tr><td>紅包金額</td><td>圖示</td></tr>";foreach($show as $val){ #線條長度計算 $width=intval($num*$val*300/$total); echo "<tr><td> {$val} </td><td width='500px;text-align:left;'><hr style='width:{$width}px;height:3px;border:none;border-top:3px double red;margin:0 auto 0 0px;'></td></tr>";}echo "</table>";
運行結果:
PS:有朋友問我產生的資料有沒有通過數學方法來驗證其是否符合標準常態分佈,因為我的數學不好,這個還真沒算過,只是看著覺得像,就當他是了。
既然遇到了這個問題,就一定要解決嘛,所以我就用php內建函數算了一下,算出來的結果在資料量小的時候還是比較接近常態分佈的,但是資料量大起來的時候就不能看了,我整不太明白這個,大家感興趣的可以找一下原因喲。
php的四個函數:stats_standard_deviation(標準差),stats_variance(方差), stats_kurtosis(峰度),stats_skew(偏度)
使用上面的函數需要安裝stats擴充@下載地址
五、In the end
到這裡,紅包就算是寫完啦,不知道能不能漲50塊工資,但應該能解決燃眉之急了。
哦對,還落下了這個代碼打包下載