浅谈概率 DP 在生物分离定律题中的运用

· · 学习·文化课

前言

最近刚学习了概率 \texttt{\textup{\textmd DP}} 和分离定律,发现可以用前者来完成或理解后者,于是就写了这篇文章来记录一下。

正文

先来看一道题:

上图为某单基因遗传病的遗传系谱图(方块为男性,圆圈为女性,黑色为患者,白色为正常人)。问:若 \textrm{\textup{\textmd III}}_1\textrm{\textup{\textmd III}}_2 婚配后生下一个正常女孩,则该女孩携带致病遗传因子的概率为多少?(易得:正常对患病为显性)

现在我们用概率 \texttt{\textup{\textmd DP}} 来做这道题。

先对图中的点重新标号,并加上最后那个女孩:

dp_{u,XX},表示在 u 节点所表示的人的基因型为 XX 的概率为多少。并用 A/a 表示该基因。则最终答案为 dp_{u,Aa}

开始分类讨论:(以下用 u 表示当前节点,p1,p2 表示父本和母本)

对于没有给出父母的人,有如下几种情况:

  1. u 节点所表示的人患病,则 Ta 的基因型一定为 aa
  2. u 节点所表示的人正常,且 Ta 的子女中有患病的,则 Ta 的基因型一定为 Aa
  3. u 节点所表示的人正常,且 Ta 的子女中没有患病的,则 Ta 的基因型有 {\large \frac{1}{2}} 的概率为 AA,有 {\large \frac{1}{2}} 的概率为 Aa
  4. 若不知道 u 节点所表示的人的表现型,则 Ta 的基因型为 AA,Aa,aa 的概率都为 {\large \frac{1}{3}}

对于给出了父母的人就需要状态转移方程了。根据生物学知识可得:(以下 \times 表杂交)

于是可推出状态转移方程为:

dp_{u,AA} = dp_{p1,AA}\times dp_{p2,AA} + \frac{1}{2} dp_{p1,AA}\times dp_{p2,Aa} + \frac{1}{2} dp_{p1,Aa}\times dp_{p2,AA} + \frac{1}{4} dp_{p1,Aa}\times dp_{p2,Aa} dp_{u,aa} = dp_{p1,aa}\times dp_{p2,aa} + \frac{1}{2} dp_{p1,aa}\times dp_{p2,Aa} + \frac{1}{2} dp_{p1,Aa}\times dp_{p2,aa} + \frac{1}{4} dp_{p1,Aa}\times dp_{p2,Aa} dp_{u,Aa} = dp_{p1,AA}\times dp_{p2,aa} + dp_{p1,aa}\times dp_{p2,AA} + \frac{1}{2} dp_{p1,AA}\times dp_{p2,Aa} + \frac{1}{2} dp_{p1,Aa}\times dp_{p2,AA} + \frac{1}{2} dp_{p1,aa}\times dp_{p2,Aa} + \frac{1}{2} dp_{p1,Aa}\times dp_{p2,aa} + \frac{1}{2} dp_{p1,Aa}\times dp_{p2,Aa}

另外,若知道这个人的表型,则需要进行如下操作:

  1. u 节点所表示的人患病,则将 dp_{u,AA}dp_{u,Aa} 置为 0
  2. u 节点所表示的人正常,且 Ta 的子女中有患病的,则将 dp_{u,AA}dp_{u,aa} 置为 0
  3. u 节点所表示的人正常,且 Ta 的子女中没有患病的,则将 dp_{u,aa} 置为 0

处理完上面三条后还需进行“归 1 化”:将没有置 0 的值在保持原比例的情况下使他们的和变为 1

然后,我们发现系谱图可以看作一个 DAG,所以用拓扑排序来 \texttt{\textup{\textmd DP}} 就行了。

最后,通过手动模拟, 把代码打出来,跑一遍,就能得到此题答案为 {\large \frac{13}{27}}

