数学知识——组合计数

排列数

从n个不同的元素中取出m个元素组成一列,产生的不同排列的数量。

Anm=n!(n−m)!A_n^m=\frac{n!}{(n-m)!}Anm=(nm)!n!

怎么得来的?先选一个元素,选法为nnn,选第二个元素,选法为n−1n-1n1,以此类推,选第mmm个元素,选法为n−m+1n-m+ 1nm+1,相乘即为Anm=n!(n−m)!A_n^m=\frac{n!}{(n-m)!}Anm=(nm)!n!

组合数

从n个不同的元素中取出m个组成一个集合(不考虑顺序),产生不同集合数量。
根据数据范围不同,求组合数有四种方法。

递推

Cab=Ca−1b+Ca−1b−1C_a^b=C_{a-1}^b+C_{a-1}^{b-1}Cab=Ca1b+Ca1b1 证明:
我们可以把选择的方案分成两大类,对于aaa个数里面的某个数,我可能选出来了,也可能没选出来
选出来了:Ca−1b−1C_{a-1}^{b-1}Ca1b1,还需要在剩下的a−1a-1a1个数选b−1b-1b1个。
没选出来:Ca−1bC_{a-1}^{b}Ca1b需要在剩下的a−1a-1a1个数选bbb个。

给定 nnn 组询问,每组询问给定两个整数 a,ba,bab,请你输出 Cba mod(109+7)C_b^a\ mod(10^9+7)Cba mod(109+7)的值。

1≤n≤10000,1≤n≤10000,1n10000,
1≤b≤a≤20001≤b≤a≤20001ba2000

先利用地推求出所有的CabC_a^bCab,时间复杂度为O(K2)O(K^2)O(K2),接下来对于每一个询问都可以在O(1)O(1)O(1)时间复杂度内回答,总的时间复杂度为O(K2+N)O(K^2+N)O(K2+N)

void init()
{
    for (int i = 0; i <= 2000; i ++ )
        for (int j = 0; j <= i; j ++ )
        {
            if (j == 0) c[i][j] = 1;
            else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
        }
}
int main()
{
    cin >> n;
    init();
    while (n -- )
    {
        int a, b;
        cin >> a >> b;
        cout << c[a][b] << endl;
    }
    return 0;
}

预处理

Cab=a!b!(a−b)!C_a^b=\frac{a!}{b!(a-b)!}Cab=b!(ab)!a!(在AabA_a^bAab的基础上多除一个b!b!b!

给定 nnn 组询问,每组询问给定两个整数 a,ba,bab,请你输出 Cba mod(109+7)C_b^a\ mod(10^9+7)Cba mod(109+7)的值。

1≤n≤10000,1≤n≤10000,1n10000,
1≤b≤a≤1051≤b≤a≤10^51ba105

fact[i]fact[i]fact[i]表示a!a!a!,用infact[i]infact[i]infact[i]表示a!a!a!的逆元。
fact[i]=fact[i−1]∗i%pfact[i] = fact[i - 1] * i \% pfact[i]=fact[i1]i%p
infact[i]=infact[i−1]∗qmi(i,p−2,p)%pinfact[i] = infact[i - 1] * qmi(i, p- 2, p) \% pinfact[i]=infact[i1]qmi(i,p2,p)%p

时间复杂度为O(KlogP+N)O(KlogP+N)O(KlogP+N)

int qmi(int a, int k, int m)
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (ll) res * a % mod;
        k >>= 1;
        a = (ll) a * a % mod;
    }
    return res;
}
int main()
{
    cin >> n;
    fact[0] = infact[0] = 1;
    for (int i = 1; i <= 100000; i ++ ) 
    {
        fact[i] = fact[i - 1] * i % mod;
        infact[i] = infact[i - 1] * qmi(i, mod - 2, mod) % mod;
    }
    while (n -- )
    {
        int a, b;
        cin >> a >> b;
        cout << fact[a] * infact[a - b] % mod * infact[b] % mod << endl;
    }
    return 0;
}

lucas定理

Cab≡Ca%pb%pCa/pb/p (mod p)C_a^b≡C_{a\%p}^{b\%p}C_{a/p}^{b/p}\ (mod\ p)CabCa%pb%pCa/pb/p (mod p)
证明:

给定 nnn 组询问,每组询问给定两个整数 a,ba,bab,请你输出 Cba mod pC_b^a\ mod\ pCba mod p的值。

1≤n≤201≤n≤201n20
1≤b≤a≤10181≤b≤a≤10^{18}1ba1018
1≤p≤1051≤p≤10^51p105

时间复杂度为O(NlogPKPlogP)O(Nlog_P^KPlogP)O(NlogPKPlogP)
NNN为数据规模
logPKlog_P^KlogPKlucaslucaslucas的递归次数
PPP为求Ca%pb%pC_{a\%p}^{b\%p}Ca%pb%p的过程中的循环次数,因为a,ba,ba,b都进行了对ppp的取模,所以求阶乘的规模就在O(P)O(P)O(P)
logPlogPlogP为qmi的复杂度

int qmi(int a, int k, int p)
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (ll) res * a % p;
        k >>= 1;
        a = (ll) a * a % p;
    }
    return res;
}
int c(ll a, ll b, int p)
{
    int res = 1;
    for (int i = 1, j = a; i <= b; i ++ , j -- )
    {
        res = (ll) res * j % p;
        res = (ll) res * qmi(i, p - 2, p) % p;
        cout << res << endl;
    }
    cout << endl;
    return res;
}
int lucas(ll a, ll b, int p)
{
    if (b < p && a < p) return c(a, b, p);
    else return (ll) c(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}
int main()
{
    cin >> n;
    while (n -- )
    {
        ll a, b;
        int p;
        cin >> a >> b >> p;
        cout << lucas(a, b, p) << endl;
    }
    return 0;
}

分解质因数

给定两个整数 a,ba,bab,请你输出 CbaC_b^aCba的值。
注意结果可能很大,需要使用高精度计算。

1≤b≤a≤50001≤b≤a≤50001ba5000

1.先筛选出aaa以内的所有素数
2.我们对a!a!a!分解质因数,其中包含的质数ppp的个数为⌊ap⌋+⌊ap2⌋+⌊ap3⌋+...\lfloor\frac{a}{p} \rfloor+\lfloor\frac{a}{p^2} \rfloor+\lfloor\frac{a}{p^3} \rfloor+...pa+p2a+p3a+...CabC_a^bCab分解的结果中某个质因数的个数其实是a!a!a!中这个质数的个数减去b!b!b!(a−b)!(a-b)!(ab)!中这个质数的个数。
3.我们得到了CabC_a^bCab每个质因数的个数,可以直接使用高精度乘法求解。

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = 1;
            if (i % primes[j] == 0) break; 
        }
    }
}
int get(int a, int p)
{
    int res = 0;
    while (a)
    {
        res += a / p;
        a /= p;
    }
    return res;
}
vector<int> mul(vector<int> a, int b)
{
    vector<int> c;
    int t = 0;
    for (int i = 0; i < a.size(); i ++ )
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    while (t)
    {
        c.push_back(t % 10);
        t /= 10;
    }
    return c;
}
int main()
{
    int a, b;
    cin >> a >> b;
    get_primes(a);
    for (int i = 0; i < cnt; i ++ )
    {
        int p = primes[i];
        sum[i] += get(a, p) - get(a - b, p) - get(b, p);
    }
    vector<int> res;
    res.push_back(1);
    for (int i = 0; i < cnt; i ++ )
        for (int j = 1; j <= sum[i]; j ++ )
        {
            res = mul(res, primes[i]);
        }
    for (int i = res.size() - 1; i >= 0; i -- )
        cout << res[i];
    puts("");
    return 0;
}

可以做一些数学题感受一下
q1.由0,1,2,3,4,50,1,2,3,4,50,1,2,3,4,5可以组成多少个没有重复数字的五位奇数。
特殊位置优先,先考虑末尾以及首位,末尾只能放1,3,51,3,51,3,5,首位不能放000,答案为C31∗C41∗A33C_3^1*C_4^1*A_3^3C31C41A33

q2.777 人站成一排,其中甲乙相邻且丙丁相邻,共有多少种不同的排法。
我们可以先将甲乙,丙丁分别看成是一个人,这时候的方案数为然后在考虑甲乙,丙丁内部的方案数,各为A22A_2^2A22
总方案数为A55∗A22∗A22A_5^5*A_2^2*A_2^2A55A22A22

q3.888人排成前后两排,每排444人,其中甲乙在前排,丙在后排,共有多少排法。
A41∗A42∗A55A_4^1*A_4^2*A_5^5A41A42A55

q4.有555个不同的小球,装入444个不同的盒内,每盒至少装一个球,求共有多少不同的装法。
555个里面选222个捆绑起来,最后把444个元素做全排列。
C52∗A44C_5^2*A_4^4C52A44

q5.6个人站成一列拍照,甲乙相邻,丙丁不相邻的方案数。
2A55−4A442A_5^5-4A_4^42A554A44

卡特兰数

在一个二维平面内,从(0, 0)出发到达(n, n),每次可以向上或者向右走一格,0代表向右走一个,1代表向上走一格,则每条路径都会代表一个01序列,则满足任意前缀中0的个数不少于1个数序列对应的路径则右下侧
在这里插入图片描述
符合要求的路径必须严格在上图中红色线的下面(不可以碰到图中的红线,可以碰到绿线)。则我们考虑任意一条不合法路径,例如下图:
在这里插入图片描述
所有路径的条数为 C2nnC_{2n}^{n}C2nn,其中不合法的路径有 C2nn−1C_{2n}^{n-1}C2nn1 条,因此合法路径有:
C2nn−C2nn−1C_{2n}^{n}-C_{2n}^{n-1}C2nnC2nn1
=(2n)!n!n!−(2n)!(n−1)!(n+1)!=\frac{(2n)!}{n!n!}-\frac{(2n)!}{(n-1)!(n+1)!}=n!n!(2n)!(n1)!(n+1)!(2n)!
=(2n)!n!n!−nn+1(2n)!n!n!=\frac{(2n)!}{n!n!}-\frac{n}{n+1}\frac{(2n)!}{n!n!}=n!n!(2n)!n+1nn!n!(2n)!
=1n+1(2n)!n!n!=\frac{1}{n+1}\frac{(2n)!}{n!n!}=n+11n!n!(2n)!
=1n+1C2nn=\frac{1}{n+1}C_{2n}^{n}=n+11C2nn

例题

牡牛和牝牛

约翰要带 NNN 只牛去参加集会里的展示活动,这些牛可以是牡牛,也可以是牝牛。

牛们要站成一排,但是牡牛是好斗的,为了避免牡牛闹出乱子,约翰决定任意两只牡牛之间至少要有 KKK 只牝牛。

请计算一共有多少种排队的方法,所有牡牛可以看成是相同的,所有牝牛也一样,答案对 500001150000115000011 取模。

输入格式
一行,输入两个整数 NNNKKK

输出格式

一个整数,表示排队的方法数。

数据范围
1≤N≤105,1≤N≤10^5,1N105,
0≤K<N0≤K<N0K<N

输入样例:

4 2

输出样例:

6

样例解释

6

种方法分别是:牝牝牝牝,牡牝牝牝,牝牡牝牝,牝牝牡牝,牝牝牝牡,牡牝牝牡。

f[i]f[i]f[i]集合:考虑前iii头牛,且第iii头牛是111的方案
f[i]f[i]f[i]属性:方案数
状态计算:f[i]=f[i−k−1]+f[i−k−2]+⋅⋅⋅+f[0]f[i] = f[i - k - 1] + f[i - k - 2] + ··· + f[0]f[i]=f[ik1]+f[ik2]+⋅⋅⋅+f[0]
(↑集合划分的依据是题目的要求,即当前1与上一个1之间至少要间隔k个0)

此处设置一个边界f[0]f[0]f[0]表示只有0没有1的边界情况

#include <iostream>
using namespace std;
const int N = 100010;
const int mod = 5000011;
int s[N], f[N];
int n, k;
int main()
{
    cin >> n >> k;
    s[0] = f[0] = 1;
    for (int i = 1; i <= n; i ++ )
    {
        f[i] = s[max(i - k - 1, 0)];
        s[i] = (s[i - 1] + f[i]) % mod;
    }
    cout << s[n] << endl;
    return 0;
}

方程的解

佳佳碰到了一个难题,请你来帮忙解决。

对于不定方程 a1+a2+⋯+ak−1+ak=g(x)a_1+a_2+⋯+a_{k−1}+a_k=g(x)a1+a2++ak1+ak=g(x),其中 k≥1k≥1k1
k∈N∗,xk∈N∗,xkNx 是正整数,g(x)=xx mod1000g(x)=x^x\ mod1000g(x)=xx mod1000
(即 xxx^xxx 除以 1000 的余数),x,kx,kx,k 是给定的数。

我们要求的是这个不定方程的正整数解组数。

举例来说,当 k=3,x=2k=3,x=2k=3,x=2 时,方程的解分别为:
{a1=1a2=1a3=2,{a1=1a2=2a3=1,{a1=2a2=1a3=1\left\{\begin{array}{l} a_1=1\\ a_2=1\\ a_3=2 \end{array}\right., \quad \left\{\begin{array}{l} a_1=1\\ a_2=2\\ a_3=1 \end{array}\right., \quad \left\{\begin{array}{l} a_1=2\\ a_2=1\\ a_3=1 \end{array}\right.a1=1a2=1a3=2,a1=1a2=2a3=1,a1=2a2=1a3=1

输入格式

有且只有一行,为用空格隔开的两个正整数,依次为 k,xk,xk,x

输出格式

有且只有一行,为方程的正整数解组数。

数据范围
1≤k≤100,1≤k≤100,1k100,
1≤x<2311≤x<2^{31}1x<231,
k≤g(x)k≤g(x)kg(x)

输入样例:

3 2

输出样例:

3

n=xxn=x^xn=xx进行快速幂求解,接下来利用隔板法来求解的组数。
一种隔板法的情况和一组解时一一对应的,例如n=7,k=4n=7,k=4n=7,k=4时,
∗∗∣∗∗∣∗∗∣∗**|**|**|*就是唯一对应一组解a1=2,a2=2,a3=2,a4=1a_1=2, a_2=2, a_3=2,a_4=1a1=2,a2=2a3=2,a4=1a1=2,a2=2.a3=2,a4=1a_1=2, a_2=2. a_3=2,a_4=1a1=2,a2=2.a3=2,a4=1,也唯一对应一种隔板法情况∗∗∣∗∗∣∗∗∣∗**|**|**|*
方案总数为Cn−1k−1C_{n-1}^{k-1}Cn1k1

#include <iostream>
using namespace std;
typedef long long ll;
const int N = 150;
int c[1010][110][N];
int k, x;
void add(int a[], int b[], int c[])
{
    for (int i = 0, t = 0; i < N; i ++ )
    {
        t += b[i] + c[i];
        a[i] = t % 10;
        t /= 10;
    }
}
int qmi(int a, int k, int b)
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (ll)res * a % b;
        a = (ll)a * a % b;
        k >>= 1;
    }
    return res;
}
int main()
{
    cin >> k >> x;
    int n = qmi(x, x, 1000);
    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j <= min(i, k - 1); j ++ )
        {
            if (j == 0) c[i][j][0] = 1;
            else add(c[i][j], c[i - 1][j], c[i - 1][j - 1]);
        }
    }
    int pos = N - 1;
    while (c[n - 1][k - 1][pos] == 0) pos -- ;
    while (pos >= 0) cout << c[n - 1][k - 1][pos -- ];
    return 0;
}

车的放置

有下面这样的一个网格棋盘,a,b,c,da,b,c,da,b,c,d 表示了对应边长度,也就是对应格子数。

1.png

a=b=c=d=2a=b=c=d=2a=b=c=d=2时,对应下面这样一个棋盘:
在这里插入图片描述

要在这个棋盘上放 kkk 个相互不攻击的车,也就是这 kkk 个车没有两个车在同一行,也没有两个车在同一列,问有多少种方案。

只需要输出答案 mod 100003mod\ 100003mod 100003 后的结果。

输入格式

共一行,五个非负整数 a,b,c,d,ka,b,c,d,ka,b,c,d,k

输出格式

包括一个正整数,为答案 mod 100003mod\ 100003mod 100003 后的结果。

数据范围
0≤a,b,c,d,k≤10000≤a,b,c,d,k≤10000a,b,c,d,k1000,保证至少有一种可行方案。

输入样例:

2 2 2 2 2

输出样例:

38

如果我们面对的是一个n×mn×mn×m的矩形的话,考虑放置kkk个车的方案数。
因为同一行肯定不能放两个,所以第一步是在nnn行里面选择kkk行放置,即CnkC_n^kCnk,对于这kkk行,不同的排列方案数为m∗(m−1)∗...∗(m−k+1)m*(m-1)*...*(m-k+1)m(m1)...(mk+1),即PmkP_m^kPmk,最终答案为Cnk∗PmkC_n^k*P_m^kCnkPmk
考虑题目中的图,我们考虑先在上面的小矩形里面放iii个车,即Cbi∗PaiC_b^i*P_a^iCbiPai,余下的k−ik-iki个车放下面的矩形里,即Cdk−i∗Pa+c−ik−iC_d^{k-i}*P_{a+c-i}^{k-i}CdkiPa+ciki
最终答案为Cnk∗Pmk∗Cdk−i∗Pa+c−ik−iC_n^k*P_m^k*C_d^{k-i}*P_{a+c-i}^{k-i}CnkPmkCdkiPa+ciki
O(nlog m)O(nlog\ m)O(nlog m)预处理出fact[i]fact[i]fact[i]infact[i]infact[i]infact[i]之后,可以直接在O(1)O(1)O(1)的时间复杂度求出答案。

#include <iostream>
using namespace std;
typedef long long ll;
const int N = 2010;
const int mod = 100003;
int fact[N], infact[N];
int c[N][N];
int qmi(int a, int k, int b)
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (ll)res * a % b;
        a  = (ll)a * a % b;
        k >>= 1;
    }
    return res;
}
int C(int a , int b)
{
    if(a < b) return 0;
    return (ll)fact[a] * infact[b] % mod  * infact[a-b] % mod;
}

int P(int a , int b)
{
    if(a < b) return 0;
    return (ll)fact[a] * infact[a - b] % mod;
}
int main()
{
    int a, b, c, d, k;
    cin >> a >> b >> c >> d >> k;
    fact[0] = infact[0] = 1;
    for (int i = 1; i < N; i ++ )
    {
        fact[i] = (ll)fact[i - 1] * i % mod;
        infact[i] = (ll)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
    }
    int res = 0;
    for (int i = 0; i <= b; i ++ )
    {
        res = (res + (ll)C(b, i) * C(d, k - i) % mod * P(a, i) % mod * P(a + c - i, k - i)) % mod;
    }
    cout << res << endl;
    return 0;
}

数三角形

给定一个 n×mn×mn×m 的网格,请计算三点都在格点上的三角形共有多少个。

下图为 4×44×44×4 的网格上的一个三角形。
在这里插入图片描述

注意:三角形的三点不能共线。

输入格式
输入一行,包含两个空格分隔的正整数 mmmnnn

输出格式
输出一个正整数,为所求三角形数量。

数据范围
1≤m,n≤10001≤m,n≤10001m,n1000

输入样例:

2 2

输出样例:

76

考虑反着求,即先求出来任选三点的方案总数,减去不合法的方案数。
任选三点的方案总数为C(n+1)(m+1)3C_{(n+1)(m+1)}^3C(n+1)(m+1)3
不合法的方案数分为以下三种情况:
1.三点构成的直线斜率为0,这样的情况总数为(n+1)Cm+13(n+1)C_{m+1}^{3}(n+1)Cm+13
2.三点构成的直线斜率不存在,这样的情况总数为(m+1)Cn+13(m+1)C_{n+1}^{3}(m+1)Cn+13
3.三点构成的直线斜率为正或为负,这两种情况方案总数是一样的,也正因此只需要考虑为正的情况,最后把结果乘2。

考虑枚举斜线的高度iii和宽度jjj,这样可以唯一确定一个直角三角形的形状,这样的三角形在方格图中有(n−i)∗(m−j)(n-i)*(m-j)(ni)(mj)个放置位置,斜边的两个端点为其中两个点,第三个点的位置为这条斜线上的整数坐标位置,这样的位置总数为gcd(i,j)−1gcd(i,j)-1gcd(i,j)1

第三种情况不合法的方案数为∑i≤n,j≤m2∗(n−i)∗(m−j)∗(gcd(i,j)−1)\sum_{i\leq n,j\leq m}2*(n-i)*(m-j)*(gcd(i,j)-1)in,jm2(ni)(mj)(gcd(i,j)1),求解的时间复杂度为O(n2logn)O(n^2logn)O(n2logn)

最终答案为C(n+1)(m+1)3−(n+1)Cm+13−(m+1)Cn+13−∑i≤n,j≤m2∗(n−i)∗(m−j)∗(gcd(i,j)−1)C_{(n+1)(m+1)}^3-(n+1)C_{m+1}^{3}-(m+1)C_{n+1}^{3}-\sum_{i\leq n,j\leq m}2*(n-i)*(m-j)*(gcd(i,j)-1)C(n+1)(m+1)3(n+1)Cm+13(m+1)Cn+13in,jm2(ni)(mj)(gcd(i,j)1)

#include <iostream>
using namespace std;
typedef long long ll;
const int N = 1010;
int n, m;
ll C(int x)
{
    return (ll)x * (x - 1) * (x - 2) / 6;
}
int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}
int main()
{
    cin >> n >> m;
    ll res = C((n + 1) * (m + 1)) - (m + 1) * C(n + 1) - (n + 1) * C(m + 1);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            res -= 2 * (n - i + 1) * (m - j + 1) * (gcd(i, j) - 1);
    cout << res << endl;
    return 0;
}

序列统计

给定三个整数 N,L,RN,L,RN,L,R,统计长度在 111NNN 之间,元素大小都在 LLLRRR之间的单调不降序列的数量。

输出答案对 106+310^6+3106+3 取模的结果。

输入格式

输入第一行包含一个整数 TTT,表示数据组数。
第二到第 T+1T+1T+1 行每行包含三个整数 N,L,RN,L,RN,L,R

输出格式

输出包含 TTT 行,每行有一个数字,表示你所求出的答案对 106+310^6+3106+3 取模的结果。

数据范围
0≤N,L,R≤109,0≤N,L,R≤10^9,0N,L,R109,
1≤T≤100,1≤T≤100,1T100
输入数据保证 L≤RL≤RLR

输入样例:

2
1 4 5
2 4 5

输出样例:

2
5

样例解释
对于第一组输入,满足条件的两个序列为 4,5{4},{5}4,5

元素大小在LLLRRR,将其映射为000R−LR-LRL之间,该问题就等价于对于0≤a1≤a2≤...≤ak≤R−L0\leq a_1\leq a_2\leq ...\leq a_k\leq R-L0a1a2...akRL,求出aia_iai合法解的总数

法1:直接对这个问题求解较为困难,我们先考虑下面这个问题:
对于x≤b1<b2<...<bk≤yx\leq b_1< b_2< ...<b_k \leq yxb1<b2<...<bky,我们想要求合法的bib_ibi的解的组数,答案就是在xxxyyy范围内选择出kkk的数的方案总数,即Cy−x+1kC_{y-x+1}^kCyx+1k,考虑能不能把本问题的场景转化为这个可以解决的问题。令ci=ai+i−1c_i=a_i+i-1ci=ai+i1,这样一来问题转化为0≤c1<c2<...<ck≤R−L+k−10\leq c_1< c_2< ...< c_k\leq R-L+k-10c1<c2<...<ckRL+k1解的组数,即CR−L+kkC_{R-L+k}^kCRL+kk
法2:令b1=a1,b2=a2−11,...,bk=ak−ak−1b_1=a_1,b_2=a_2-1_1,...,b_k=a_k-a_{k-1}b1=a1,b2=a211,...,bk=akak1,这样问题就转化为求满足条件0≤b1+b2+...+bk≤R−L0\leq b_1+b_2+...+b_k\leq R-L0b1+b2+...+bkRLbib_ibi的解的组数,联想隔板法,一般的隔板法解决的是解为正整数的情况,但是本问题中bib_ibi的取值是非负数,考虑令ci=bi+1c_i=b_i+1ci=bi+1,这样一来把问题转化为求满足条件k≤c1+c2+...+ck≤R−L+kk\leq c_1+c_2+...+c_k\leq R-L+kkc1+c2+...+ckRL+kcic_ici的解的组数,传统隔板法要求的是何为定值的情况,这里却要求小于等于一个值的情况,考虑加入kkk个隔板而不是k−1k-1k1个,这样前kkk个隔内的数量就是问题的一组解,最后一个隔就是没有选的数,最终结果为CR−L+kkC_{R-L+k}^kCRL+kk

法1和法2都得到了相同的结果,本问题最终要求的是∑i=1kCR−L+ii\sum_{i=1}^kC_{R-L+i}^ii=1kCRL+ii∑i=1kCR−L+ii=CR−L+11CR−L+22+...+CR−L+kk=CR−L+1R−L+CR−L+2R−L+...+CR−L+kR−L=CR−L+1R−L+1+CR−L+1R−L+CR−L+2R−L+...+CR−L+kR−L−CR−L+1R−L+1=CR−L+2R−L+1+CR−L+2R−L+...+CR−L+kR−L−1=CR−L+kR−L+1−1\sum_{i=1}^kC_{R-L+i}^i=C_{R-L+1}^1C_{R-L+2}^2+...+C_{R-L+k}^k=C_{R-L+1}^{R-L}+C_{R-L+2}^{R-L}+...+C_{R-L+k}^{R-L}=C_{R-L+1}^{R-L+1}+C_{R-L+1}^{R-L}+C_{R-L+2}^{R-L}+...+C_{R-L+k}^{R-L}-C_{R-L+1}^{R-L+1}=C_{R-L+2}^{R-L+1}+C_{R-L+2}^{R-L}+...+C_{R-L+k}^{R-L}-1=C_{R-L+k}^{R-L+1}-1i=1kCRL+ii=CRL+11CRL+22+...+CRL+kk=CRL+1RL+CRL+2RL+...+CRL+kRL=CRL+1RL+1+CRL+1RL+CRL+2RL+...+CRL+kRLCRL+1RL+1=CRL+2RL+1+CRL+2RL+...+CRL+kRL1=CRL+kRL+11,使用lucas直接求解即可。

时间复杂度为O(TlogpN(p+logp))O(Tlog_p^N(p+logp))O(TlogpN(p+logp))

#include <iostream>

using namespace std;

typedef long long LL;

const int p = 1e6 + 3;

int qmi(int a, int b) {
    int res = 1;
    while (b) {
        if (b & 1) res = (LL) res * a % p;
        a = (LL) a * a % p;
        b >>= 1;
    }
    return res;
}
int C(int a, int b) {
    int up = 1, down = 1;
    for (int i = a, j = 1; j <= b; ++ j, -- i) {
        up = (LL) up * i % p;
        down = (LL) down * j % p;
    }
    return (LL) up * qmi(down, p - 2) % p;  //逆元
}
int Lucas(int a, int b) {
    if (a < p && b < p) return C(a, b);
    return (LL)C(a % p, b % p) * Lucas(a / p, b / p) % p;
}
int main() {
    int T;
    cin >> T;
    while (T -- ) {
        int n, l, r;
        cin >> n >> l >> r;
        cout << (Lucas(r - l + n + 1, r - l + 1) - 1 + p) % p << endl;
    }
    return 0;
}

网络

某城市的街道呈网格状,左下角坐标为 A(0,0)A(0,0)A(0,0),右上角坐标为 B(n,m)B(n,m)B(n,m),其中 n≥mn≥mnm

现在从 A(0,0)A(0,0)A(0,0) 点出发,只能沿着街道向正右方或者正上方行走,且不能经过图示中直线左上方的点,即任何途径的点 (x,y)(x,y)(x,y) 都要满足 x≥yx≥yxy,请问在这些前提下,到达 B(n,m)B(n,m)B(n,m) 有多少种走法。

在这里插入图片描述

输入格式

仅有一行,包含两个整数 nnnmmm,表示城市街区的规模。

输出格式

输出一个整数,表示不同的方案总数。

数据范围
1≤m≤n≤50001≤m≤n≤50001mn5000

输入样例:

6 6

输出样例:

132

依据卡特兰数的推导,找出(n,m)(n,m)(n,m)关于y=x+1y=x+1y=x+1的对称点(m−1,n+1)(m−1,n+1)(m1,n+1),每一条非法路径都对应一条(0,0)(0,0)(0,0)(m−1,n+1)(m−1,n+1)(m1,n+1)的路径,因此合法路径=总路径数-非法路径,即Cn+mn−Cn+mm−1C_{n+m}^n-C_{n+m}^{m-1}Cn+mnCn+mm1

#include <iostream>
using namespace std;
const int N = 10010;
int primes[N], cnt;
bool st[N];
int a[N], b[N];
int n, m;
void init(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] * i <= n; j ++ )
        {
            st[primes[j] * i] = 1;
            if (i % primes[j] == 0) break;
        }
    }
}
int get(int n, int p)
{
    int s = 0;
    while (n) s += n / p, n /= p; 
    return s;
}
void mul(int r[], int &len, int p)
{
    int t = 0;
    for (int i = 0; i < len; i ++ )
    {
        t += r[i] * p;
        r[i] = t % 10;
        t /= 10;
    }
    while (t)
    {
        r[len ++ ] = t % 10;
        t /= 10;
    }
}
int C(int a, int b, int r[])
{
    int len = 1;
    r[0] = 1;
    for (int i = 0; i < cnt; i ++ )
    {
        int p = primes[i];
        int s = get(a, p) - get(b, p) - get(a - b, p);
        while (s -- ) mul(r, len, p);
    }
    return len;
}
void sub(int a[], int al, int b[], int bl)
{
    for (int i = 0, t = 0; i < al; i ++ )
    {
        a[i] -= t + b[i];
        if (a[i] < 0) a[i] += 10, t = 1;
        else t = 0;
    }
}
int main()
{
    cin >> n >> m;
    init(N - 1);
    int al = C(n + m, n, a);
    int bl = C(n + m, m - 1, b);
    sub(a, al, b, bl);
    int k = al - 1;
    while (!a[k]) k -- ;
    while (k >= 0) cout << a[k -- ];
    return 0;
}

有趣的数列

我们称一个长度为 2n2n2n 的数列是有趣的,当且仅当该数列满足以下三个条件:

  • 它是从 1∼2n1 \sim 2n12n2n2n2n 个整数的一个排列 {an}n=12n\{a_n\}_{n=1}^{2n}{an}n=12n

  • 所有的奇数项满足 a1<a3<⋯<a2n−1a_1<a_3< \dots < a_{2n-1}a1<a3<<a2n1,所有的偶数项满足 a2<a4<⋯<a2na_2<a_4< \dots <a_{2n}a2<a4<<a2n

  • 任意相邻的两项 a2i−1a_{2i-1}a2i1a2ia_{2i}a2i 满足:a2i−1<a2ia_{2i-1}<a_{2i}a2i1<a2i

对于给定的 nnn,请求出有多少个不同的长度为 2n2n2n 的有趣的数列。
因为最后的答案可能很大,所以只要求输出答案对 ppp 取模。

输入格式

一行两个正整数 n,pn,pn,p

输出格式

输出一行一个整数表示答案。

输入输出样例 #1

输入 #1

3 10

输出 #1

5

说明/提示

【数据范围】
对于 50%50\%50% 的数据,1≤n≤10001\le n \le 10001n1000
对于 100%100\%100% 的数据,1≤n≤1061\le n \le 10^61n1061≤p≤1091\le p \le 10^91p109

【样例解释】
对应的5个有趣的数列分别为(1,2,3,4,5,6),(1,2,3,5,4,6),(1,3,2,4,5,6),(1,3,2,5,4,6),(1,4,2,5,3,6)。

分析一道题是不是卡特兰数的方法有两种
1.从递推式的角度来看,如果f[n]=f[1]∗f[n−1]+f[2]∗f[n−2]f[n] = f[1] * f[n - 1] + f[2] * f[n - 2]f[n]=f[1]f[n1]+f[2]f[n2]… 那么就是卡特兰数,比如求二叉树的个数
2.挖掘一种性质:任意前缀中,某种东西一定不能少于等于另一种东西。

1112n2n2n把每个数依次分给每个奇数堆和偶数堆,分析可得,在任意时候奇数堆的数量都要>=偶数堆的数量,因此本题是标准的catlan数模型。另外由于ppp不一定是质数,所以不能用逆元的方法求组合数,要使用分解质因数结合快速幂的方法。
时间复杂度为O(n+n/ln(n)∗(logpn+loglogn))O(n+n/ln(n)*(log_p^n+loglogn))O(n+n/ln(n)(logpn+loglogn)),其中nnn是求素数的时间花费,n/ln(n)n/ln(n)n/ln(n)是分解质因数的过程中需要枚举的质数数量,logpnlog_p^nlogpn为求质数ppp数量的时间花费,loglognloglognloglogn是快速幂的时间花费,时间复杂度接近于O(n)O(n)O(n)

#include <iostream>
using namespace std;
typedef long long ll;
const int N = 2000010;
int primes[N], cnt;
bool st[N];
int n, p;
void init(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] * i <= n; j ++ )
        {
            st[primes[j] * i] = 1;
            if (i % primes[j] == 0) break;
        }
    }
}
int get(int n, int p)
{
    int s = 0;
    while (n) s += n / p, n /= p;
    return s;
}
int qmi(int a, int k)
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = res * a % p;
        k >>= 1;
        a = a * a % p;
    }
    return res;
}
int C(int a, int b)
{
    int res = 1;
    for (int i = 0; i < cnt; i ++ )
    {
        int prime = primes[i];
        int s = get(a, prime) - get(b, prime) - get(a - b, prime);
        res = (ll) res * qmi(prime, s) % p;
    }
    return res;
}
int main()
{
    cin >> n >> p;
    init(N - 1);
    cout << (C(n * 2, n) - C(n * 2, n - 1) + p) % p;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值