标签: 数学

[SDOI2015]序列统计 题解

[SDOI2015]序列统计 题解

题目地址:洛谷:【P3321】[SDOI2015]序列统计 – 洛谷、BZOJ:Problem 3992. — [SDOI2015]序列统计

题目描述

小C有一个集合S,里面的元素都是小于M的非负整数。他用程序编写了一个数列生成器,可以生成一个长度为N的数列,数列中的每个数都属于集合S。小C用这个生成器生成了许多这样的数列。但是小C有一个问题需要你的帮助:给定整数x,求所有可以生成出的,且满足数列中所有数的乘积mod M的值等于x的不同的数列的有多少个。小C认为,两个数列{Ai}和{Bi}不同,当且仅当至少存在一个整数i,满足Ai≠Bi。另外,小C认为这个问题的答案可能很大,因此他只需要你帮助他求出答案mod 1004535809的值就可以了。

输入输出格式

输入格式:
一行,四个整数,N、M、x、|S|,其中|S|为集合S中元素个数。第二行,|S|个整数,表示集合S中的所有元素。

输出格式:
一行,一个整数,表示你求出的种类数mod 1004535809的值。

输入输出样例

输入样例#1:

4 3 1 2
1 2

输出样例#1:

8

题解

参考资料:bzoj 3992: [SDOI2015]序列统计 (NTT+快速幂+DP) – CSDN博客
本题需要的数学姿势:数学笔记:数论(欧拉函数、阶、原根) | KSkun’s Blog
首先,计数自然是一个动态规划,我们考虑设计状态dp[i][j]为选了i个数且结果mod M的值是j,转移如下
dp[i][j] = \sum_{k \in S} dp[i-1][j'] \ (j'k \bmod M = j)
直观上感觉直接做是一个O(n|S|)的,好像只能得10分。
接下来我们要考虑一个问题,模数很特殊,并不是常见的1000000007,而是1004535809,它的原根为3,是常用的NTT模数,难道出题人在指引我们使用NTT?
想用NTT做,首先得搞出卷积,卷积下标是加减的事情没办法像上面那样乘吧。那么考虑利用原根把乘法转换为加法,dp[i][j]表示选到第i个数结果mod M的值为g^j,改变以后的转移方程就成这样了
dp[i][j] = \sum_{g^k \bmod M \in S} dp[i-1][j'] \ ((j' + k) \bmod (M - 1) = j)
或者这样?用f[i]表示g^i是否在集合中。
dp[i][j] = \sum_{k=1}^{M-1} dp[i-1][j-k] \cdot f[k]
咦,后面那部分现在可以卷积了,而且你会发现每一层的转移中f数组都是不变的,也就是说这是个线性变换,参照矩阵快速幂的操作,我们可以做多项式的快速幂,即dp[n] = dp[0] \times f^n。答案便是 dp[n][i] \ (g^i=x)
于是我们的思路就很明确了:求原根、算出f数组、多项式快速幂。
没完呢!这里有一个坑点,集合内的元素可以有0,0显然对答案没贡献,所以你要跳过它处理。

代码

// Code by KSkun, 2018/4
#include <cstdio>
#include <cmath>
#include <cstring>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

inline LL fpow(LL n, LL k, LL p) {
    n %= p;
    LL res = 1;
    while(k) {
        if(k & 1) res = res * n % p;
        n = n * n % p;
        k >>= 1;
    }
    return res;
}


const int MAXN = 1 << 15, G = 3, MO = 1004535809;

int n, m, x, s, t;
LL f[MAXN], g[MAXN];
bool vis[MAXN];

LL rev[MAXN], dft1[MAXN], dft2[MAXN], dftr[MAXN];

inline void calrev(int n, int len) {
    for(int i = 0; i < n; i++) {
        rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (len - 1));
    }
}

inline void dft(int n, LL *arr, int f) {
    for(int i = 0; i < n; i++) {
        if(i < rev[i]) std::swap(arr[i], arr[rev[i]]);
    }
    for(int i = 1; i < n; i <<= 1) {
        LL gn = fpow(G, (MO - 1) / (i << 1), MO);
        if(f == -1) gn = fpow(gn, MO - 2, MO);
        for(int j = 0; j < n; j += i << 1) {
            LL g = 1;
            for(int k = 0; k < i; k++) {
                LL x = arr[j + k], y = g * arr[j + k + i] % MO;
                arr[j + k] = (x + y) % MO;
                arr[j + k + i] = ((x - y) % MO + MO) % MO;
                g = g * gn % MO;
            }
        }
    }
    if(f == -1) {
        LL invn = fpow(n, MO - 2, MO);
        for(int i = 0; i < n; i++) {
            arr[i] = arr[i] * invn % MO;
        }
    }
}

inline void ntt(LL *a, LL *b) {
    memset(dft1, 0, sizeof(dft1));
    memset(dft2, 0, sizeof(dft2));
    memset(dftr, 0, sizeof(dftr));
    int n, len = 0;
    for(n = 1; n <= (m - 1) << 1; n <<= 1) len++;
    for(int i = 0; i < n; i++) {
        dft1[i] = a[i];
        dft2[i] = b[i];
    }
    calrev(n, len);
    dft(n, dft1, 1); dft(n, dft2, 1);
    for(int i = 0; i < n; i++) {
        dftr[i] = dft1[i] * dft2[i] % MO;
    }
    dft(n, dftr, -1);
    for(int i = 0; i < m - 1; i++) {
        a[i] = (dftr[i] + dftr[i + m - 1]) % MO;
    }
}

inline LL calg(LL n) {
    int fact[8005], tot = 0, x = n - 1;
    for(int i = 2; i * i <= x; i++) {
        if(x % i == 0) {
            fact[tot++] = (n - 1) / i;
            while(x % i == 0) x /= i;
        }
    }
    if(x > 1) fact[tot++] = (n - 1) / x;
    bool success;
    for(int i = 2;; i++) {
        success = true;
        for(int j = 0; j < tot; j++) {
            if(fpow(i, fact[j], n) == 1) {
                success = false;
                break;
            }
        }
        if(success) return i;
    }
}

int main() {
    n = readint(); m = readint(); x = readint(); s = readint();
    for(int i = 1; i <= s; i++) {
        t = readint();
        vis[t] = 1;
    }
    int xpow, G = calg(m), gn = 1;
    for(int i = 0; i < m - 1; i++) {
        if(vis[gn]) f[i] = 1;
        if(gn == x) xpow = i;
        gn = gn * G % m;
    }
    g[0] = 1;
    for(int i = n; i; i >>= 1) {
        if(i & 1) ntt(g, f);
        ntt(f, f);
    }
    printf("%lld", g[xpow]);
    return 0;
}
Lucas定理及其扩展原理及应用

Lucas定理及其扩展原理及应用

Lucas定理(Lucas’s theorem)

内容

对于非负整数m和n和素数p,有
\mathrm{C}_n^m \equiv \prod_{i=0}^k \mathrm{C}_{n_i}^{m_i} \pmod{p}
其中,m = m_kp^k + m_{k-1}p^{k-1} + \cdots + m_1p + m_0n = n_kp^k + n_{k-1}p^{k-1} + \cdots + n_1p + n_0
也就是说,这个是m和n的p进制展开。
m < n时,\mathrm{C}_n^m = 0

求组合数的值

既然我们可以把m和n以p进制展开求值,那么我们可以设计递归来解决这个问题。
我们知道\mathrm{C}_n^0 = 1,那么以m=0为递归终点即可。代码如下。

inline LL lucas(LL n, LL m, LL p) {
    if(!m) return 1;
    return C(n % p, m % p, p) * lucas(n / p, m / p, p) % p;
}

其中C就是直接求组合数值的函数,这个可以预处理杨辉三角或者预处理阶乘及阶乘逆元。

例题:【P3807】【模板】卢卡斯定理 – 洛谷

直接应用上述方法即可。代码如下。

// Code by KSkun, 2018/3
#include <cstdio>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 100005;

LL T, n, m, p, inv[MAXN], mul[MAXN];

inline LL C(LL n, LL m, LL p) {
    if(m > n) return 0;
    return mul[n] * inv[n - m] % p * inv[m] % p;
}

inline LL lucas(LL n, LL m, LL p) {
    if(!m) return 1;
    return C(n % p, m % p, p) * lucas(n / p, m / p, p) % p;
}

inline void init() {
    mul[0] = inv[0] = mul[1] = inv[1] = 1;
    for(int i = 2; i <= std::min(n, p); i++) {
        inv[i] = -(p / i) * inv[p % i] % p;
    }
    for(int i = 2; i <= std::min(n, p); i++) {
        inv[i] = inv[i] * inv[i - 1] % p;
        mul[i] = mul[i - 1] * i % p;
    }
}

int main() {
    T = readint();
    while(T--) {
        n = readint(); m = readint(); p = readint(); n += m;
        init();
        printf("%lld\n", (lucas(n, m, p) + p) % p);
    }
    return 0;
}

扩展Lucas定理(exLucas)

内容

扩展Lucas定理与Lucas定理的区别是模数非质数,此时,我们考虑将模数质因数分解如下。
P = p_1^{k_1} \cdot p_2^{k_2} \cdots p_n^{k_n}
只要我们能求出组合数对于每个p_i^{k_i}的余数,我们就可以利用中国剩余定理(中国剩余定理及其扩展原理及应用 | KSkun’s Blog)解决这个问题。也就是说,我们将问题变为了这个:
\begin{cases} \mathrm{C}_n^m \equiv a_1 \pmod{p_1^{k_1}} \\ \mathrm{C}_n^m \equiv a_2 \pmod{p_2^{k_2}} \\ \vdots \\ \mathrm{C}_n^m \equiv a_n \pmod{p_n^{k_n}} \end{cases}
现在的问题是,上面每个式子的余数怎么求出。
如果我们使用组合数的定义,我们可以分别求出n! \bmod p_i^{k_i}, m! \bmod p_i^{k_i}, (n-m)! \bmod p_i^{k_i}
首先,我们知道阶乘中存在一些p_i因子,对于这些因子我们可以处理出来单独运算。对于提取一个因子后的阶乘,我们观察它们的规律。假如p=3,要求10!,提取后就变成了:
1 \times 2 \times 1 \times 4 \times 5 \times 2 \times 7 \times 8 \times 3 \times 10 \times 3^3
p_i的倍数组成了一个新的阶乘,这一部分我们递归下去处理。考虑剩下的部分,即1 \times 2 \times \cdots \times (p_i^{k_i}-1) \times (p_i^{k_i}+1) \times \cdots \times (2p_i^{k_i}-1) \times \cdots。我们发现\prod_{i=1, i \bmod p_i \neq 0}^{p_i^{k_i}-1} i \equiv \prod_{i=p_i^{k_i}+1, i \bmod p_i \neq 0}^{2p_i^{k_i}-1} i \pmod{p_i^{k_i}},也就是说,只需要求一个长为p_i^{k_i}的阶乘,并且计算有多少完整的一段,使用快速幂即可求出。至于多出去的那段,直接暴力乘进去即可。

例题:[国家集训队]礼物 题解 | KSkun’s Blog

代码参见该题题解文章。代码内有注释,可以对照上述内容理解。

参考资料

数学笔记:逆元

数学笔记:逆元

逆元

一整数a对同余n之模逆元是满足以下公式的整数b
a^{-1} \equiv b \pmod{n}
也可以写成
ab \equiv 1 \pmod{n}

求逆元的方法

扩展欧几里得算法

费马小定理

费马小定理可以转换成如下形式
若a是一个整数,p是一个质数,则
a^{p-2} \equiv a^{-1} \pmod{p}

线性递推

我们设p=ki+r, r < i, 1 < i < p,然后把左边的式子展开
ki + r \equiv 0 \pmod{p}
两边同乘 (ir)^{-1}
\begin{aligned} kr^{-1} + i^{-1} &\equiv 0 &\pmod{p} \\ i^{-1} &\equiv -kr^{-1} &\pmod{p} \\ i^{-1} &\equiv -\lfloor \frac{p}{i} \rfloor \cdot (p \bmod i)^{-1} &\pmod{p} \end{aligned}
于是我们就得到了逆元的线性递推式:

inv[i] = -(MO / i) * inv[MO % i];

阶乘的逆元

先处理 (n!)^{-1} ,然后 [(n-1)!]^{-1} \equiv n(n!)^{-1} \pmod{p}

参考资料