::::success[屎山代码]

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5;
int gcd(int a, int b){
    a=abs(a),b=abs(b);
    return b?gcd(b,a%b):a;
}
struct fs{
    int a, b;
    fs(int a_=0,int b_=1):a(a_),b(b_){
        if(b<0)a=-a,b=-b;
        int g=gcd(a,b);
        if(g!=0)a/=g,b/=g;
        if(a==0)b=1;
    }
    friend fs operator+(const fs&x,const fs&y){return fs(x.a*y.b+y.a*x.b,x.b*y.b);}
    friend fs operator*(const fs&x,const fs&y){return fs(x.a*y.a,x.b*y.b);}
    friend fs operator/(const fs&x,const fs&y){
        if(y.a==0)cerr<<"Error: division by zero\n",exit(1);
        return fs(x.a*y.b,x.b*y.a);
    }
    friend ostream&operator<<(ostream&os,const fs&o){
        os<<o.a<<"/"<<o.b;return os;
    }
}dp[N][3];//0:AA 1:Aa 2:aa
int n,m,a[N],P1[N],P2[N],hp[N],bj[N],deg[N];
vector<int>G[N];
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    cin>>m;
    for(int i=1,u;i<=m;i++){
        cin>>u,cin>>P1[u]>>P2[u],deg[u]=2,hp[u]=1;
        G[P1[u]].push_back(u),G[P2[u]].push_back(u);
        if(a[u]==1)bj[P1[u]]=bj[P2[u]]=1;
    }
    queue<int>q;
    for(int i=1;i<=n;i++)if(!deg[i])q.push(i);
    while(!q.empty()){
        int u=q.front();q.pop();
        for(int v:G[u])if(!--deg[v])q.push(v);
        if(a[u]==1)dp[u][2]=fs(1),dp[u][0]=dp[u][1]=fs(0);
        else if(bj[u]==1)dp[u][1]=fs(1),dp[u][0]=dp[u][2]=fs(0);
        else if(!hp[u]){
            if(a[u]==0)dp[u][0]=dp[u][1]=fs(1,2),dp[u][2]=fs(0);
            if(a[u]==2)dp[u][0]=dp[u][1]=dp[u][2]=fs(1,3);
        }else{
            fs *dp1=dp[P1[u]],*dp2=dp[P2[u]];
            dp[u][0]=(dp1[0]*dp2[0])+(fs(1,2)*dp1[0]*dp2[1])+(fs(1,2)*dp1[1]*dp2[0])+(fs(1,4)*dp1[1]*dp2[1]);
            dp[u][2]=(dp1[2]*dp2[2])+(fs(1,2)*dp1[2]*dp2[1])+(fs(1,2)*dp1[1]*dp2[2])+(fs(1,4)*dp1[1]*dp2[1]);
            dp[u][1]=(dp1[0]*dp2[2])+(dp1[2]*dp2[0])+(fs(1,2)*dp1[1]*dp2[1])+
            (fs(1,2)*dp1[0]*dp2[1])+(fs(1,2)*dp1[1]*dp2[0])+(fs(1,2)*dp1[2]*dp2[1])+(fs(1,2)*dp1[1]*dp2[2]);
            if(!a[u]){
                dp[u][2]=fs(0);
                if(dp[u][0].a==0)dp[u][1]=fs(1);
                else if(dp[u][1].a==0)dp[u][0]=fs(1);
                else{
                    fs t=dp[u][0]+dp[u][1];
                    dp[u][0]=dp[u][0]/t,dp[u][1]=dp[u][1]/t;
                }
//              cout<<u<<" "<<dp[u][0]<<" "<<dp[u][1]<<"\n";
            }
        }
    }
    cout<<dp[n][1];
    return 0;
}
/*
输入格式:
第一行一个整数n,表示节点数
第二行n个整数(0,1,2),第i个表示编号为i的人为正常(0),患病(1),未知(2)
第一行一个整数m,表示家庭关系数
接下来m行,每行三个整数u,p1,p2,表示p1,p2分别为u的爸和妈
*/
/*
input:
16
0 0 1 0 0 0 1 0 0 0 0 0 0 0 1 0
9
7 1 2
8 1 2
9 1 2
10 3 4
13 9 10
12 5 6
14 11 12
15 11 12
16 13 14
output:
13/27
*/

::::

总结

实际上,用概率 \texttt{\textup{\textmd DP}} 来做这种题,和生物老师们教的方法在本质上并没有太大的区别,只不过是换了种更适合 OIer 的角度来看待罢了。但也希望这篇博客能为你带来帮助,感谢你的浏览!