浅谈全体多项式概率分布的实现

yummy

2023-01-11 10:40:31

Personal

昨天晚上半夜发现自己写的东西假掉了,今天早上赶紧起来更新。

然后我发现昨天那玩意可以扩展到全体多项式,虽然求解 n 个复根仍然不现实。

最近翻了翻自己非常著名的 Kotori 的数据生成器,发现当时实际上已经利用了一个不均匀随机数生成器,但是当时对这个随机数生成器并没有仔细研究。正好现在是放假,于是我想花一点时间来仔细思考下这个问题,但是后来发现事情并不是这么简单。

因为 yummy 没有系统学习过概率论,也没系统学习过多项式,还没系统学习过线性代数,因此很多理解可能是很 trivial 的,一些记号也可能是错的。

本文将提供一种方法,O(n^3) 预处理,单次调用最坏 O(n) 次均匀随机数生成器,不解高次方程,产生服从 P(x)=k\prod\limits_{i=0}^{n-1} (x-x_i),x\in(l,r) 的随机数。

前置知识:概率密度函数。 建议大家看 这篇日报 的“概念 - 分布”这一小块。这篇日报介绍的分布都比较高级,但很多使用了逆分布法和拒绝采样法。

普通高次方程不可解,因此逆分布法失效了;同时拒绝采样法没有高超的技巧的话期望调用次数很高,最坏调用次数为正无穷。

所以我们要采用别的方式。本文分为下面几个部分:

  1. P(x)=kx+b
  2. P(x)=(k+1)x^k
  3. P(x)=aP_1(x)+(1-a)P_2(x),a\in (0,1)
  4. (大幅修改)P(x)=k\prod\limits_{i=0}^{n-1} (x-x_i)
  5. (新增) P(x)=k\prod\limits_{i=0}^{a-1}(x-x_i)\prod\limits_{i=0}^{b-1}(x-z_i)(x-\bar{z_i})
  6. 小练习答案

1. 线性分布

例 1:构造随机数生成器,使 P(x)=0.5+x,x\in (0,1)

线性分布中,如果作出 P(x)-x 图像,那么它会和 x=0 夹出一个直角梯形(可能退化成三角形)。

如果我们在这个直角梯形内均匀选取一个点(此处“均匀”的意思是,任取两块面积相同的区域,选中点在这两块区域内概率相等),那么感性理解一下,这个点的横坐标就是服从 P(x) 的一个随机数。

问题转化成怎么在一个直角梯形内选取一个点。

用小学知识我们知道,两个直角梯形可以拼成一个矩形。

以例 1 为例,用两个直角梯形我们拼出了 x\in (0,1),y\in (0,2) 这个矩形。使用 2 次均匀生成器获取 (x,y),然后:

可以证明,这种选取方式是均匀的,因此返回值服从 P(x)

显然地,这种方法可以推广到任意合法的概率分布 P(x)=kx+b

小练习 1:作出 P(x)=\dfrac{2}{\pi}\sqrt{1-x^2},x\in (-1,1) 的图像,并生成服从该分布的随机变量。

2. 幂函数

例 2: a,b,c 是三个 (0,1) 内均匀独立的随机变量,分别求它们最小值、最大值、中位数的概率密度函数。

我们可以求累积分布函数 F,然后求导得到概率密度函数 f

先看最大值。F(x)=P(\max(a,b,c)\le x)=P(a\le x,b\le x,c\le x)

