逆元Inv(模板+应用)

博客介绍了逆元的概念,当满足特定公式时,a 与 b 互为逆元。在求解 (a / b) % m 可能爆精度时,可利用逆元将除法转换为乘法。还详细阐述了扩展欧几里得、费马小定理、递推、递归等多种求逆元的方法及适用场景。

逆元:

如果满足公式a*b\equiv 1(mod m),则有a 是 b的逆元同时b也是a的逆元。

逆元的应用:

设c为b在对m取余的意义下的逆元;

在求解公式 (a / b) % m的时候,如果b可能会非常的大,所以会出现爆精度的问题,这个时候就需要将除法转换成乘法来做,即:

(a / b )  % m = (a * c)%m。

逆元的求法:

一、扩展欧几里得求逆元

复杂度:O(logn)(实际就是斐波那契数列)

将公式(b、p已知)   a∗b≡1(mod p)   转换为   a∗b+k∗p=1  则有a为b对p取余意义下的逆元,且只有当a与p互质是逆元才存在

注意:只要存在逆元就可以求,适用于逆元个数不多,但是mod很大的时候。

附一个百度百科的例子加深一下理解:

代码:

/*
    Time:2018/8/31
    Writer:Sykai
    Function:利用扩展欧几里得求逆元
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <set>
#include <queue>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn = 1e6 + 100;
const int MOD = 1e9 + 7;
typedef long long ll;
typedef pair<int,int> P;

//公式a∗b+k∗p=1中a即是a,p即是b
ll exgcd(ll a,ll b,ll& x,ll& y)
{
    if(b == 0)
    {
        x = 1;
        y = 0;
        return a;
    }
    ll res = exgcd(b, a%b, y, x);
    y -= a/b*x;
    return res;
}
//公式a∗b+k∗p=1中a即是a,p即是mod
ll getInv(int a,int mod)//求a在mod下的逆元。如果不存在就返回-1
{
    ll x,y;
    ll res = exgcd(a,mod,x,y);
    return res = 1 ? (x%mod + mod)%mod:-1;//return res = 1?(x + mod)%mod : -1;
}

int main()
{
    ll a = 5;
    printf("%lld\n",getInv(a,MOD));
    return 0;
}

二、费马小定理求逆元

复杂度:O(logn)

费马小定理: 假如p是质数,且gcd(a,p)=1,那么 a^(p-1)≡1(mod p)。

a*a^(p-2) ≡ 1 (mod p),则有a^(p-2)是a的对p取余时候的逆元

注意:当p是素数的时候一般选用费马小定理来求逆元。

代码:

/*
    Time:2018/8/31
    Writer:Sykai
    Function:利用费马小定理求逆元
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <set>
#include <queue>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn = 1e6 + 100;
const int MOD = 1e9 + 7;
typedef long long ll;
typedef pair<int,int> P;

ll qpow(ll a,ll b)//求a*a^(p-2) ≡ 1 (mod p)中a^(p-2)
{
    ll res = 1;
    while(b)
    {
        if(b&1)
            res = res * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return res;
}

ll getInv(ll a,ll mod)
{
    return qpow(a,mod-2);
}

int main()
{
    int a = 5;
    printf("%lld\n",getInv(a,MOD));
    return 0;
}

三、递推求逆元

复杂度:O(n)

注意:

1、mod需要是质数,求得是1~N关于mod的逆元。

2、适用于mod不是太大,且被多次调用。

3、程序开始前需要预处理打表。

代码:

/*
    Time:2018/8/31
    Writer:Sykai
    Function:线性求逆元
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <set>
#include <queue>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn = 1e6 + 100;
const int MOD = 1e9 + 7;
typedef long long ll;
typedef pair<int,int> P;
ll inv[maxn];//数组的大小需要根据实际情况来调整

void getInv()
{
    inv[1] = 1;
    for(int i = 2; i < maxn; i++)
        inv[i] = (MOD-MOD/i)*inv[MOD%i]%MOD;
}

int main()
{
    getInv();
    printf("%lld\n",inv[5]);
    return 0;
}

四、递归求逆元

复杂度:O(logn)

注意:mod需要是素数(中国剩余定理中不太好用)

/*
    Time:2018/8/31
    Writer:Sykai
    Function:递归求逆元
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <set>
#include <queue>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn = 1e6 + 100;
const int MOD = 1e9 + 7;
typedef long long ll;
typedef pair<int,int> P;
ll inv[maxn];//数组的大小需要根据实际情况来调整

ll getInv(ll x)
{
    if(x == 1) return 1;
    return (MOD-MOD/x)*getInv(MOD%x)%MOD;
}

int main()
{
    printf("%lld\n",getInv(5));
    return 0;
}

五、求阶乘的逆元

代码:(不确定怎么用)

/*
    Time:2018/8/31
    Writer:Sykai
    Function:递归求逆元
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <set>
#include <queue>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn = 1e6 + 100;
const int MOD = 1e9 + 7;
typedef long long ll;
typedef pair<int,int> P;
ll inv[maxn];//数组的大小需要根据实际情况来调整
ll fac[maxn+1];//阶乘数组
ll qpow(ll a,ll b)
{
    ll res = 1;
    while(b)
    {
        if(b&1)
            res = res * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return res;
}


int main()
{
    inv[maxn] = qpow(fac[maxn],MOD-2);
    for(ll i = maxn-1; i>=0; i--)
    {
        inv[i] = (inv[i+1]*(i+1))%MOD;
    }
    return 0;
}

参考博客:

https://blog.youkuaiyun.com/baidu_35643793/article/details/75268911

https://blog.youkuaiyun.com/xiaoming_p/article/details/79644386

<think>好的,我现在需要解决用户的问题:如何用线性逆元高效计算组合数,并给出算法实现。首先,我需要回顾一下用户提供的引用内容,看看有哪些已有的信息可以利用。 根据引用[2]和引用[4],用户提到了两种求逆元的方法:费马小定理和线性算法。用户特别关注的是线性逆元应用,所以重点应该放在线性逆元方法上。引用[4]中的代码使用了费马小定理来计算阶乘的逆元,而用户现在想用线性逆元的方法,也就是引用[2]中的第二种方法。 首先,我需要理清组合数的计算公式。组合数C(n, m)的公式是n!/(m!*(n-m)!))。在模运算中,除法需要转换为乘以逆元。因此,组合数取模可以表示为:C(n,m) % p = (n! * inv(m!) * inv((n-m)!)) % p,其中inv表示逆元。 接下来,线性逆元的计算方法。引用[2]中的线性算法部分给出了一个预处理逆元数组的方法,预处理每个数的逆元。代码示例中的ny数组是通过递推公式计算的:ny[i] = (p - p/i) * ny[p%i] % p。这里的ny[i]是i的逆元。这种方法可以在O(n)的时间内预处理出1到n的逆元。 但这里有个问题,组合数计算中需要的是阶乘的逆元,而不是单个数的逆元。所以,可能需要分两步处理:首先预处理出每个数的逆元,然后计算阶乘的逆元。例如,先预处理inv[1...n],然后阶乘的逆元可以通过递推得到,比如infact[i] = infact[i-1] * inv[i] % p。这样,阶乘的逆元可以通过线性时间计算出来。 再看引用[3],其中提到了一种同时计算阶乘和逆元的方法,使用了一个递推式。可能这种方法可以更高效地同时处理阶乘及其逆元。但用户现在希望用线性逆元的方法,也就是先预处理每个数的逆元,再计算阶乘的逆元。 所以,步骤大致如下: 1. 预处理逆元数组inv,其中inv[i]是i在模p下的逆元,使用线性递推方法。 2. 计算阶乘数组fact,其中fact[i] = i! % p。 3. 计算阶乘的逆元数组infact,其中infact[i] = (i!)^{-1} % p。这可以通过infact[i] = infact[i-1] * inv[i] % p来计算。 4. 组合数C(n,m)的计算公式即为 fact[n] * infact[m] % p * infact[n-m] % p。 现在需要验证这个过程是否正确。例如,假设已经计算了fact数组和infact数组,那么组合数的计算是否正确。是的,因为每个阶乘的逆元已经预先计算好了,相乘即可得到分母部分的逆元。 接下来要考虑的是实现中的细节,比如数组的大小,预处理的范围。如果n和m的最大值较大,比如1e5或者更高,需要确保数组足够大。例如,在引用[4]的代码中,预处理了N=1e5+10的阶乘和阶乘逆元。用户的问题中可能需要类似的预处理。 那么具体的实现步骤: 预处理部分: - 初始化fact[0...maxN]和infact[0...maxN],以及inv[1...maxN]。 - fact[0] = 1,然后fact[i] = fact[i-1] * i % p。 - 预处理inv数组,使用线性递推:inv[1] = 1,然后对于i>=2,inv[i] = (p - p/i) * inv[p % i] % p。 - 计算infact数组:infact[0] = 1,然后infact[i] = infact[i-1] * inv[i] % p。 这样,当需要计算C(n,m)时,只要n和m不超过maxN,就可以直接通过fact[n] * infact[m] % p * infact[n-m] % p得到结果。 需要注意的是,这种方法的时间复杂度是O(maxN),对于多次查询组合数的情况非常高效,因为预处理后每次查询都是O(1)时间。 现在需要检查是否存在可能的错误。例如,当p不是质数时,线性逆元的方法是否有效?根据线性递推的条件,p必须是质数才能确保每个数都有逆元吗?其实,线性逆元方法要求i和p互质,所以当p是质数时,1到p-1的数都与p互质,此时可以正确计算逆元。如果p不是质数,可能某些i没有逆元,导致预处理失败。因此,该方法适用于p为质数的情况。 用户提供的引用中,例如引用[2]中的费马小定理部分提到p为素数且a和p互质。所以,这里可能需要假设p是一个足够大的质数,这样预处理才能正确进行。 因此,在实现中,需要确保p是质数,并且在预处理时处理到足够大的maxN,以满足后续查询的n和m的范围。 现在,编写代码的大致结构: 首先预处理inv数组,然后fact和infact数组。例如: const int maxN = 1e5 + 10; int fact[maxN], infact[maxN], inv[maxN]; int p; // 模数,假设为质数 void preprocess() { fact[0] = infact[0] = 1; inv[1] = 1; for (int i = 2; i < maxN; ++i) inv[i] = (p - p / i) * inv[p % i] % p; for (int i = 1; i < maxN; ++i) { fact[i] = fact[i-1] * i % p; infact[i] = infact[i-1] * inv[i] % p; } } 然后,计算组合数时: int C(int n, int m) { if (m < 0 || m > n) return 0; return fact[n] * infact[m] % p * infact[n - m] % p; } 但需要确保n和m都不超过maxN-1,否则数组会越界。如果n可能很大,比如达到1e5,则需要预先设置maxN足够大。或者,如果n的范围不确定,可能需要动态处理,但预处理通常适用于已知最大范围的情况。 测试一下是否正确。例如,当p=1e9+7,计算C(5,2)=10。按照上述步骤: fact[5] = 120,infact[2] = 2的阶乘逆元逆元。假设inv[2]=500000004(因为2*500000004 mod 1e9+7=1),infact[2] = infact[1] * inv[2] → infact[1]是1的逆元的乘积。可能过程需要更仔细的计算。 或者,可能直接通过阶乘的逆元来计算。例如,fact[5]=120,infact[2]= (2!)^{-1} = (2)^{-1} * (1)^{-1} → 但按照infact的计算方法,infact[i] = infact[i-1] * inv[i],所以infact[2] = infact[1] * inv[2}。而infact[1} = inv[1}=1。所以infact[2}=1 * inv[2}。inv[2}是500000004。因此,infact[2}=500000004。同理,infact[3}=infact[2} * inv[3},以此类推。 所以,当计算C(5,2)时,fact[5] * infact[2} * infact[3}。即120 * 500000004 * 333333336 % 1e9+7。计算这个值: 500000004 * 333333336 = (1/2) * (1/3) = 1/6,所以乘以120得到20。但正确的结果是10。这说明哪里出错了? 哦,这里可能哪里弄错了。例如,C(5,2)=10。但根据上述计算,结果得到的是20? 可能我的计算有误。让我们详细计算: 假设p=1e9+7,此时: fact[5] = 120 infact[2}= (2!)^{-1} = inv[1] * inv[2} = 1 * inv[2} = 500000004 infact[3}= (3!)^{-1} = infact[2} * inv[3} → inv[3}=333333336,所以 infact[3}=500000004 * 333333336 mod p = 500000004 * 333333336 = (1/2) * (1/3) = 1/6 → 因此,infact[3}=166666668. 所以,组合数C(5,2) = 120 * 500000004 * 166666668 % p. 计算这三个数的乘积: 500000004 * 166666668 = ? 先计算500000004 × 166666668: 500000004 * 166666668 = (5e8 +4) * (1.66666668e8) 这可能需要模运算中的计算,但直接计算的话: 500000004 × 166666668 = ? 但是模1e9+7的话,这些数都是它们在该模下的逆元。例如,500000004是2的逆元,166666668是6的逆元。所以,2的逆元乘以6的逆元等于 (2*6)逆元=12的逆元。但 120 × (2的逆元) × (6的逆元) = 120 × (12的逆元) = 120 /12 =10。所以结果正确。因此,上述方法是正确的。 这说明算法是正确的。 接下来,需要根据这个思路编写代码。例如,引用的代码中,用户提供的引用[4]使用了费马小定理来预处理阶乘的逆元,而用户现在希望用线性逆元的方法,所以需要修改这部分代码,将infact的预处理改为使用线性逆元数组。 因此,正确的代码结构应该是: 预处理inv数组,然后预处理fact和infact数组。例如: #include <bits/stdc++.h> using namespace std; const int N = 1e5 + 10; // 假设最大处理到1e5 int fact[N], infact[N], inv[N]; int p; void preprocess() { fact[0] = infact[0] = 1; inv[1] = 1; for (int i = 2; i < N; ++i) { inv[i] = (p - p / i) * 1LL * inv[p % i] % p; } for (int i = 1; i < N; ++i) { fact[i] = fact[i - 1] * 1LL * i % p; infact[i] = infact[i - 1] * 1LL * inv[i] % p; } } int C(int n, int m) { if (m < 0 || m > n) return 0; return fact[n] * 1LL * infact[m] % p * infact[n - m] % p; } int main() { // 示例:假设p是质数,比如1e9+7 p = 1e9 + 7; preprocess(); int n, m; cin >> n >> m; cout << C(n, m) << endl; return 0; } 这样,预处理部分使用线性逆元的方法来计算inv数组,然后用inv数组来递推计算infact数组。这样就避免了使用费马小定理的快速幂方法,从而提高了预处理的速度,尤其是在需要处理大范围的情况下,线性方法更高效。 需要注意的是,这种方法的时间复杂度为O(N),适用于多次查询的情况。同时,需要确保N足够大以覆盖所有可能的n和m值。 用户的问题已经得到了解决,现在需要生成相关问题。根据用户的问题和回答,相关问题可能包括线性逆元的原理、组合数计算的其他方法、或者如何处理更大的n和m的情况等。</think>### 线性逆元求组合数算法实现 #### 核心步骤 1. **预处理线性逆元数组**:使用递推公式$inv[i] = (p - \lfloor p/i \rfloor) \cdot inv[p\%i] \% p$,时间复杂度$O(n)$ 2. **计算阶乘数组**:$fact[i] = fact[i-1] \times i \% p$ 3. **计算阶乘逆元数组**:$infact[i] = infact[i-1] \times inv[i] \% p$ 4. **组合数计算**:$C(n,m) = fact[n] \times infact[m] \times infact[n-m] \% p$ #### 算法模板 ```cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 1e5 + 10; int fact[MAXN], infact[MAXN], inv[MAXN], mod; void preprocess() { // 初始化边界条件 fact[0] = infact[0] = 1; inv[1] = 1; // 线性求逆元 for (int i = 2; i < MAXN; ++i) inv[i] = 1LL * (mod - mod / i) * inv[mod % i] % mod; // 计算阶乘及其逆元 for (int i = 1; i < MAXN; ++i) { fact[i] = 1LL * fact[i-1] * i % mod; infact[i] = 1LL * infact[i-1] * inv[i] % mod; } } int comb(int n, int m) { if (m < 0 || m > n) return 0; return 1LL * fact[n] * infact[m] % mod * infact[n - m] % mod; } ``` #### 复杂度分析 - **时间复杂度**:预处理$O(n)$,单次查询$O(1)$ - **空间复杂度**:$O(n)$ - **适用条件**:模数$p$为质数且$n,m < MAXN$[^2][^4] #### 应用示例 当$p=10^9+7$时,计算$C(10000,5000)$: 1. 预处理阶乘数组和逆元数组 2. 直接调用comb(10000, 5000)即可得到结果 3. 计算结果满足:$C(n,m) \equiv \frac{n!}{m!(n-m)!} \mod p$[^1][^3]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值