ホームページ  >  記事  >  バックエンド開発  >  ランダムな赤い封筒を生成するための PHP アルゴリズム

ランダムな赤い封筒を生成するための PHP アルゴリズム

PHPz
PHPzオリジナル
2017-04-04 14:31:352390ブラウズ

1. 背景の紹介

少し前、会社のビジネスでは、固定の赤い封筒とランダムな赤い封筒に分けられる赤い封筒を生成する必要がありました。最小値と最大値を指定する場合、少なくとも A 最大値である必要があります。最小値は存在できませんが、赤いエンベロープは最小値より小さくすることはできません。
これまでにこれを行ったことがなかったので、少し混乱したため、Baidu に行って調べてみました。見つけたすべての赤いエンベロープ アルゴリズムにはさまざまなバグがあることがわかりました。負の値が計算されたり、最大値を超えたりする可能性がありました。価値があるので、自分でセットすることにしました。

ランダムな赤い封筒を生成するための PHP アルゴリズム


2. 基本的なアイデア

乱数生成に関しては、このブロガーの悲惨な叔父のアイデアを借用しました:

原文: たとえば、 1 つの赤い封筒を N 人に渡すことは、実際には N パーセンテージのデータを取得することと同じです。条件は、これらの N パーセンテージの合計 = 100/100 です。これらの N パーセントの平均は 1/N です。 そして、これらの N パーセントのデータは正規分布に準拠します (ほとんどの値は平均値に近づきます)。
解釈: たとえば、1,000 元があり、50 枚の赤い封筒を送る場合、ランダムに 50 個の数字を選択し、$avg/(1/N) を使用してこれら 50 個の数字の平均 $avg を計算します。基本の $mixrand を取得し、ランダムに生成された 50 個の数値を $mixrand で割って、基本の $randVal に対する各数値の割合を取得し、$randVal に 1,000 元を乗じて、各赤い封筒の特定の金額を取得します。 。

何が起こっているのかまだわかりませんか?関係ないので一緒にコーディングしましょう!


ランダムな赤い封筒を生成するための PHP アルゴリズム


3. 話は安いので、コードを見せてください!

レッドエンベロープ生成コアアルゴリズム:
<?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;
    }
}
4. 散歩に引っ張り出してみる

基本コード:
各種初期値を設定

<?php
/**
 * Created by PhpStorm.
 * User: lufei
 * Date: 2017/1/4
 * Time: 22:49
 */
header(&#39;content-type:text/html;charset=utf-8&#39;);
ini_set(&#39;memory_limit&#39;, &#39;128M&#39;);

require_once(&#39;CreateReward.php&#39;);
require_once(&#39;Reward.php&#39;);

$total = 50000;
$num = 300000;
$max = 50;
$min = 0.01;

$create_reward = new CreateReward();

パフォーマンステスト:
memory_limitの制限があるため、5回の平均のみを測定し、結果はすべて1.6秒程度。

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);
}
実行結果:

ランダムな赤い封筒を生成するための PHP アルゴリズム


データチェック:
負の値の有無、最大値の有無、最大値の個数、最大値の有無を検出最小値より小さい値

$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 &#39;<hr>已生成红包总金额为:'.($sum/100).';总个数为:'.count($reward_arr).'<hr>';
//检测有没有小于0的值
echo "<br />最大值:".($val/100).',共有'.$max_count.'个最大值,共有'.$min_count.'个值比最小值小';
実行結果:

ランダムな赤い封筒を生成するための PHP アルゴリズム


正規分布図:
絵を描くとき、​​あまりにも多くの赤い封筒を与えないでください。ページはレンダリングされず、クラッシュします


ランダムな赤い封筒を生成するための PHP アルゴリズム


$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=&#39;font-size:12px;width:600px;border:1px solid #ccc;text-align:left;&#39;><tr><td>红包金额</td><td>图示</td></tr>";
foreach($show as $val)
{
    #线条长度计算
    $width=intval($num*$val*300/$total);
    echo "<tr><td> {$val} </td><td width=&#39;500px;text-align:left;&#39;><hr style=&#39;width:{$width}px;height:3px;border:none;border-top:3px double red;margin:0 auto 0 0px;&#39;></td></tr>";
}
echo "</table>";
操作結果:

ランダムな赤い封筒を生成するための PHP アルゴリズム


PS: 友人が、生成されたデータが数学的に以下に準拠しているかどうかを尋ねました。標準正規分布、私の数学は苦手なので、特に考えたこともありませんでした。ただ見た目が似ていたので、彼のものだと思いました。

この問題に遭遇したので、これを解決する必要があるので、PHPの組み込み関数
を使用して計算しました。データ量が少ない場合、計算結果はまだ比較的正規分布に近いですが、データ量が多くなると、データが増えてしまい、今は見ることができません。興味のある方は、その理由を調べてください。 PHPの4つの関数:stats_standard_deviation(標準偏差)、stats_variance(分散)、stats_kurtosis(尖度)、stats_skew(歪度) 上記の関数を使用するには、
インストール
stats拡張子@ダウンロードアドレス5が必要です。この時点で、赤い封筒を書き終えました。50元の昇給が得られるかどうかはわかりませんが、緊急のニーズは解決できるはずです。


ランダムな赤い封筒を生成するための PHP アルゴリズム

そうそう、このコードパッケージのダウンロードも残しました



ランダムな赤い封筒を生成するための PHP アルゴリズム


以上がランダムな赤い封筒を生成するための PHP アルゴリズムの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。