抢红包算法--四种抢红包算法对比(附源码)

还记着longlong ago

我还在做绿色征途手游版的时候

有天

策划同学要求同事

一定要优化下抢红包算法

本着划水第一

吃瓜并列第一的原则

于是

我听到了一堆数学名词

***定理

XXX公式

呼~

是我不配了

怪我没有好好学习

再仔细听一听

问题原来是

原先的红包分配算法是

先抢的人从总金额随机

后抢的人从剩余金额中随机

所以导致抢红包越早

抢的金额就越大

策划现在就想让金额稍微平均下

嚯,我直呼好家伙

就这个东西

有必要整那些有的没的吗

一会定理

一会公式的

最后他们的方案

也很务实

就是每个人先做个保底

再根据之前的红包算法进行分配

果然大隐隐于市

翠花,上酸菜

分析:

针对这个问题的解决方法,有四种(普通法,线段切割法,双倍随机法,投篮球法)。

前三种算法,网上基本都在流传,投篮球算法,是我自己瞎起的名字。

这个算法还是因为当年校招,面试北京涂鸦移动,面试官现场引导我一个问题,用了投篮球这个例子。而我写这篇博客的时候,想起那个算法,蛮适用的,因而叫他投篮球算法。

一、普通法:每次针对剩余总金额做一次随机,随机值就是第一个人的数值。这个算法也就是这个同事之前写的要求被优化的算法。这个算法的好处是完全随机,坏处是极有可能造成前人抢的太多,后人太少。例如,100元分给10个人,第一个人在0-100随机,均值为50。而第二个人的均值只剩25,之后是12.5。

       而他们修改后的方案,大致就是先有保底。然后再随机,大致算法如下:

//普通法    有保底
//iTotalGold总金额    iNum份数    iBaseGold保底金额
void oldThink(int iTotalGold, int iNum, int iBaseGold)
{
  if (iBaseGold*iNum > iTotalGold)
  {
    cout << "保底太多 " << iBaseGold<<endl;
    return;
  }
  iTotalGold -= (iBaseGold *iNum);
  std::vector<int> veGold(iNum);
  for (int i = 0; i < iNum-1; ++i)
  {
    int iAddNum = 0;
    if(iTotalGold != 0)
      iAddNum += rand() % (iTotalGold);
    veGold[i] = iAddNum + iBaseGold;
    iTotalGold -= iAddNum;
  }
  veGold[iNum - 1] = iTotalGold + iBaseGold;
  cout << "普通法:" << endl;
  copy(veGold.begin(), veGold.end(), ostream_iterator<int>(cout, " "));
  cout << endl;
} 
​
int main()
{
    int iTotalGold = 100;
    int iNum = 10; 
    int iBaseGold = 5;
    oldThink(iTotalGold, iNum, iBaseGold);
    
    return 0;
}

二、线段切割法:将总金额想象成一条那么长的线段,需要分割成num份,随机num-1次,将每次的随机值映射到该线段上。这样的好处是将随机交给程序,缺点是有小概率造成某个人分配过多。例如,100个人分10个红包,我们除了需要考虑随机值重合之外,每次完全随机,可能造成不够随机的情况。

//线段切割法    无保底
//iTotalGold总金额    iNum份数    iBaseGold保底金额
void cutline(int iTotalGold, int iNum)
{
  if (iNum > iTotalGold)
  {
    return;
  }
  if (iNum == iTotalGold)
  {
    for (int i = 0; i < iNum; ++i)
      cout << 1 << " ";
    cout << endl;
    return;
  }
  std::set<int> setGold;
  for (int i = 0; i < iNum-1; ++i)
  {
    while (1)
    {
      int iPos = rand() % iTotalGold;
      if (setGold.find(iPos) == setGold.end())
      {
        setGold.insert(iPos);
        break;
      }
    }
  }
  cout << "线段切割法(无保底):" << endl;
  int iPreLine = 0;
  for (auto &it : setGold)
  {
    cout << it - iPreLine << " ";
    iPreLine = it;
  }
  cout << iTotalGold - iPreLine << " ";
  cout << endl;
}
​
int main()
{
    int iTotalGold = 100;
    int iNum = 10; 
    int iBaseGold = 5;
    cutline(iTotalGold, iNum);
    return 0;
}

