(组合数的计算)牛客周赛 Round 35 小红的子序列权值和 (easy / hard)

文章介绍了如何通过分析题目的结构,利用数学方法快速计算给定序列的权值,重点关注了避免遍历全序列的方法,以及在面对不同难度时如何处理组合数和取模操作。
摘要由CSDN通过智能技术生成

难度解读

首先这个题分为了两种难度,而差别就只有n的不同,很多人只去看了难的那一个,虽然如果难的那一个会做了,简答题也就不攻自破了,但是既然这样设计了,那就代表着一些细节的处理上,简单的那个更好想而且更好做,所以本篇将针对难度来讲解两个做法

正片开始

分析一下题意,我们需要计算出这 2 n − 1 2^n - 1 2n1的序列的权值和,先且不说计算出一个序列内所有数字乘积的权值需要多少时间,就这个全部序列遍历一遍就已经非常夸张了,所以我们一定有什么办法直接算出来

我们先来解决一个序列中权值的问题,由题意可得,序列中只会出现三个数字,分别是1、2、3,其实只要写几个数字看看就会发现,1对最后的权值没有贡献,也就是说1的数量在解决这个问题时不重要
2和3呢
举个例子就明白了
6的权值是4,也就是一个2和一个3相乘
那么3的能分解为 1 和 3
同样2能分解为1 和 2
有发现什么吗? 6分解出来的就是 1 ∗ 1 , 1 ∗ 3 , 2 ∗ 1 , 2 ∗ 3 1*1 , 1*3 , 2* 1 , 2 * 3 11,13,21,23
其实就是说一个序列的权值就是其中(2的数量 + 1) * (3的数量 + 1)

好了解决这个问题就到了我们最重要的如何避免遍历 2 n − 1 2^n - 1 2n1

其实也很简单,整个数组只可能出现三种数字,那也就是三种数字的排列组合再结合上面说的规律算出总数即可

可能这么说有点抽象,那么写出公式就是不难理解了

假设 num[1] = 1的数量
num[2] = 2的数量
num[3] = 3的数量

n 1 = 2 n u m [ 1 ] n1 = 2^{num[1]} n1=2num[1]

n 2 = ∑ i = 0 i = n u m [ 2 ] ( i + 1 ) ∗ C n u m [ 2 ] i n2 = \sum_{i = 0}^{i=num[2]}(i + 1) *C_{num[2]}^i n2=i=0i=num[2](i+1)Cnum[2]i

n 3 = ∑ i = 0 i = n u m [ 3 ] ( i + 1 ) ∗ C n u m [ 3 ] i n3 = \sum_{i = 0}^{i=num[3]}(i + 1) *C_{num[3]}^i n3=i=0i=num[3](i+1)Cnum[3]i

n1是因为1的不影响权值,无关紧要,只需要知道1的组合总数有多少就可以了,用二项式定理可得
n 1 = ∑ i = 0 i = n u m [ 1 ] C n u m [ 1 ] i = ( 1 + 1 ) n u m [ 1 ] n1 = \sum_{i = 0}^{i=num[1]}C_{num[1]}^i = (1 + 1)^{num[1]} n1=i=0i=num[1]Cnum[1]i=(1+1)num[1]

所以最后的结果就是n1 * n2 * n3 - 1, 减一是因为不可能都是0的情况

那为什么有两个难度

问题就在组合数怎么求上了

先来看F题
数据范围并不大,组合数就可以用动态规划的想法求出来

实际上就是 C n m = C n − 1 m − 1 + C n − 1 m C_n^m = C_{n-1}^{m-1} + C_{n-1}^{m} Cnm=Cn1m1+Cn1m

代码如下

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1005 , mod = 1e9 + 7;
int n,t;
int num[4];
long long C[N][N];
void init()
{
    for(int i=0;i<N;i++){
        for(int j=0;j<=i;j++){
            if(!j) C[j][i] = 1;
            else C[j][i] = (C[j-1][i-1] + C[j][i-1]) % mod;
        }
    }
}
int qmi(int a,int b)
{
    ll res = 1;
    while (b){
        if(b & 1) res = res * (ll)a % mod;
        a = (ll)a * a % mod;
        b >>= 1;
    }
    return res % mod;
}
int check(int j)
{
    ll res = 0;
    for(int i = 0; i <= num[j] ; i ++){
        res = res % mod + ((C[i][num[j]] % mod) * (i+1)); 
    }
    return res % mod;
}
int main()
{
    init();
    cin >> n;
    for(int i=0;i<n;i++){
        cin >> t;
        num[t] ++;
    }
    
    ll one,two,thr;
    one = qmi(2,num[1]) % mod;
    two = check(2) % mod;
    thr = check(3) % mod;
    
    cout << (((one * two) % mod * thr) % mod - 1 + mod) % mod << endl;
}

但是G题并不是的数据范围不能开一个二维数组啊,所以就需要从组合数的公式本身出发

C n m = n ! m ! ∗ ( n − m ) ! C_n^m = \frac{n!}{m! * (n-m)!} Cnm=m!(nm)!n!

所以我们只需要遍历出来数据范围内所有的数字的阶乘就行了

看到这里你可能兴致冲冲的想要开始做了,但是我必须提醒一下的是

这个题是要求取模的,但是取模运算是不适用于除法的,也就是说不能单纯的对结果取个模就行了

难就难在这了

我们需要一个数学概念,逆元
就是 a ∗ a − 1 ≡ 1 ( m o d p ) a * a^{-1} \equiv 1 (mod \quad p) aa11(modp)
这里的 a − 1 a^{-1} a1不是a的倒数,而是一个乘以a然后对p取余还能等于1的数字

而我们就是要算出数据范围内阶乘以及他的逆元

具体实现如下

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod =  1e9 + 7,  N = 2e5 + 5;
ll nums[4], n, t;

//快速幂
int qmi(int a,int b)
{
    ll res = 1;
    while (b){
        if(b & 1) res = res * (ll)a % mod;
        a = (ll)a * a % mod;
        b >>= 1;
    }
    return res % mod;
}

//为了储存阶乘和逆元用的
ll f[N],inf[N];
void init()
{
    f[0] = inf[0] = 1;
    for(int i=1;i<N;i++){
        f[i] = f[i - 1] * i % mod;
        inf[i] = inf[i - 1] * qmi(i,mod-2) % mod;
    }
}

//组合数计算
ll C(int a,int b)
{
    return f[b] * inf[b-a] % mod * inf[a] % mod;
}

//算一共有多少种可能用的
int check(int o)
{
    ll res = 0;
    for(int i=0;i<=nums[o];i++){
        res = (res + (i + 1) * C(i,nums[o]) % mod) % mod;
    }
    return res % mod;
}
int main()
{
    init();
    
    cin >> n;
    while (n--){
        cin >> t;
        nums[t] ++;
    }
    
    ll one,two,thr;
    one = qmi(2,nums[1]) % mod;
    two = check(2) % mod;
    thr = check(3) % mod;
    
    //涉及到取模的减法运算一定别忘了要加一个mod,不然可能变成负数
    cout << ((one * two) % mod * thr % mod + mod - 1) % mod << endl;
    return 0;
}

  • 30
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值