然后对 $F$ 求导,得到 $f(x)=3x^2$。 其实我们也可以直接得到 $f$(只不过下面的记号不太专业)。 $f(x)=P(a=x,b\le x,c\le x)+P(a\le x,b=x,c\le x)+P(a\le x,b\le x,c=x)=3\cdot 1x^2$。 我们从“拒绝采样法”的角度来理解一下这个式子:$a$ 成为最大值,当且仅当 $b,c$ 都不超过 $a$;但是该方法和拒绝采样法的不同在于,$a$ 被拒绝后,我们仍然可以分配一个答案。 依样画葫芦,可得最小值为 $3(1-x)^2$,中位数为 $x(1-x)$。 显然这个方法很容易推广到 $f(x)=nx^{n-1}(x\in (0,1)$。只需要产生 $n$ 个均匀独立的 $(0,1)$ 内随机变量,并求其最大值即可。 ```cpp mt19937 rnd(time(0)); uniform_real_distribution<> Get(0,1); double genpwr(double l,double u,int x_l,int r_x) //生成服从 P(x)=k* (x-l)^x_l * (u-x)^r_x 的随机数 { vector<double> res; for(int i=0;i<x_l+r_x+1;i++) res.push_back(Get(rnd)); nth_element(res.begin(),res.begin()+x_l,res.end()); return res[x_l]*(u-l)+l; } ``` > 小练习 2:$a,b,c$ 是三个均匀独立的 $(0,1)$ 内随机变量,写出 $a+b+c$ 的概率分布。 ## 3. 概率密度函数的和 > 例 3:构造随机数生成器,使 $P(x)=x^2+\dfrac{x}{3}+\dfrac{1}{2},x\in (0,1)$。 我们已经掌握了 $P_1(x)=3x^2,P_2(x)=2x,P_3(x)=1$ 的产生方法,接下来要把它们混合在一起。 一般地,对于两个随机数生成器 $u,v$ 分别服从 $u(x),v(x)$,如果我们以 $a$ 的概率返回 $u$,以 $1-a$ 的概率返回 $v$,那么概率密度函数 $f(x)$ 为: $f(x)=P(\text{选}u)P(u=x)+P(\text{选}v)P(v=x)=au(x)+(1-a)v(x)$。 因此,对于例 3,设 $x,y,z$ 为三个 $(0,1)$ 内均匀独立生成的随机变量,我们以 $\frac{1}{3}$ 的概率返回 $\max(x,y,z)$,以 $\frac{1}{6}$ 的概率返回 $\max(x,y)$,以 $\frac{1}{2}$ 的概率返回 $x$。 > 例 4:构造随机数生成器,使 $P(x)=3(1-2x)^2,x\in (0,1)$。 前置知识:**抛物线都相似**。 如果直接展开得到 $P(x)=3(4x^2-4x+1)$,有负系数,不好做。换个方向,写成 $ax^2+bx(1-x)+c(1-x)^2$,发现 $a,b,c$ 仍有负数。 考虑我们如果已经有一个服从 $P(x)=3x^2,x\in (0,1)$ 的随机变量 $u$,怎么求 $v=0.5u$ 的概率分布。 $P(x)=\lim\limits_{d\to 0^+}\dfrac{P(x<v<x+d)}{d}=2\lim\limits_{d\to 0^+}\dfrac{P(2x<u<2x+2d)}{2d}=3\cdot (2x)^2$。 不难发现,当随机变量进行线性变换时,概率密度函数只会等比例放大缩小。学过导数的同学对此应该比较熟悉。 因此,先产生一个 $P(x)=3x^2$ 的 $u$,然后以 $0.5$ 的概率返回 $\dfrac{1+u}{2}$,以 $0.5$ 的概率返回 $\dfrac{1-u}{2}$,即可满足题意。 ```cpp struct Pos_polygenerator//系数全正,采用例 3 的做法 { double l,u; vector<double> Int; Pos_polygenerator(double ll,double uu,vector<double> a)//区间左右端点,和多项式所有系数 { l=ll;u=uu; Int.clear(); double curl=1,curu=1,curint=0; for(int i=0;i<a.size();i++) { curint+=a[i]*(curu-curl)/(i+1); Int.push_back(curint); curl*=l; curu*=u; } } double operator ()() { double val=Get(rnd)*Int.back(); int pwr=lower_bound(Int.begin(),Int.end(),val)-Int.begin(); return genpwr(l,u,pwr,0); } }; ``` ## 4. 没有定义域内重根和虚数根的多项式 > 例 5:构造随机数生成器,使 $P(x)=1.5(1-x^2),x\in(0,1)$。 让我们把上面三部分结合在一起,解决 $P(x)=k\prod\limits_{i=0}^{n-1} (x-x_i),x\in(l,r)$。 对于每个因式 $x-x_i$,分类讨论其零点的位置($f(x)$ 表示除去该因式的多项式)。 1. $x_i\le l$,将 $(x-x_i)f(x)$ 改成 $(x-l)f(x)+(l-x_i)f(x)$,分别计算两部分积分求出以多少概率变成常数,否则变成 $x-l$。 2. $l<x_i<r$,则该根必为重根,下一部分讨论这个问题。 3. $r\le x_i$。从常数里搞个 $-1$ 使得该项恒正,转化为 $(r-x)f(x)+(x_i-r)f(x)$,分别计算两部分积分求出以多少概率变成常数,否则变成 $r-x$。 经过以上处理后,你会剩下 $k(x-l)^a(r-x)^b$。 然后就简单了。直接搞 $a+b+1$ 个 $(l,r)$ 内均匀随机变量,找出其中第 $a+1$ 小即可。 对于例 5,先显然地因式分解一下,然后—— - 以 $0.25$ 的概率生成 $P(x)=kx(1-x)

5. 所有多项式

例 6:构造随机数生成器,使 P(x)=\dfrac{3}{19}(2x-1)^2(x^2+x+1)

接下来将解决重根和虚根的问题。

首先有一个经典结论:实系数多项式的虚根总是两两共轭。

不难发现,\bar{a}\bar{b}=\overline{ab}, \bar{a}+\bar{b}=\overline{a+b},因此若 z=a+bi 满足 f(z)=0,则必有 \bar z=\overline{f(z)}=0

换言之,一个多项式总能被分解成若干实系数一次式和若干无实根实系数二次式的积,只不过高次的咱不会。

回顾例 4,我们可以以对称轴为界,将 (l,r) 分成若干段,分别算出 x 落在每一段的概率并根据这个概率选择一段 (L,R)

在例 6 中,(0,1) 要被 (2x-1)^2 的对称轴分成两段 (0,0.5)(0.5,1)。然后分别计算前缀积分(虽然本例只有一个分界点):P(x<0.5)=\dfrac{23}{120},然后以 \dfrac{23}{120} 的概率选择 (0,0.5),否则选 (0.5,1)

选完落在哪一段后,对于 \Delta=0 的二次式 (x-p)^2,我们可以拆成两个在这一小段恒非负的一次式——具体地,若 (L,R) 落在 p 左边则拆成两个 p-x,否则拆成两个 x-p

对于 \Delta<0 的二次式 (x-p)^2+q^2,我们直接计算一下有多大概率这一维选 (x-p)^2,有多大概率这一维选 q^2 即可。类似地,设 f(x) 为去除该多项式的剩余部分,只需计算 q^2f(x)P(x)(L,R) 上积分。

例如例 6 中,x^2+x+1 可以写成 (x+0.5)^2+0.75,假如我们选中 (0,0.5),算出 (x+0.5)^2(x-0.5)^20.75(x-0.5)^2[L,R] 中的定积分 Qint=\dfrac{1}{6}Cint=\dfrac{1}{32},并以 \dfrac{Qint}{Qint+Cint} 的概率选中 (x+0.5)^2,否则选中常数。如果选中 (x+0.5)^2(0,0.5) 在对称轴右边,因此拆成两个 x+0.5 的积处理。

我大概写了一下,长这个样子:

#include<bits/stdc++.h>
using namespace std;
const double Eps=1e-6;
mt19937 rnd(time(0));
uniform_real_distribution<> Get(0,1);
double genpwr(double l,double u,int x_l,int r_x)
{
    vector<double> res;
    for(int i=0;i<x_l+r_x+1;i++)
        res.push_back(Get(rnd));
    nth_element(res.begin(),res.begin()+x_l,res.end());
    return res[x_l]*(u-l)+l;
}
bool cmp_real(complex<double> x,complex<double> y){return real(x)<real(y);}
bool cmp_sec(pair<double,double> x,pair<double,double> y){return x.second<y.second;}
double sqrabs(complex<double> x){return real(x)*real(x)+imag(x)*imag(x);}
vector<double> polyint(vector<double> a)
{
    vector<double> res={0};
    for(int i=0;i<a.size();i++)
        res.emplace_back(a[i]/(i+1));
    return res;
}
double polyval(vector<double> &a,double x)//有条件的可以用多项式多点求值 
{
    double tot=0,curx=1;
    for(int i=0;i<a.size();i++)
    {
        tot+=a[i]*curx;
        curx*=x;
    }
    return tot;
}
vector<double> polydivlinear(vector<double> a,double b)// makes sense when (b+x) | a
{
    if(-Eps<b and b<Eps)
    {
        assert(-Eps<a[0] and a[0]<Eps);
        a.erase(a.begin());
        return a;
    }
    vector<double> res;
    for(int i=0;i<a.size()-1;i++)
    {
        double topush=a[i]/b;
        a[i+1]-=topush;
        res.emplace_back(topush);
    }
    return res;
}
vector<double> polydivquad(vector<double> a,double b,double k)// makes sense when (b+kx+x^2) | a
{
    if(-Eps<b and b<Eps)
    {
        assert(-Eps<a[0] and a[0]<Eps);
        a.erase(a.begin());
        return a;
    }
    vector<double> res;
    for(int i=0;i<a.size()-2;i++)
    {
        double topush=a[i]/b;
        a[i+1]-=topush*k;
        a[i+2]-=topush;
        res.emplace_back(topush);
    }
    return res;
}
vector<double> polymult(vector<double> a,vector<double> b)
//这点规模懒得搬我 Karatsuba 板子 or FFT 板子了 
{
    vector<double> res(a.size()+b.size()-1,0);
    for(int i=0;i<a.size();i++)
        for(int j=0;j<b.size();j++)
            res[i+j]+=a[i]*b[j];
    return res;
}
struct polygenerator
{
    double ov_l,ov_u,Int;
    vector<double> re;
    vector<complex<double> > im; 
    vector<pair<double,double> > x_F;
    vector<vector<double> > rints;
    vector<vector<pair<double,double> > > iints;//前原积分,后乘上 (x-L) 积分 
    polygenerator(double ll,double uu,vector<double> re_rts,vector<complex<double> > im_pos_rts)
    //  [l,u]、定义域外实根、共轭根 
    {
        ov_l=ll;ov_u=uu;Int=0;
        re=re_rts;
        im=im_pos_rts;
        vector<double> f={1},F;
        for(double i:re)f=polymult(f,{-i,1});
        for(auto i:im)f=polymult(f,{sqrabs(i),-2*real(i),1});
        F=polyint(f);
        if(polyval(F,ov_u)<0)
            for(int i=0;i<F.size();i++)
                F[i]=-F[i];
        F[0]-=polyval(F,ov_l);
        x_F.emplace_back(ov_l,0); 
        for(int i=0;i<im.size();i++)//对于共轭根将对称轴拿出来 
        {
            double x=im[i].real();
            if(ov_l<=x and x<ov_u)
                x_F.emplace_back(x,polyval(F,x));
        }
        sort(x_F.begin(),x_F.end());
        x_F.emplace_back(ov_u,polyval(F,ov_u));
        Int=x_F.back().second;
        //接下来每个函数每个段求一个定积分
        rints.resize(x_F.size()-1);
        iints.resize(x_F.size()-1);
        for(int i=0;i<re.size();i++)
        {
            auto tmp=polyint(polydivlinear(f,-re[i]));
            for(int j=0;j+1<x_F.size();j++)
                rints[j].emplace_back(abs(polyval(tmp,x_F[j+1].first)-polyval(tmp,x_F[j].first)));
        }
        for(int i=0;i<im.size();i++)
        {
            double mod=sqrabs(im[i]),P=real(im[i]);
            auto fdv=polydivquad(f,mod,-2*real(im[i])),tmp=polyint(fdv);
            for(int j=0;j+1<x_F.size();j++)
            {
                double Lrg=x_F[j].first,Rrg=x_F[j+1].first;
                double Cint=abs(polyval(tmp,Rrg)-polyval(tmp,Lrg));
                vector<double> tp1;
                if(P<Lrg+Eps)
                {
                    tp1=polyint(polymult(fdv,{P*P,-2*P,1}));
                    iints[j].emplace_back(Cint,2*(Lrg-P)*abs(polyval(tp1,Rrg)-polyval(tp1,Lrg)));
                }
                else
                {
                    tp1=polyint(polymult(fdv,{P*P,-2*P,1}));
                    iints[j].emplace_back(Cint,2*(P-Rrg)*abs(polyval(tp1,Rrg)-polyval(tp1,Lrg)));
                }
            }
        }
    }
    double operator () ()
    {
        double val=Get(rnd)*Int;
        int rg=upper_bound(x_F.begin(),x_F.end(),make_pair(0,val),cmp_sec)-x_F.begin()-1;
        double L=x_F[rg].first,R=x_F[rg+1].first,Qint=x_F[rg+1].second-x_F[rg].second;
        int x_l=0,r_x=0;
        for(int i=0;i<re.size();i++)
        {
            if(re[i]<=L)
            {
                double Cint=rints[rg][i]*(L-re[i]);
                if(Get(rnd)*Qint>=Cint)x_l++;
            }
            else
            {
                double Cint=rints[rg][i]*(re[i]-R);
                if(Get(rnd)*Qint>=Cint)r_x++;
            }
        }
        for(int i=0;i<im.size();i++)
        {
            double P=real(im[i]),Q2=imag(im[i])*imag(im[i]);
            if(Get(rnd)*Qint<iints[rg][i].first*Q2)continue;
            double Tint=Qint-iints[rg][i].first*Q2;
            if(P<=L)
            {
                if(Get(rnd)*Tint>=(L-P)*iints[rg][i].second)
                    x_l++;
                if(Get(rnd)*Tint>=(L-P)*iints[rg][i].second)
                    x_l++;
            }
            else
            {
                if(Get(rnd)*Tint>=(P-R)*iints[rg][i].second)
                    r_x++;
                if(Get(rnd)*Tint>=(P-R)*iints[rg][i].second)
                    r_x++;
            }
        }
        return genpwr(L,R,x_l,r_x);
    }
};
//polygenerator A(0,1,{1},{0.5+0.86602540378443864676372317075294i});
//polygenerator A(0,1,{},{0.5});
//polygenerator A(0,1,{-1,1},{});
//polygenerator A(0,1,{},{0.5,-0.5+0.86602540378443864676372317075294i});
void Plot(int Tms,double D,int L,int R,int perstar)
{
    int res[1005]={0};
    for(int i=1;i<=Tms;i++)
    {
        double tmp=A();
        assert(L<=tmp and tmp<R);
        res[int((tmp-L)*D)]++;
    }
    for(int i=0;i<=D;i++)
    {
        printf("%.3f: %6d",i/D+L,res[i]);
        for(int j=0;j<res[i]/perstar;j++)
            printf("*");
        puts("");
    }
}
int main()
{
    //Plot(10,10,0,1,20);
    Plot(1000000,100,0,1,400);
}

小练习答案

小练习 1 答案:

如果忽略 \frac{2}{\pi},那么作出的图是一个半圆,原题相当于我们要在半圆内均匀随机生成一个点。

不难发现,半圆弧长度 \pi r 和半径 r 成正比,也就是随机变量 r 的概率密度函数 P(x)=2x

在此基础上只要随一个极角即可。

综上,在 (0,1) 内随机生成三个实数 \theta,r_1,r_2,返回 \max(r_1,r_2)\cos \theta\pi 即可。

小练习 2 答案:

接下来就是经典中学分类讨论了: > 在 平面直角坐标系 $aOb$ 中,固定正方形 $ABCD:\{(a,b)|0<a,b<1\}$ 在两条动态平行直线 $a+b=x-1$ 和 $a+b=x$ 之间的面积是多少? - 当 $0\le x< 1$ 时,$P(x)=0.5x^2$。 - 当 $1\le x< 2$ 时,$P(x)=1-0.5(x-1)^2-0.5(2-x)^2$。 - 当 $2\le x< 3$ 时,$P(x)=0.5(3-x)^2$。 这题已经做完了,但是作出[上述式子的图象](https://www.desmos.com/calculator/xupichipsk?lang=zh-CN),我们发现它很接近正态分布。 废话,中心极限定理好像就是这么定义的。 ## 后记 实际上这玩意没有太大的用处,首先需要高次普通多项式分布的情形并不多(对我个人而言二次已经是极限),然后因为需要提前把根解出来,适用范围就更窄了。 不知道数学里有没有更多应用。话说回来,一个发明刚出来的时候,似乎都是没几个人能体会到它的用处的。