水木社区上看到的一道概率题

水木社区上看到的一道概率题

原帖是这样的:

发信人: runformore (奎罗伊), 信区: Mathematics

标 题: 问个谷歌面试的问题

发信站: 水木社区 (Mon Feb 1 03:51:24 2016), 站内

1维,1米长的路面,每次下一滴雨,每滴雨落到地面上长度是0.01米,落点假设均匀分布,求问下了多少滴雨之后路面会全部湿透,求期望?

这个题目有一处说的不严谨,需要特别解释一下。

雨滴落点假设均匀分布这句话应该理解为每个雨滴的中心均匀分布在1米长的路面上,也就是雨滴实际可以覆盖的长度是 1.01 米(两边都可以多出 0.005 米来)。

这个问题刚一看可能觉得挺简单的。但是仔细算算就发现真的很难。水木数学版上大家讨论了几天也只是得到了个无穷级数解。至今没有人能给出个有限项的闭式解来。这个无穷级数解是 dragonheart6 给出的,推导过程如下。
这里写图片描述
(dragonheart6 是水木社区 IQDoor 版的版主,智商超高。每次看他的帖子都有种智商被无情碾压的感觉)

关于这个推导这里不多解释。今天想说一说这个问题如何用编程用数值仿真实验来估算结果。

这个问题是个用 N 个小区间来覆盖一个大区间的概率问题。我们的区间都是定义在实数域上的。理论上来说有无穷多种小的区间,有无穷多种覆盖方式。模拟这个问题就有两种思路:

  1. 将大区间分成有限个小区间,假设每一滴雨都会覆盖连续的几个小区间。这样实际上就是把连续问题离散化了。这样处理编程会简单很多,但是这些小区间的份数的选择是个问题,份数太少计算出的结果会与原问题的结果有很大的出入。份数过多计算量会非常大。而这个份数的选取只能靠做一些数值实验来确定。

  2. 定义一个数据结构来表示区间,直接进行区间之间的运算。对于这个问题来说其实只有一种关键的运算,就是相交区间的合并运算。当这些小的区间(雨滴)合并为一个能够覆盖 [0, 1] 的区间后计算计数就可以停止了。

第一种思路编程实现很简单。没必要多说。这里主要介绍第二种思路的编程实现。我用到的编程语言是 C++,用 C++ 写这种代码还是挺麻烦的,如果用 Python 一类的语言可以写的更简单些。不过考虑到蒙特卡洛仿真程序属于典型的计算密集型程序。考虑到运算速度最终还是选择了 C++。

首先,我们定义了一个结构体来表示区间。

struct Interval
{
    Interval(double l, double u): low(l), up(u) {}
    inline void enlarge(Interval b);
    double low;
    double up;
};

// 试着将两个区间合并为一个区间
void Interval::enlarge(Interval b)
{
    if(b.low < low)
    {
        low = b.low;
    }
    if(b.up > up)
    {
        up = b.up;
    }
}

其中的 enlarge() 用来将两个区间扩充为一个大区间,这个扩充操作不需考虑两个区间是否相交。只是简单的扩展操作就可以。

由于我们要操作许多个区间,要经常进行区间的插入与删除操作。为了方便,我们用一个链表来存储这些区间。并且保证区间的下边界是升序排列。这里没有使用 STL 中的链表,用的是 Qt 里面的实现。这个链表的样子如下:

QLinkedList<Interval> list;

升序排列是靠插入时放到合适的位置来保证的。

// 将区间插入到 List 中,只要保证区间的下边界是升序排列就可以了
void insert(QLinkedList<Interval> &list, Interval interval)
{
    if(list.empty())
    {
        list.append(interval);
        return;
    }
    QLinkedList<Interval>::iterator iter;
    for (iter = list.begin(); iter != list.end(); ++iter)
    {
        if(iter->low >= interval.low)
        {
            list.insert(iter, interval);
            return;
        }
    }
    list.append(interval); // 到这里表示所有的现有区间的下边界都比这个区间的小,因此将和这个区间插入到最后
}

之所以要求这些区间的下边界是升序的,是为了能够方便的计算哪些区间可以合并,并快速的合并这些区间。下面的代码就是用来合并有交集的区间的,很简单,就是依次判断链表中相邻的两个区间是否有公共部分,有的话就合并。没有的话就去判断下一对区间。合并之后这些区间的下边界还是升序排列的。


// 合并相邻区间,要求区间的下边界是增序排列的
bool merge(QLinkedList<Interval> &list)
{
    QLinkedList<Interval>::iterator current;
    QLinkedList<Interval>::iterator next;
    for (current = list.begin(); current != list.end(); ++current)
    {
        next = current + 1;
        while(next != list.end() && current->up >= next->low)
        {
            current->enlarge(*next);
            list.erase(next);
            next = current + 1;
        }
    }
    return list.count() == 1;
}

这个函数的返回值是布尔类型,当 list 中只剩下一个区间时返回真,否则返回假。

之所以要这样设计,是因为我们会预先往这个list 中放两个区间。分别是 [-1, 0] 和 [1, 2]。 那么通过 merge 操作能够合并为一个区间时肯定就是雨滴完整的覆盖了[0, 1] 这个区间了。
还要有个随机生成一个区间(雨滴)的函数。


Interval rain_drop(double range)
{
    double center = static_cast<double> (rand()) / RAND_MAX;
    double min = center - range;
    double max = center + range;
    return Interval(min, max);
}

有了这几个函数就可以进行我们的数值实验了。我们将一次实验写为一个函数。

int test()
{
    QLinkedList<Interval> list;
    insert(list, Interval(-1, 0));
    insert(list, Interval(1, 2));
    int n = 0;
    do
    {
        insert(list, rain_drop(0.005));
        n++;
    }while( !merge(list) );
    return n;
}

最后是主程序,我们计算了 50000 次实验的结果。将其取平均值。


int main(int argc, char *argv[])
{
    double n = 0;
    std::srand((unsigned int)(std::time(NULL)));
    QTime t; t.start();
    for(int i = 0; i < 50000; i++)
    {
        n += test();
    }
    int tm = t.elapsed();

    n /= 50000.0;
    std::cout << "n = " << n << std::endl;
    std::cout << "Time elapsed: " << tm / 1000 << " s" << std::endl;
}

计算结果如下:

n = 726.782
Time elapsed: 75 s
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值