欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Nowcoder 5477E. 弦(卡特兰数、组合数学)

程序员文章站 2022-04-14 20:38:53
...

题目描述:

给定一个圆,圆上有2N个互不重叠的点。每次操作随机选择两个先前未选择过的点连一条弦,共连成N条弦,求所有弦不交的概率。

输入描述:
一行,只有一个整数N(1≤N≤10^7)。

输出描述:
一行,即答案。答案应以模10 ^ 9+7的形式输出。正式的说,令M=10 ^ 9+7,答案可以表示为最简分数p/q的形式,其中p和q为整数且q与M互质。输出整数x满足 0≤x<M且 x⋅q ≡ p(mod M)。

示例1
输入

2

输出

666666672

算法标签:卡特兰数、组合数学

首先确定思考方向,要用合法方案/总方案来计算出概率,那么关键在于如何计算合法的方案数。

不妨先将题目中的圆和2N个点,转换为一个正2N边形,这样就得到了一个比较经典的卡特兰数模型,即用不相交的直线连接多边形的顶点,问一共多少种划分方式。

以N=3构成的正六边形为例,可以得到下图的五种合法方案。
Nowcoder 5477E. 弦(卡特兰数、组合数学)

怎样来计算合法的方案数呢(令f(N)为2N个结点的合法方案数)。可以以左下角红点为基点,分类讨论与红点相连的结点是哪个。
Nowcoder 5477E. 弦(卡特兰数、组合数学)
很明显,合法的连接方法只有三条绿边,因为如果和蓝点相连,正六边形被分割成的两个区域内结点个数都是奇数,一定会发生边相交的情况。

于是依次考虑三条绿边左右两边的连接方案,由于左右区域不可能互相连接(否则与绿边相交)于是可以分别视作两个独立的区域,将各自的方案数相乘,并相加后得到f(3)=f(0)*f(2)+f(1)*f(1)+f(2)*f(0)=1*2+1*1+2*1=5。

将其推广到n的情况,得到这样一个公式:
f(n)=i=0n1f(i)f(ni1) f(n)=\sum_{i=0}^{n-1}f(i)*f(n-i-1)

这种形式的递推数列即称之为卡特兰数。其相对应的应用还有很多,这里可以展开讨论一下,并尝试找到其本质含义。

比如在一个堆栈中,元素入栈的顺序依次为1、2、3、…、n,问出栈方法总共有多少种?

求解思路依然是一个分类讨论思想。假设最后一个出栈的数为k,那么,将整个过程可以分为四个部分

  1. 1、2、3、…、k-1 进栈并按某种顺序出栈
  2. k进栈
  3. k+1、k+2、…、n进栈,并按某种顺序出栈
  4. k出栈

不难发现第一步的方案数为f(k-1),第三步的方案数为f(n-k),其乘积f(k-1)*f(n-k)即为“最后一个出栈的数为k”的方案数,k从1取至n,则有

f(n)=i=1nf(i1)f(ni)=i=0n1f(i)f(ni1) f(n)=\sum_{i=1}^{n}f(i-1)*f(n-i)=\sum_{i=0}^{n-1}f(i)*f(n-i-1)

同样符合卡特兰数的情形。

那么进一步思考一下,为什么两个看似无关的问题,最终的解是一样的呢?为了揭示其本质意义,我们换一种角度来看待以上两个问题。

考虑堆栈入栈和出栈方案时,实际上可以认为是n次入栈与n次出栈所组成的组合。那么,令入栈为“1”,出栈为“0”,任何一种方案,都可以等价刻画为一串长度为2n的“01”串,如下图,出栈顺序为1、3、4、2,则对应的“01”串为10110100。
Nowcoder 5477E. 弦(卡特兰数、组合数学)
从图中可以看到,01串中的每一位对应了每个时刻,堆栈内发生的操作(入或出)。该串具有以下两个性质:

  1. 由n个“1”和n个“0”组成,长度为2n。
  2. 取出串的任意前k位,“1”的个数>="0"的个数(否则意味着某时刻对空栈做了pop操作,无实际意义)

01串表现的是对堆栈的操作方法,而所有满足以上两个条件的01串,可以证明与出栈的方案是一一对应的。(任何一种出栈方案都可以找到一种操作方法,且不同的操作方法所产生的出栈顺序总是不同的)因此,“01”串的方案数就是堆栈问题方案数的等价刻画。

同样,我们找一种方法来刻画正多边形连边方案数的问题。选择一个点作为起始点,从起始点出发,逆时针绕多边形一周,对于某一个结点,如果之前访问过与它相连的另一个结点,则记为“0”,如果是第一次访问这条边,则记为“1”。
Nowcoder 5477E. 弦(卡特兰数、组合数学)
如图中这种连法对应的“01”串就是“10110010”,该串同样也具有以上两个性质,并且这种刻画方法与原问题的方案也一一对应。

于是总结一下,卡特兰数f(n)的一个本质含义,就是满足

1. 由n个“1”和n个“0”组成,长度为2n。

2. 取出串的任意前k位,“1”的个数>="0"的个数

这两个条件的01串的个数。

为啥要绕这么一大圈呢,一方面是加深一下对卡特兰数的理解,另一方面是因为,单纯从递推的角度,计算f(n)的时间复杂度是O(n^2),把问题转换成这样,才便于我们找到卡特兰数通项公式。

f(n)=i=0n1f(i)f(ni1)=C2nnC2nn+1 f(n)=\sum_{i=0}^{n-1}f(i)*f(n-i-1)=C_{2n}^n-C_{2n}^{n+1}

下面来看一下这个公式的神仙推导方法
Nowcoder 5477E. 弦(卡特兰数、组合数学)
我们再给这样的“01”串赋予一个比较熟悉的实际意义,即从(0,0)沿着网格线,每一步向上或向右走到(n,n)的一种方案。我们令“1”是向右走一步,“0”是向上走一步,那么由性质可知,所有合法的路线都不能穿过红色对角线。只有红线以下(如绿色的线)才是合法的

这里的计算方法是先计算出总共的方案数C2nnC_{2n}^n,再减去不合法(即越过红线的)方案数。

问题转换后,就有了下图这样一个构造方法:
Nowcoder 5477E. 弦(卡特兰数、组合数学)

观察上图不合法的紫线,找到它第一处越过红线的位置,将从这一点起到(5,5)的路径沿着y=x+1这条粉线对称翻转一下,得到了黄线。由对称翻转可知,终点一定被翻转到了(n-1,n+1)也就是(4,6)这一点上。
Nowcoder 5477E. 弦(卡特兰数、组合数学)
再把黄线和下半段紫线拼起来,发现这就是一条从(0,0)到(4,6)的路线(即图中绿线)。

由此我们再反过来想,对于每一条从(0,0)到(4,6)的路线,必然会在某一点越过红线,将这一点到(4,6)的部分对称翻转并和前半段拼接,不就得到了一种从(0,0)到(5,5),且越过红线的方案了嘛。(此处还是有一个经过对称翻转后,两种路线一一对应的关系存在,避免太啰嗦就不展开说了,应该挺好理解的)

总之,通过这样一种构造,可以得到非法方案就是C2nn+1C_{2n}^{n+1}C2nn+1C_{2n}^{n+1}=C2nn1C_{2n}^{n-1})种。用总方案减去非法的,就是合法方案的个数了,如果把组合数再展开一下,就能得到最后的阶乘公式了。
f(n)=i=0n1f(i)f(ni1)=C2nnC2nn+1=(2n)!n!(n+1)! f(n)=\sum_{i=0}^{n-1}f(i)*f(n-i-1)=C_{2n}^n-C_{2n}^{n+1}=\frac{(2n)!}{n!(n+1)!}
(题外话:卡特兰数前几项是:1、2、5、42、132、429、1430、4862…,所以有时手推前几项,可以反过来发现是用卡特兰数来做的)

把卡特兰数讲清楚之后这道题就很显然了。合法方案数就是f(n),直接代入公式计算。

总方案数是从2n个结点中两两选一组连边,再除以n!消除先后顺序
C2n2+C2n22+...+C22n!=(2n)!2nn! \frac{C_{2n}^2+C_{2n-2}^2+...+C_2^2}{n!}=\frac{(2n)!}{2^n·n!}

合法方案/总方案后得到答案,O (n)计算即可。
ans=2n(n+1)! ans=\frac{2^n}{(n+1)!}

#include<bits/stdc++.h>
using namespace std;
const int MOD=1e9+7;
typedef long long ll;
ll qpower(int x,int p)
{
    ll base=x,ret=1;
    while(p){
        if (p&1==1) ret=(ret*base)%MOD;
        base=(base*base)%MOD;
        p=p>>1;
    }
    return ret;
}
int main(){
    int n;
    ll ans=1,div=1;
    cin>>n;
    for (int i=1;i<=n;i++) ans=(ans*2)%MOD;
    for (int i=1;i<=n+1;i++) div=(div*i)%MOD;
    ans=(ans*qpower(div,MOD-2))%MOD;
    printf("%lld",ans);
    return 0;
}

相关标签: 算法 概率论