【JZOJ 3466】 选课 select

37 篇文章 0 订阅
17 篇文章 0 订阅

Description

你真的认为选课是那么容易的事吗?HYSBZ的ZY同志告诉你,原来选课也会让人产生一种想要回到火星的感觉。假设你的一周有n天,那么ZY编写的选课系统就会给你n堂课。但是该系统不允许在星期i和星期i+1的时候选第i堂课,也不允许你在星期n和星期一的时候选第n堂课。然后连你自己也搞不清哪种选课方案合法,哪种选课不合法了。你只想知道,你到底有多少种合法的选课方案。
对于第i组数据,n<=10*i, 1<=i<=3
对于100%的数据:1<=n<=100000

Analysis

本来还想暴力然后高斯消元找递推式的但是没时间打了。
看了题解才知道递推式鬼一样难找。
但是,本题还有另一种不需要递推的方法,即组合数方法。

转化

首先,正难则反,考虑恰好有 k 门课选错的方案数。如果我们能解决这个问题,就可以直接上容斥原理。
这个问题的解决方法极为巧妙,以我的智商是很难想到的。
首先,把1 i 的不合法的位置写出来,分别是(1,2)(2,3).....(n,1)
拆括号,变成一个序列, 1,2,2,3,3,4,.....n,1
首先,同一天不能选两门课,所以第 2i1,2i 个不能同时选到。
又由于第 2i,2i+1 个相同,同一门课不能被同时两天选,所以也不能选到。
所以恰好选错 k 门课的方案数就是在这个序列里选出k个元素,且两两不能相邻。
而且这个序列实质上是一个圆环。所以,问题变成了求圆排列上选 k 个互不相邻的元素的方案数。
这个问题比较难,我们从简单想起。
如果不是圆环而就是一个序列,怎么算?

方法一


如图,黑色球为选的,白色为不选的。
我们让黑色球吃掉它右边的第一个球,最后一个黑色球不吃,序列就变成了这样。

这里,元素共有nk+1个,要选出 k 个,方案数就是Cknk+1
为什么呢?反过来,所有的长度为 nk+1 的序列,选出 k 个黑色球,都可以让那k个黑球吐一个球在它右边,除了最后一个,使得每个球都不相邻。
这两个是一一对应的。所以,其方案数也是相等的。

方法二

还有一种来自Alan_cty的方法,也能推出来。
把黑球拿出来排成一排,每个黑球的前面后面都恰好有一个盒子,显然总共有 k+1 个盒子。除了最左和最右的盒子,中间的那些盒子都至少放一个球来使这些黑球不相邻,所以这是限制放置数的组合数,可以参考一下【SDOI2013方程】这道题。

方法三

正难则反!(这是一个非常好的思想)
我们不妨把选出一些球看成插入一些球,这样就能用组合数了嘛。
问题也就转化为,原本有 nk 个球,有 nk+1 个空,现在要插入 k 个球,而且两个球不能同时插入一个空。
这样,方案数显而易见地就是Cknk+1
这个方法是最容易理解,最简单的方法。

推广

在圆环上,可以把它剪开,破坏成链。显然,满足链上的不相邻方案的且首尾不能同时选的方案数就是环上的方案数。
首尾若都选,则第二位和倒数第二位都必不选,那么就是中间的元素不相邻的方案数,即 n4 个元素选出 k2 个元素。
那么方案数就是 Cknk+1Ck2(n4)(k2)+1 ,化简,得 nnkCknk
当然,确定了这 k 门课,其他nk门课可以乱选,所以再乘上一个 (nk)!
于是就可以容斥了。 O(nT) , T <script type="math/tex" id="MathJax-Element-1019">T</script>为数据组数。
实现要注意优化常数。

Code

#include<cstdio>
#include<algorithm>
#define fo(i,a,b) for(int i=a;i<=b;i++)
using namespace std;
typedef long long ll;
const int N=200010,mo=int(1e9)+7;
ll fac[N],ny[N],_ny[N];
ll qmi(ll x,int n)
{
    ll t=1;
    for(;n;n>>=1)
    {
        if(n&1) t=t*x%mo;
        x=x*x%mo;
    }
    return t;
}
ll C(int m,int n)
{
    return fac[m]*ny[n]%mo*ny[m-n]%mo;
}
int main()
{
    int n;
    fac[0]=ny[0]=1;
    fo(i,1,N-10) fac[i]=fac[i-1]*i%mo,ny[i]=qmi(fac[i],mo-2),_ny[i]=qmi(i,mo-2);
    while(scanf("%d",&n)!=EOF)
    {
        if(n==1)
        {
            printf("0\n");
            continue;
        }
        ll t,ans=0;
        fo(k,0,n)
        {
            t=fac[n-k]*2*n%mo*_ny[2*n-k]%mo*C(2*n-k,k)%mo;
            if(k&1) ans=(ans-t+mo)%mo;
            else ans=(ans+t)%mo;
        }
        printf("%lld\n",ans);
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值