组合数问题-容斥&DP&Lucas

“蓝桥杯”练习系统 (lanqiao.cn)

参考链接:[(7条消息) 蓝桥杯][2019年第十届真题]组合数问题(数位dp,卢卡斯定理)_xuxiaobo1234的博客-CSDN博客

题意:

给定\(n,m,k\),求有多少对\((i,j)\)满足\(1 \leq i \leq n, 0 \leq j \leq min(i,m)\)\(C \equiv 0 \pmod k\)\(k\)是质数,其中\(C_i^j\)是组合数,表示从\(i\)个不同的数中选出\(j\)个组成一个集合的方案数。

思路:

根据Lucas定理,

\[C_n^m \equiv \prod_{i=0}^k C_{n_i}^{m_i} \pmod p\\ n = n_{k}p^{k} + n_{k-1}p^{k-1} + \dots + n_{1}p + n_{0}\\ m = m_{k}p^{k} + m_{k-1}p^{k-1} + \dots + m_{1}p + m_{0} \]

我们可以发现,当对\(k\)取模为\(0\)的时候,必然存在某个\(C_{n_i}^{m_i}\)\(0\)。组合数定义中,当\(n<m\)的时候,\(C_n^m\)的值为\(0\)。因此我们只需要求使在\(p\)进制下至少存在一位\(i\)使\(a_i < b_i\)。那么为了方便,可以用排除法,先求总的结果数量,再求所有位上\(a_i \geq b_i\)的结果数量并相减得到答案。

题目最后转化为给定两个数\(n,m\)\(k\)进制表示。求有多少种\((a,b)\)组合\((a\in [1,n], b \in [1,m])\),使每一位上\(a_i \geq b_i\),并且\(a>=b\)

可以通过容斥和DP来求这个结果。

可以把数据通过容斥分为四类进行DP,求前\(i\)位DP的四种状态:(设第一位是最小的一位,从高位往低位DP)

\[高i位n、m都是上界:\\ dp[i][0]=dp[i+1][0] \& (a[i] >= b[i])\\ 高i位n是上界,m不是:\\ dp[i][1]=dp[i+1][1]*cal1(a[i],q)+dp[i+1][0]*cal(a[i],b[i]-1)\\ 高i位m是上界,n不是:\\ dp[i][2]=dp[i+1][2]*cal2(q,b[i])+dp[i+1][0]*cal(a[i]-1,b[i])\\ 高i位n和m都不是上界:\\ dp[i][3]=dp[i+1][3]*cal(p,p)\\ +dp[i+1][1]*cal(a[i]-1,p)+dp[i+1][2]*cal(a[i],b[i]-1)\\ +dp[i+1][0]*cal(a[i]-1,b[i]-1) \]

需要注意的坑点是:

  1. cal函数中取模需要在判断\(x,y\)大小之后,避免出现取模后判断大小相反的情况
  2. \(a,b\)数组需要初始化
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int mx = 1e2 + 10;
const ll mod = 1e9 + 7;

ll n, m, k, t;
ll cnt;
ll a[mx], b[mx];
ll dp[mx][4];
ll inv2;

ll ksm(ll x, ll y){
    ll ans = 1;
    while (y){
        if (y & 1) ans = (ans * x) % mod;
        x = (x * x) % mod;
        y = y >> 1;
    }
    return ans;
}

ll inv(ll x){
    return ksm(x, mod - 2LL);
}

//第一个数据[0,x] 第二个数据[0,y] 使x>=y
ll cal(ll x, ll y){//注意!!必须在if判断之后再进行取模 不然会出现 y 和 x 的大小关系相反的问题!!
//    x %= mod, y %= mod;
    if (y >= x){
        x %= mod, y %= mod;
        return (((x + 2) * (x + 1) % mod) * inv2) % mod;
    }
    x %= mod, y %= mod;
    return (cal(y, y) + ((x - y) * (y + 1)) % mod) % mod;
}

//第一个数 x0 第二个数[0,y] 使 x>=y
ll cal1(ll x0, ll y ){
    return min(x0, y) + 1;
}

//第一个数 [0,x] 第二个数 y0 使 x>=y
ll cal2(ll x, ll y0){
    return x < y0 ? 0 : x - y0 + 1;
}

ll add(ll x, ll y){return (x+y)%mod;}
ll add(ll x, ll y, ll z){return add(add(x,y), z);}
ll add(ll x, ll y, ll z, ll w){return add(add(x,y,z),w);}

void solve(){
    scanf("%lld %lld", &n, &m);
    ll cnta = 0, cntb = 0;
    if (m > n) m = n;
    ll nn = n, mm = m;
    while (nn){
        a[++cnta] = nn % k;
        nn /= k;
    }
    while (mm){
        b[++cntb] = mm % k;
        mm /= k;
    }
    cnt = max (cnta, cntb);

    //容斥
    dp[cnt+1][0] = 1;//两者都上界
    dp[cnt+1][1] = 0;//前i个n到达上界 m没有到达
    dp[cnt+1][2] = 0;//前i个m到达上界 n没有到达
    dp[cnt+1][3] = 0;//两个都没有到达上界
    for (ll i = cnt; i >= 1; i--){
        dp[i][0] = dp[i+1][0] & (a[i] >= b[i]);
        //n到达上界 所以必须取 a[i] m没有到达上界 但dp[i+1][1]已经是不到达上界的了 所以可以随便取
        //但是dp[i+1][0]中 m已经到达了上界 为了让dp[i][1]不到达上界 第二个数只能取 [0,b[i]-1]
        dp[i][1] = add(dp[i+1][1] * cal1(a[i],k-1),dp[i+1][0] * cal1(a[i], b[i]-1));
        //同理
        dp[i][2] = add(dp[i+1][2] * cal2(k-1, b[i]), dp[i+1][0] * cal2(a[i]-1, b[i]));
        //算两者都没有到达上界的
        //对于dp[i+1][0]之前两者都到达了上界 所以这次两个都不能取上界值
        dp[i][3] = add(dp[i+1][3] * cal(k-1,k-1),
                       dp[i+1][0] * cal(a[i]-1, b[i]-1),
                       dp[i+1][1] * cal(a[i]-1,k-1),
                       dp[i+1][2] * cal(k-1, b[i]-1));
        a[i] = b[i] = 0;//要记得初始化!!! 因为得到a b数组的时候 有些地方默认为 0 如果不初始化会影响后续的样例
    }
    ll tot = cal(n, m);
    for (ll i = 0; i <= 3; i++) tot = (tot - dp[1][i] + mod) % mod;
    printf("%lld\n", tot);
}

int main(){
    inv2 = inv(2LL);
    scanf("%lld %lld", &t, &k);
    while (t--)
    solve();
    return 0;
}
posted @ 2021-09-26 09:09  反射狐  阅读(59)  评论(0编辑  收藏  举报