计数类题目内容汇总

计数题常用初始化

计数题常常用到组合数和排列数等,需要预先处理较大范围的逆元、阶乘和阶乘逆元,该部分需要使用线性求逆元等方式进行初始化。

逆元、阶乘和阶乘逆元的初始化代码:

//大质数
const int mod = 998244353;

//计数范围
const int maxn = 2e5+5;

//逆元数组、阶乘数组、阶乘逆元数组,
//inv[i] = i的逆元, mul[i] = i!, invmul[i] = mul[i]的逆元
int inv[maxn],mul[maxn],invmul[maxn];

void init(){
    //下标为0和1时,数组都初始化为1
    inv[0] = inv[1] = mul[0] = mul[1] = invmul[0] = invmul[1] = 1;
    for(int i=2;i<maxn;i++){
        mul[i] = mul[i-1]*i%mod;
        //线性逆元公式
        inv[i]= (mod - mod/i)*inv[mod%i]%mod;
        invmul[i] = invmul[i-1]*inv[i]%mod;
    }
}

计数过程中,常常需要使用到快速幂和某个数的逆元,快速幂和逆元代码如下:

//取模的大质数
const int mod = 998244353;

//快速幂,求a^k%mod的结果,时间复杂度为O(log(k))
int qpow(int a,int k){
    int res=1;
    while(k){
        if(k&1)res = res*a%mod;
        a = a*a%mod;
        k>>=1;
    }
    return res;
}

//逆元,x^(-1) = x^(mod-2) % mod 
int Inv(int x){
    return qpow(x,mod-2);
}

排列数、组合数的代码如下:

//n中取m(不考虑顺序)的方案数
int C(int n,int m){
    return mul[n]*invmul[m]%mod*invmul[n-m]%mod;
}

//n中取m(考虑顺序)的方案数
int A(int n,int m){
    return mul[n]*invmul[m]%mod;
}

例题(补题后更新)

牛客2023寒假基础训练营3 Hhttps://ac.nowcoder.com/acm/contest/46811/H

牛客竞赛OJ视频讲解https://www.bilibili.com/video/BV13D4y1p7Sg?p=11&vd_source=a501dd41e3f5b6da951f25fd0f0f7d06        和的期望等于期望的和,但是积的期望未必等于期望的积(独立时取等于)所以考虑乘积的期望或期望的成绩这种方式是不正确的,但是可以对所求部分进行计数,最后除以方案数即可。

计数过程转化为5状态动态规划,5状态分别为空串(状态0),red-red(状态1),red-edr(状态2),edr-red(状态3)和edr-edr(状态4),A-B表示字符串的前缀为A后缀为B。下图为状态机图,橙色为状态编号,紫色为操作的编号,也是状态转移的方式,绿色为数值的变化。

 每个状态在转移过程中需要维护两个值,一为该状态下所有字符串的满足要求子串数量,一为该状态下方案数,初始只有状态0的方案数为1,其余值为0,按照上图方式转移k次即可算出各状态的贡献,求和后计算期望即可。

AC码:

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 1e9+7;
const int inv3 = (mod+1)/3;
int qpow(int a,int k){
    int res=1;
    while(k){
        if(k&1)res = res*a%mod;
        k>>=1;
        a=a*a%mod;
    }
    return res;
}
signed main(){
    int k;
    cin>>k;
    int dp[5][2]={0};
    dp[0][1] = 1;
    int buf[5]={0};
    for(int i=0;i<k;i++){
        buf[0] = 0;
        buf[1] = (dp[0][0]+dp[1][0]*10%mod+dp[1][0]+dp[2][0]+dp[0][1]+dp[1][1]+dp[2][1])%mod;
        buf[2] = (dp[1][0]+dp[2][0]+10*dp[2][0]%mod+dp[2][1])%mod;
        buf[3] = (dp[0][0]+dp[3][0]+dp[3][0]*10%mod+dp[4][0]+dp[3][1]+dp[3][1]*9%mod)%mod;
        buf[4] = (dp[3][0]+dp[4][0]+dp[4][0]*10%mod+dp[3][1]+dp[4][1])%mod;
        for(int i=0;i<5;i++)dp[i][0] = buf[i];
        
        buf[0] = dp[0][1];
        buf[1] = (dp[0][1]+dp[1][1]+dp[1][1]+dp[2][1])%mod;
        buf[2] = (dp[2][1]+dp[2][1]+dp[1][1])%mod;
        buf[3] = (dp[0][1]+dp[3][1]+dp[3][1]+dp[4][1])%mod;
        buf[4] = (dp[4][1]+dp[4][1]+dp[3][1])%mod;
        for(int i=0;i<5;i++)dp[i][1] = buf[i];
    }
    int s = 0;
    for(int i=0;i<5;i++)s=(s+dp[i][0])%mod;
    cout<<s*qpow(inv3,k)%mod<<endl;
}   

