页面

2010年12月29日星期三

某厂商的产品推广活动中的漏洞

关键词:数论 概率 双色球

近日某国内IT厂商为推广旗下新产品,推出了一个活动,内容如下:

大奖第二波:新浪微博前1200名粉丝随机抽奖规则
12月28日还会产生第二位幸运大奖!为了能够取得一个公平的随机数,我们将采用12月28日晚21点30分开奖的双色球号码(http://www.zhcw.com/ssq/index.shtml)作为基数除以1200,得到的余数将作为获奖粉丝的号码(注:如果余数为0,则第1200位粉丝将获奖)
以下是前1200名粉丝名单:
....

我专门去了解了一下规则。双色球的奖是由6个红球和1个蓝球组成。其中6个红球是从01号到33号这33个球中随机选择6个球并从小到大排序。蓝球是从01号到16号这16个球中随机选择1个球,排在6个红球的后面。像上文所提到的12月28号的奖是这样的:

这样的规则下选出来的号码(4091721253101)由于各个位置上的数字不是完全随机,那么除以1200之后的余数当然也就不够随机了。说什么“一个公平的随机数”,该厂商一直给人很严谨的感觉,却犯了这么一个低级的错误。


为了证明我的推理,特别计算了一下号码中各位数字对余数的影响力。也就是将某个位上的数字从0变到9,看看余数将做出什么样的变化。


我们以号码00,00,00,00,00,00,00为起点进行分析,这个时候余数为0。当个位数从0开始每增加1时,余数也增加1。说明个位数对余数的影响是“+1”。同理,十位数对余数的影响是“+10”,百位数是“+100”,千位数是“+1000”,万位以后都是“+400”。因为是求余,有可能某位数增加的过程中,余数会因为满1200而减少,所以实际中各位数对余数的影响如下表所示:

影响
+1,-1199
+10,-1190
+100,-1100
+1000,-200
+400,-800
以下同 +400,-800

可以看出,对余数的个位数和十位数能施加影响的,只有号码的后两位可以做到,而且是后两位直接复制到余数。号码的后两位是什么数,余数的后两位就也是什么数。而号码的后两位是一个蓝球,它只能是1-16中的一个数值。显而易见,粉丝号码不满足这个条件的,当选的概率就是0了。

不仅如此,由于百位以上的数字变化也不是完全随机,那么剩下的这些满足条件的号码的概率也都是不相等的。由于各数字的变化非常微妙,单独纯理论计算各余数的出现概率似乎不太可行。我们借助计算机来模拟一下双色球的号码并计算号码除以1200所得各个余数的概率。

写计算机程序模拟演算之前,为了简化运算,我们有必要将模型简化一下。由于后两位是1-16的完全随机,对于(mnxy)型的余数,显然(mn01),(mn02),・・・,(mn16)的概率是一样的。所以我们只需要考察红球区的号码除以12所得余数的概率就可以了。

C#代码如下:

using System;
using System.Collections;

public class SimSSQ
{
[STAThread]
static void Main(string[] args)
{
//各余数的命中次数数组
int[,] probs = new int[,] {
{0,0},
{1,0},
{2,0},
{3,0},
{4,0},
{5,0},
{6,0},
{7,0},
{8,0},
{9,0},
{10,0},
{11,0},
};

//循环次数 1千万次 执行时间大约10秒
int rootTime = 10000000;

//随机数发生器,对于这次的应用,该发生器可以视为完全随机的
Random random = new Random();
long redNum;
int yu;

for (int i = 0; i < rootTime; i++)
{
//得到一个红球号码
redNum = getRedNum(random);

//求余
yu = (int)(redNum % 12L);

probs[yu, 1] = probs[yu, 1] + 1;
}

Console.WriteLine("余数\t命中次数\t概率");
for (int i = 0; i < 12; i++)
{
Console.WriteLine("{0}\t{1}\t\t{2}",
probs[i, 0], probs[i, 1], (float)probs[i, 1] / (float)rootTime);
}
Console.ReadLine();
}

//随机获得一个红球号码
private static long getRedNum(Random random)
{
//红球
int[] redBalls = new int[] {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31, 32, 33
};

//循环6次取6个不同的球 这个循环的方法很精巧 是从网上搜到的
int[] ballsGet = new int[6];
int index = 0;
for (int i = 0; i < 6; i++)
{
index = random.Next(0, 33 - i);
ballsGet[i] = redBalls[index];
if (index < 33 - i - 1)
{
redBalls[index] = redBalls[33 - i - 1];
}
}

//排序
int temp;
for (int i = 0; i < 5; i++)
{
for (int j = 0; j < 5 - i; j++)
{
if (ballsGet[j] > ballsGet[j + 1])
{
temp = ballsGet[j];
ballsGet[j] = ballsGet[j + 1];
ballsGet[j + 1] = temp;
}

}
}

return ballsGet[0] * 10000000000 + ballsGet[1] * 100000000
+ ballsGet[2] * 1000000 + ballsGet[3] * 10000 + ballsGet[4] * 100 + ballsGet[5];
}

}
 
  代码中循环1千万次来模拟1千万个号码,费时大约10秒。以防万一,将程序运行3遍,结果如下:
第一次运行:

余数 命中次数 概率
0 901619 0.0901619
1 1092387 0.1092387
2 601096 0.0601096
3 737644 0.0737644
4 900792 0.0900792
5 1094439 0.1094439
6 601951 0.0601951
7 739544 0.0739544
8 899516 0.0899516
9 1090694 0.1090694
10 602235 0.0602235
11 738083 0.0738083


第二次运行:


余数 命中次数 概率
0 900673 0.0900673
1 1090294 0.1090294
2 602277 0.0602277
3 741315 0.0741315
4 900915 0.0900915
5 1090490 0.109049
6 600926 0.0600926
7 739800 0.07398
8 901101 0.0901101
9 1091689 0.1091689
10 600657 0.0600657
11 739863 0.0739863


第三次运行:


余数 命中次数 概率
0 900186 0.0900186
1 1092241 0.1092241
2 601803 0.0601803
3 736812 0.0736812
4 901554 0.0901554
5 1092559 0.1092559
6 602827 0.0602827
7 738813 0.0738813
8 902931 0.0902931
9 1093280 0.109328
10 600721 0.0600721
11 736273 0.0736273


可以看出,三次运行结果高度一致。从运行结果上推断,(00xy),(04xy),(08xy)的概率相等,应该是0.09附近,(01xy),(05xy),(09xy)的概率是0.11附近,(02xy),(06xy),(10xy)的概率是0.06附近,(03xy),(04xy),(11xy)的概率是0.074附近。如果有概率方面的高手能手动计算出这个概率值,请务必让我景仰一下。


结语:厂商预期每个粉丝的当选概率都是1/1200即0.000833。从上面的分析看来,粉丝号段是101-116,501-516,901-916的人当选概率是最大的,达到了0.00683,几乎是预期值的10倍。而实际中是号码为1101的人获得了这个奖项,正是上面说的那期号码(4091721253101)除以1200的余数。

使用Facebook社交插件参与评论: