这篇文章其实不全算转载,因为文章的后面一部分我进行了改动并用自己的不同的思路进行了验证。但是,文章的前面大部分我都没有改动,所以也不好意思归类为原创了。原文作者是大才子matrix67,原文地址: http://www.matrix67.com/blog/archives/366
考虑一个事件,它有两种概率均等的结果。比如掷硬币,出现正面和反面的机会是相等的。现在我们希望知道,如果我不断抛掷硬币,需要多长时间才能得到一个特定的序列。
序列一:反面、正面、反面
序列二:反面、正面、正面
首先,我反复抛掷硬币,直到最近的三次抛掷结果形成序列一,然后我记下这次我抛掷了多少次才得到了我要的序列。重复执行这个过程,我可以算出得到序列一平均需要的抛掷次数。同样地,反复抛掷硬币直到序列二产生,它所需要的次数也有一个平均值。你认为这两个平均值哪一个大哪一个小?换句话说,出现序列一平均所需的抛掷次数少还是出现序列二平均需要的次数少?
大多数人会认为,两个序列会以同样快的速度出现,因为在所有“正”和“反”的8种三元组合里,“反正反”和“反正正”各占1/8,其概率是均等的。而事实上,我们将会看到掷出序列二所需的次数更少一些。不妨考虑这样一个问题:在由“正”和“反”构成的n位01序列中,有多少个序列以序列一结尾但之前不曾出现过序列一?有多少个序列以序列二结尾但之前不曾出现过序列二?当n比较小时,两者答案是一样的(例如n=3时符合要求的情况都是唯一的),但到后来n越大时,两者的差距越明显:后者的个数总比前者的个数要多一些。不妨看一看n=6的情况。对于序列一,只有以下5个序列是符合要求的:
- 反反反反正反
- 反正正反正反
- 正正正反正反
- 正反反反正反
- 正正反反正反
但对于序列二来说,符合条件的序列就有7个:
- 反反反反正正
- 反正反反正正
- 反反正反正正
- 正反反反正正
- 正正反反正正
- 正正正反正正
- 正反正反正正
你可以通过计算机编程枚举,计算一下n为其它值的情况。计算结果和刚才也一样:在n位01序列中,以序列二结尾但之前不含序列二的情况不会少于以序列一结尾但之前不含序列一的情况。这说明,抛掷第n次硬币后恰好出现了序列二,其概率不会小于恰好出现序列一的概率。显然,当n渐渐增大时,这个概率应该呈下降趋势;同时,随着n的增长,两个序列各自出现的概率由相等开始慢慢拉开差距,第n次抛掷产生序列二的概率下降得要缓慢一些,或者说更多的情况集中发生在n更小的时候。因此总的来说,出现序列二所需要的抛掷硬币次数的期望值更小。
虽然我们通过一系列的观察验证了这个结论,并且我们也相信这个结论是正确的(虽然没有严格的证明),但我们仍然不是很接受这个结论。这种情况是有悖于我们的直觉的,它与我们的生活经验不相符合。此刻,我们迫切需要一个解释,来说明这种出人意料的反常现象产生的原因。
如果不亲自做几次试验的话,你很难体会到这种微妙的差距。考虑整个游戏的实际过程,“反正正”序列显然会出现得更早一些。假如某一次我们得到了序列“反正”。如果我们需要的是“反正反”序列,那么下一次抛掷结果为反面将结束本轮的抛掷,而下一次是正面则前功尽弃,你必须再次从零开始。如果我们需要的是“反正正”序列,那么下一次抛掷结果为正面将结束本轮的抛掷,而下一次是反面的话我至少不会惨到一切归零,这相当于我已经有了一个反面作为新的开头,只需再来两个正面即可。这样看的话,提前掷出“反正正”的可能性更大一些。
反复体会上面的想法,了解KMP算法网友会恍然大悟:这就是KMP算法的基本思路!考虑这样一个问题:我们在当前字串中寻找子串“反正正”第一次出现的位置。假如当前已经能匹配模式串的前两个字“反正”,主串中的下一个字是“正”则匹配成功,主串的下一个字是“反”则将使模式串的当前匹配位置退到第一个字。考虑一个更复杂的例子:我们希望在主串中寻找子串abbaba,现在已经在主串中找到了abbab。如果主串下一个字符是a,则成功匹配;如果主串下一个字符是b,则模式串最多能匹配到的位置退到了第三个字符,我只需要从abb开始继续匹配,而不必一切从头再来。
我们可以用KMP算法完美地解决上面的问题。(KMP算法的代码摘自我之前的博文)
首先我们构造两个模式串010和011,分别代表投掷硬币中的反正反和反正正(0代表反,1代表正)。然后再构造N位的所有2进制数排列(代表所有可能出现的硬币正反面组合)作为主串。然后定义两个计数量,通过KMP算法进行主串和模式串的匹配,010分别与所有的二进制数进行匹配,匹配成功一次计数量1加一,统计出010的总共匹配成功次数。同理,统计011的总共匹配成功次数。然后比较两个计数量的大小,即可得到我们想要的结论:到底反正反和反正正哪个更容易出现。
#include<iostream>
#include<cstring>
using namespace std;
void getNext(char *p,int *next)//p为模式串
{
int j,k;
next[0]=-1;
j=0;
k=-1;
while(j<strlen(p)-1)
{
if(k==-1||p[j]==p[k]) //匹配的情况下,p[j]==p[k]
{
j++;
k++;
if (p[j] != p[k])
next[j] = k;
else
//因为不能出现p[j] = p[ next[j]],所以当出现时需要继续递归,k = next[k]
next[j] = next[k];
}
else //p[j]!=p[k]
k=next[k];
}
}
int KMP(char *s,char *p)//s为主串,p为模式串
{
int next[strlen(p)];
int i,j;
i=0;
j=0;
getNext(p,next); //得到模式串的next数组
while(i<strlen(s))
{
if(j==-1||s[i]==p[j])
{
i++;
j++;
}
else
j=next[j];
if(j==strlen(p))
return i-j;//返回匹配的初始位置
}
return -1;
}
int main(){
char s[1000][10];
int i,j,N,count1=0,count2=0;
for(N=1;N<10;N++)
{for(i=0;i<(1<<N);i++)
{for(j=0;j<N;j++)
s[i][N-1-j]=(i&(1<<j))?'1':'0';//通过位运算构造所有二进制组合
s[i][N]='\0';}
char a[]="010";
char b[]="011";
for(i=0;i<(1<<N);i++)
if(KMP(s[i],a)!=-1)
count1++;
for(i=0;i<(1<<N);i++)
if(KMP(s[i],b)!=-1)
count2++;
cout<<"N:"<<N<<endl;
cout<<"反正反序列:"<<count1<<" ";
cout<<"反正正序列:"<<count2<<endl;}
return 0;
}