CF1792 F1https://codeforces.com/contest/1792/problem/F1

Educational Codeforces Round 142 Editorialhttps://codeforces.com/blog/entry/111835

牛客2023寒假基础训练营6 Lhttps://ac.nowcoder.com/acm/contest/46814/L

牛客竞赛OJ视频讲解https://www.bilibili.com/video/BV1JY411D7mr?p=12 考虑每个噩梦格子,每个噩梦格子的影响分为两种,一种为直接影响到达美梦格子的方案数,一种为影响到达其他噩梦格子的方案数,于是到达下层的噩梦格子的方案数会受到上层噩梦格子的影响而减小,平方时间复杂度考虑噩梦格子之间的相互影响即可。

AC码:

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 1e9+7;
 
int qpow(int a,int k){
    int res=1;
    while(k){
        if(k&1)res = res*a%mod;
        a = a*a%mod;
        k>>=1;
    }
    return res;
}
 
const int maxn = 2e5+5;
  
int inv[maxn],mul[maxn],invmul[maxn];
  
void init(){
    inv[0] = inv[1] = mul[0] = mul[1] = invmul[0] = invmul[1] = 1;
    for(int i=2;i<maxn;i++){
        mul[i] = mul[i-1]*i%mod
        inv[i]= (mod - mod/i)*inv[mod%i]%mod;
        invmul[i] = invmul[i-1]*inv[i]%mod;
    }
}
 
bool cmp(pair<int,int> p1, pair<int,int> p2){
    return p1.first+p1.second<p2.first+p2.second;
}
 
int C(int n,int m)
{
    return mul[n]*invmul[m]%mod*invmul[n-m]%mod;
}
 
bool under(pair<int,int>p1,pair<int,int> p2){
    if(p1.first<=p2.first&&p1.second<=p2.second)
        return true;
    return false;
}
 
signed main(){
    init();
    int n,k;
    cin>>n>>k;
    vector<pair<int,int> > vp(k);
    for(int i=0;i<k;i++)cin>>vp[i].first>>vp[i].second;
    vector<int> val(k);
    sort(vp.begin(),vp.end(),cmp);
    int s = qpow(2,n-1);
    for(int i=0;i<k;i++){
        val[i] = C(vp[i].first+vp[i].second-2,vp[i].first-1);
        for(int j=0;j<i;j++){
            if(under(vp[j],vp[i])){
                val[i] = (val[i]-(C(vp[i].first+vp[i].second-vp[j].first-vp[j].second,vp[i].first-vp[j].first)*val[j]%mod)+mod)%mod;
            }
        }
        s=(s-qpow(2,n+1-vp[i].first-vp[i].second)*val[i]%mod+mod)%mod;
    }
    cout<<s<<endl;
}

CF1788Dhttps://codeforces.com/contest/1788/problem/D

看似是求点的移动,实际上是比较间隔的大小,当一个间隔长度同时超过左右两边的间隔长度时,该间隔有一个贡献,统计每个间隔可以有贡献的方案数,即统计左侧间隔小于该间隔的方案数和右侧间隔小于该间隔的方案数做乘积。

由枚举点到枚举区间的贡献,实在是妙。但是在时间复杂度上卡掉了快速幂求2的幂次,必须预处理,实在是可恶!

AC码

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
const int mod = 1e9+7;

using namespace std;

vector<int> q2;

void init(){
    q2.push_back(1);
    int cnt = 3000;
    while(cnt--){
        q2.push_back((q2[q2.size()-1]<<1)%mod);
    }
}

void solve(){
    int n;
    cin>>n;
    vector<int>a(n+2);
    for(int i=1;i<=n;i++)cin>>a[i];
    a[0] = -0x7f7f7f7f7f7f;
    a[n+1]= 0x7f7f7f7f7f7f;
    int res=0;
    for(int i=1;i<=n;i++){
        for(int j=i+1;j<=n;j++){
            int l = a[j]-a[i];
            int x = 0;
            int ll=0,rr=i;
            while(ll+1<rr){
                int mid = (ll+rr)/2;
                if(a[i]-a[mid]>l){
                    ll=mid;
                }
                else rr=mid;
            }
            int d = i-rr;
            x = (q2[d]-1)*q2[ll]%mod;

            int y = 0;
            ll = j;
            rr = n+1;
            while(ll+1<rr){
                int mid = (ll+rr)/2;
                if(l>a[mid]-a[j])ll = mid;
                else rr = mid;
            }
            d = ll-j;
            y = (q2[d]-1)*q2[n-ll]%mod;
            res = (res+x*y%mod)%mod;
        }
    }
    cout<<(res+q2[n]-1-n+mod)%mod<<endl;
}

signed main()
{
    init();
    int T=1;
    while(T--){
        solve();
    }
    return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值