洛谷博客题解

SP19148【INS14G - Kill them All】

题目大意:

 有n只怪兽,每一次可让Digo杀或Sharry杀。求在每杀掉一只怪物后,Digo的击杀数都比Sharry的击杀数多的方案书。

输入:
第一行输入n,为输入的数据数
接下来n行输入每个数据的怪兽数量

输出:
分行输出每个数据的合法方案数,答案值取mod 1e9+7后的值。

前置知识:组合数 乘法逆元

感觉其他博客讲的不是很清楚,也没有说组合数公式是怎么来的,我这样数论极菜的萌新看了好久才想明白qwq。。

还是先给出本题组合数公式C(n-1,n/2)

分析一

转化成坐标系上的移动方案问题

把第一个人杀的怪物看成横坐标,第二个人的看成纵坐标,怪物由第一个人杀向右走,反之向上走。

可知第一步一定向右,枚举终点的纵坐标m(即向上的步数),题目就变成从点(1,0)开始走到(n-m,m)不越过y=x的方案数。枚举向上步数m,总方案数为C(n-1,m)(n-1步中选m步),现在我们要减去非法方案数。

从(1,0)走到(n-m,m)的不合法方案按y=x翻转后与从(0,1)走到(n-m,m)的方案相对应

注意这里的翻转,终点是不翻转的,只翻转起点。

不合法方案为(n-1步中向上走了m-1步),由此可知答案为

公式(用i枚举终点纵坐标,0步没有不合法方案特判,0≤m(第二个人杀的次数)≤n/2):

(公式来自@Alpha)

得组合数公式C(n-1,n/2)

(感谢@Alpha提供的图例与分析!)

分析二

建议可以先看看关于卡特兰数的博客中对卡特兰数的应用,和这题很像,便于理解

把题目放到笛卡尔坐标系(平面直角坐标系)中考虑(如图1),x轴表示每个怪兽,从原点出发向右上方走表示由D杀了,向右下方走表示由S杀了。相当于我从原点出发,我的轨迹不能在过程中碰到x轴。

图1

可知第一步一定向上,所以原题转化为从原点出发,轨迹在过程中不能跑到x轴下面。

枚举向上步数m,总方案数为C(n-1,m),现在我们要减去非法方案数。对于非法的轨迹,有两种翻转方式:

  1. 把它第一次碰到y=-1前的轨迹按y=-1翻转(见P1641 [SCOI2010]生成字符串),也就是从(0,-2)到达原先的终点的方案与不合法方案一一对应,会发现向上走的步数比翻转前多1,即得C(n,m+1)

  2. 把它第一次碰到y=-1后的轨迹按y=-1翻转(如图2),在n,m确定的情况下,终点是确定的,翻转后的轨迹和原轨迹的方案一一对应。那么终点在哪里呢? 设p为翻转后终点纵坐标,m为翻转前的向上步数,则有m - (n - m) + p = -2 (n-m为向下走的步数,翻转前终点纵坐标+1=翻转后纵坐标的相反数-1)-> p = -2 + n - 2m ,设当向上走x步时可以到达p,则 x - (n - x) = -2 + n - 2m -> x = n - m - 1,由于翻转后的轨迹和原轨迹的方案一一对应,所以到达p的方案数就是不合法的方案数

所以非法方案数为C(n,n-m-1)=C(n,m+1),(见组合数中的互补性质,即从m个不同元素中取出n个元素的组合数=从m个不同元素中取出(m-n)个元素的组合数)

枚举向上步数为m,总方案数为C(n,m)-C(n,m+1);

公式(n已减1,n-1步没有不合法方案特判,n/2≤m(第一个人杀的次数)≤n-1):

(公式来自@Alpha)

答案为C(n-1,n/2)

图2

(以上思路由这个博客改进而来)

代码1

fac[i]存1~i的阶乘

inv[i]存1~i逆元的乘积

#include<cstdio>
using namespace std;
#define p 1000000007
long long fac[1000005],inv[1000005];
inline long long C(int n,int m) {
    if (n==m || m==0) return 1;
    return fac[n]*inv[m]%p*inv[n-m]%p;
}
int main() {
    int T,n;
    scanf("%d",&T),fac[0]=inv[0]=inv[1]=1;
    for (int i=1; i<1000005; i++) fac[i]=fac[i-1]*i%p;
    for (int i=2; i<1000005; i++) inv[i]=p-(p/i)*inv[p%i]%p;
    for (int i=2; i<1000005; i++) inv[i]=inv[i-1]*inv[i]%p;
    while(T--)
        scanf("%d",&n),printf("%lld\n",C(n-1,n/2));
}

代码2

#include<cstdio>
using namespace std;
#define p 1000000007
#define int long long
int fac[1000005];
inline int pow(int x) {
    int ans=1;
    x%=p;
    for (int i=p-2; i; i>>=1,x=x*x%p)
        if (i&1) ans=ans*x%p;
    return ans;
}//快速幂求逆元(x^(p-2))

signed main() {
    int T,n;
    scanf("%lld",&T),fac[0]=1;
    for (int i=1; i<=1000005; i++) fac[i]=fac[i-1]*i%p;
    while(T--)
        scanf("%lld",&n),printf("%lld\n", fac[n-1]*pow(fac[n/2]*fac[n-1-n/2]%p)%p);
}

通过这题的确对组合数的应用加深了不少,如果分析有误还望指出qwq!

参考文章:

最后再次感谢@Alpha的耐心讲解与图例公式^_^