这种算法,相比于第一种已经非常好了。如果觉得过于随机,可以针对这种算法做保底策略。可以预见,效果也一定好于第一种。

三、双倍随机法:每次随机的时候,取0-每个人平均金额的2倍进行随机。这样已经基本完成了我们想要的样子,不错的方法。例如:100个人分10分,每次随机都是0-100/10*2去随机,基本可以保证每个人平均在10左右,是相当平均的算法。不过需要注意最后几个人可能已经不足20的情况。

//双倍随机法
//iTotalGold总金额    iNum份数    iBaseGold保底金额
void twobase(int iTotalGold, int iNum, int iBaseGold)
{
  if (iNum *iBaseGold > iTotalGold)
  {
    cout << "保底太多 " << iBaseGold << endl;
    return;
  }
  std::vector<int> veGold(iNum, iBaseGold);
  iTotalGold -= iNum*iBaseGold;
  int iBaseTmp = iTotalGold / iNum * 2;
  for (int i = 0; i < iNum - 1; ++i)
  {
    if (iTotalGold == 0)
      break;
    int iTmp = 0;
    if (iTotalGold >= iBaseTmp)
      iTmp = rand() % iBaseTmp;
    else
      iTmp = rand() % iTotalGold;
    veGold[i] += iTmp;
    iTotalGold -= iTmp;
  }
  veGold[iNum - 1] = iTotalGold;
  cout << "双倍随机法:" << endl;
  copy(veGold.begin(), veGold.end(), ostream_iterator<int>(cout, " "));
  cout << endl;
}
​
int main()
{
    int iTotalGold = 100;
    int iNum = 10; 
    int iBaseGold = 5;
    twobase(iTotalGold, iNum, iBaseGold);
  
    return 0;
}
​

从效果可以看出,这种随机方法,已经达到了非常棒的随机效果。几乎可以避免玩家的投诉了。

四、我自创的名字,投篮球法:之前总是用金额去除以人数num以寻求平均。而投篮球法,则是以金额的单元值最为基准。以上三种算法都在极力的寻求随机,而投篮球法则是为了保证完全平均。例如:100个人分10分,每次取金额的最小单元值,比如一块钱,然后把10个人当成篮筐,一块钱当成篮球,每次都去投篮。这样的好处是不用模仿完全随机,他本身就是完全随机,坏处也很明显,循环次数过多。

针对于他循环过多的缺点,我自己做了一层优化。采用最小金额的单元值,例如每次取三块、五块。这种优化,一来可以避免循环过多。二来也可以避免极端情况下过于随机的结果。

//投篮球法    有保底
//iTotalGold总金额    iNum份数    iBaseGold保底金额
void basketball(int iTotalGold, int iNum, int iBaseGold)
{
  if (iBaseGold*iNum > iTotalGold)
  {
    cout << "保底太多 " << iBaseGold << endl;
    return;
  }
  iTotalGold -= (iBaseGold *iNum);
  std::vector<int> veGold(iNum, iBaseGold);
  for (int i = 0; i < iTotalGold; i += 2) //这里基准值用了2,减少循环次数
  {
    int iPos = rand() % iNum;
    veGold[iPos] += 2;
  }
  cout << "投篮球法:" << endl;
  copy(veGold.begin(), veGold.end(), ostream_iterator<int>(cout, " "));
  cout << endl;
}
​
int main()
{
    int iTotalGold = 100;
    int iNum = 10; 
    int iBaseGold = 5;
    basketball(iTotalGold, iNum, iBaseGold); 
      
    return 0;
}

闲杂人等回避

我要装b了

从数学理论来说,只要随机次数足够多,那么结果一定是无限趋近于平衡的。所以这种算法,虽然循环次数过多,但是数据量够大的情况下,他一定是最优、最平衡的。

当然,算法服务于功能,上述结论局限于,策划希望大家拿到的都差不多。

最后,四种算法一起运行下,来对比下结果。

上图还存在一个小点,两次运行程序,得到的结果完全相同。这是因为rand函数在C++老版本里的随机因子是固定值的问题。关于随机因子和随机数,改天有空了再整理一篇文章专门讲述下。

这里,先解决下这种情况。只需要每次在程序运行时,重新给定随机因子就可以了。例如:srand((unsigned)time(0));

运行结果:

 源码:

#include <iostream>
#include <vector>
#include <stdlib.h>
#include <iterator>
#include <set>
#include <time.h>
using namespace std;
​
​
​
//普通法    有保底
//iTotalGold总金额    iNum份数    iBaseGold保底金额
void oldThink(int iTotalGold, int iNum, int iBaseGold)
{
    if (iBaseGold*iNum > iTotalGold)
    {
        cout << "保底太多 " << iBaseGold<<endl;
        return;
    }
    iTotalGold -= (iBaseGold *iNum);
    std::vector<int> veGold(iNum);
    for (int i = 0; i < iNum-1; ++i)
    {
        int iAddNum = 0;
        if(iTotalGold != 0)
            iAddNum += rand() % (iTotalGold);
        veGold[i] = iAddNum + iBaseGold;
        iTotalGold -= iAddNum;
    }
    veGold[iNum - 1] = iTotalGold + iBaseGold;
    cout << "普通法:" << endl;
    copy(veGold.begin(), veGold.end(), ostream_iterator<int>(cout, " "));
    cout << endl;
}
​
​
//线段切割法    无保底
//iTotalGold总金额    iNum份数    iBaseGold保底金额
void cutline(int iTotalGold, int iNum)
{
    if (iNum > iTotalGold)
    {
        return;
    }
    if (iNum == iTotalGold)
    {
        for (int i = 0; i < iNum; ++i)
            cout << 1 << " ";
        cout << endl;
        return;
    }
    std::set<int> setGold;
    for (int i = 0; i < iNum-1; ++i)
    {
        while (1)
        {
            int iPos = rand() % iTotalGold;
            if (setGold.find(iPos) == setGold.end())
            {
                setGold.insert(iPos);
                break;
            }
        }
    }
    cout << "线段切割法(无保底):" << endl;
    int iPreLine = 0;
    for (std::set<int>::iterator it = setGold.begin(); it != setGold.end(); ++it)
    {
        cout << *it - iPreLine << " ";
        iPreLine = *it;
    }
    cout << iTotalGold - iPreLine << " ";
    cout << endl;
}
​
​
//双倍随机法
//iTotalGold总金额    iNum份数    iBaseGold保底金额
void twobase(int iTotalGold, int iNum, int iBaseGold)
{
    if (iNum *iBaseGold > iTotalGold)
    {
        cout << "保底太多 " << iBaseGold << endl;
        return;
    }
    std::vector<int> veGold(iNum, iBaseGold);
    iTotalGold -= iNum*iBaseGold;
    int iBaseTmp = iTotalGold / iNum * 2;
    for (int i = 0; i < iNum - 1; ++i)
    {
        if (iTotalGold == 0)
            break;
        int iTmp = 0;
        if (iTotalGold >= iBaseTmp)
            iTmp = rand() % iBaseTmp;
        else
            iTmp = rand() % iTotalGold;
        veGold[i] += iTmp;
        iTotalGold -= iTmp;
    }
    veGold[iNum - 1] = iTotalGold;
    cout << "双倍随机法:" << endl;
    copy(veGold.begin(), veGold.end(), ostream_iterator<int>(cout, " "));
    cout << endl;
}
​
​
//投篮球法    有保底
//iTotalGold总金额    iNum份数    iBaseGold保底金额
void basketball(int iTotalGold, int iNum, int iBaseGold)
{
    if (iBaseGold*iNum > iTotalGold)
    {
        cout << "保底太多 " << iBaseGold << endl;
        return;
    }
    iTotalGold -= (iBaseGold *iNum);
    std::vector<int> veGold(iNum, iBaseGold);
    for (int i = 0; i < iTotalGold; i += 2) //这里基准值用了2,减少循环次数
    {
        int iPos = rand() % iNum;
        veGold[iPos] += 2;
    }
    cout << "投篮球法:" << endl;
    copy(veGold.begin(), veGold.end(), ostream_iterator<int>(cout, " "));
    cout << endl;
}
​
int main()
{
    srand((unsigned)time(0));
    int iTotalGold = 100;
    int iNum = 10;
    int iBaseGold = 5;
    oldThink(iTotalGold, iNum, iBaseGold);
    cutline(iTotalGold, iNum);
    twobase(iTotalGold, iNum, iBaseGold);
    basketball(iTotalGold, iNum, iBaseGold);
    return 0;
}

 关注公众号【头发头发等等我】,查看更多分享

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值