1. 程式人生 > 其它 >取模意義下除法的處理&&乘法逆元 理解與求法

取模意義下除法的處理&&乘法逆元 理解與求法

乘法逆元的意義**

取餘下,有些除號要變逆元(/b = *b^(-1)),有些除號可以消去 ( a /b *b =a) **

逆元記作 ..^(-1)之後直接當冪計算了。**

不確定的性質,嘗試能否力所能及地舉幾個反例

(a / b) % p = (a%p / b%p) %p (錯)可見取模下若有除法,結果可能不太對。

為了實現取模意義下的除法/計算取模意義下的除法算式;對於一些題目,我們必須在中間過程中進行求餘,否則數字太大,電腦存不下,那如果這個算式中出現除法,

取模下除號可看做分數。分數為整數還好,否則除號就得看作除法(取模是整數運算,沒有對小數的相關定義)。為整數時分數與除法的結果都一樣,若無逆元則只能看做分數**

參考文獻:

https://baijiahao.baidu.com/s?id=1609032096408414934&wfr=spider&for=pc

https://www.luogu.com.cn/blog/1239004072Angel/post-shuo-xue-sheng-fa-ni-yuan

逆元:

  逆元素(簡稱逆元)是指一個可以取消另一給定元素運算的元素,如加法中的加法逆元(相反數)和乘法中的倒數。簡單來說,A的逆元就是能抵消A的影響的元素。

數論中的乘法逆元:

  定義:理解也很好,a^(-1) *a =1,即逆元消除了乘a的影響。

  作用/意義:實現模意義下的大部分除法。(除法就是求另一個因數,這樣乘上逆元在消除除數就好了)

    結合除法的定義(https://baike.baidu.com/item/%E9%99%A4%E6%B3%95/6280598?fr=aladdin),模意義下的a/b,即為求a在模意義下除了b的另一個因數(設為c),即求b*c≡a (mod p)的c,這時給a乘上b的逆元就消去了乘b的影響,結果為c,即b*c *b^(-1)≡a *b^(-1) ≡c(mod p)。即,模意義下除以一個數就等價於乘這個數的乘法逆元。還說明,到了剩餘系中,四則運算的意義較平時會有些微的差別。

  

取模意義下的除法的實現:(常用方法與保底方法)

  取模意義下的除法的本質是求取模意義下的另一個因數。理解本質後就方便幹更多事了。

  但有時我們發現並非所有模意義下除法都要必須轉化為乘法逆元去做,而且有些除法還不能用乘法逆元。比如6/2 (modp):可以求2的逆元x,在算6*x=y(逆元演算法);但是6/2是可整除的,可直接得出6除了2的另一個因數(即為結果)為3(直接算)。可以證明兩種演算法的結果在[0,p)內是相同的。到了這時就有了一個想法,嘗試驗證當a能被b整除時,商模p後(設為y)也可作為模意義下a/b的答案:

    首先說結論:直接算和逆元演算法這兩種演算法做取模下的除法的結果都正確且在p的範圍內唯一。y也是滿足模意義下除法的定義的(顯然y*b≡a (modp)),設通過逆元的方法得出a/b≡c (modp),再對c模p(一般要求的答案都要在p的範圍內)。此時有式子:b*y≡b*c≡a (modp),即b*(y-c)≡ 0 (modp)。

      當b,p互質時,逆元c才存在(下文有講逆元的存在性),此時p|(y-c),若y,c都小於p大於0的話(可通過模p、小於0時加p解決,本質不變),可知y=c,計算除法的即兩演算法都可用,且結果一樣;

      接上文,當b,p不互質時,逆元c不存在,a/b只能用直接算。

  總之取模下的除法可以直接算(能整除)也可以用逆元算(存在逆元)。既不能整除也不存在逆元的話,還有適用性最廣的演算法:擴充套件歐幾里得。

  擴充套件歐幾里得求 除法:由前文除法的定義,求a/b (modp),即為求解方程bx≡a (modp)。除法有解的充要條件為gcd(b,p)|a (也是同餘方程、轉化的不定方程有解的條件)。由於複雜度為O logn,不如直接算(O 1)和逆元可快速(比 logn更快)計算時用逆元算,也就在上兩種處理除法的演算法條件都不滿足時才適用。

  

乘法逆元的性質:

  1、存在唯一性:對a 來說,若它有逆元,則在p範圍內一定只有一個逆元。

    簡單證明:

    

    a*k≡ 0 (modp)。a有逆元,則a,p互質,則p|k 。a'和a''都在p範圍內,則k=0,矛盾,故假設不成立。

  

  

  2、完全積性函式:(為了接下來方便,我們把 a 的逆元表示為 inv[a] )兩個數的逆元的積等於這兩個數積的逆元, inv[a]*inv[b]=inv[a*b] 。(逆元的積能抵消原數的積)

    簡單證明:(a*b)* (a^(-1) * b^(-1))=1,所以(a^(-1) * b^(-1))是積的逆元,同時它又是逆元的積。

  

  3、存在的充要條件:a,p互質

乘法逆元的求法:

  1、拓展歐幾里得求逆元:(解等價的不定方程)

      

      再解出這個不定方程的x就好。

    

    不定方程與同餘方程等價,也說明了同餘方程有解(有逆元)的充要條件與不定方程有解的一致。

    時間複雜度:logp

    程式碼:

    

#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

long long a, b;

int exgcd(long long a, long long b, long long &x, long long &y)
{
    if (b == 0)
    {
        x = 1, y = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return d;
}

int main()
{
    scanf("%lld%lld", &a, &b);
    long long x = 0, y = 0;
    exgcd(a, b, x, y);
    printf("%lld", (x % b + b) % b);//這裡防止出現負數
    return 0;
}

  2、尤拉定理求逆元:(求一個快速冪。指數減幾各有意義,記準指數就好)

      

      a^φ(p)≡ a * a^(φ(p)-1)≡ 1 (mod p),a^(φ(p)-1)即為a的逆元。

      Osqrt(n)列舉因數,再套公式:

      

      1- (1/pi)處理時變形為 /pi * (pi - 1) ,因為pi|n,不會出現有小數的情況。然後再做快速冪。

    時間複雜度log φ(p) + sqrt(n)(時間複雜度是忽略“-1”等 的常數)(求尤拉函式的複雜度(sqrt(n))在某些情況可以被優化,可見:尤拉函式和線性篩

    程式碼:

#include <cstdio>
#include <iostream>
#include <algorithm>

using namespace std;

long long n, p;

long long phi(long long x)//求x的尤拉函式
{
    long long res = x, tmp = x;//初始化答案為x
    for (int i = 2; i * i <= tmp; ++i)
    {
        if (x % i == 0)
        {
            res = (res / i) % p * ((i - 1) % p) % p;//找到x的一個質因子,計算其對答案的貢獻
            while (x % i == 0) x /= i;//統計完答案後,除去該質因子
        }
    }
    if (x > 1) res = (res / x) % p * ((x - 1) % p) % p;//防止漏篩質因子
    return res;
}

long long power(long long a, long long b)//快速冪
{
    long long res = 1;
    while (b)
    {
        if (b & 1) res = res * a % p;
        a = a * a % p;
        b >>= 1;
    }
    return res;
}

int main()
{
    scanf("%lld%lld", &n, &p);
    long long tmp = phi(p) - 1;
    printf("%lld", power(n, tmp));
    return 0;
}

  3、費馬小定理求逆元:(尤拉定理的特殊情況,不用求尤拉函數了。記準指數)

    

    a^(p-1)≡ a * a^(p-2)≡ 1 (mod p) 。a^(p-2)即為逆元。做一個快速冪就好。

    時間複雜度logp

    由於大多數題目的模數都為質數,所以很泛用。

    核心程式碼:

inline int power(int a, int b)//快速冪
{
    int res = 1;
    while (b)
    {
        if (b & 1) res = 1ll * res * a % p;
        a = 1ll * a * a % p;
        b >>= 1;
    }
    return res;
}

int main()
{
    int a, b;
    cin>>a>>b;
        printf("%d", (int)(1ll * a * power(b, p - 2) % p));
    return 0;
}

以上程式碼都沒有逆元存在性的判定,根據具體題目情況,判定該加就加。

  4、線性求逆:(讓i的逆元出現在公式,按照公式遞推)

    要求p為質數。

      

      (邊界條件)

      設當前要求i的逆元。對p進行帶餘除法,p=ki+r(r<i,1<i<p),將其轉化為同餘式會得到:

      (希望讓i^(-1)單獨出現在一邊,要消係數。p%p=0,也可通過這個式子來探索性質。)

      

      

      

    時間複雜度:On 這裡n實際是值域,數值範圍很大時就不好用了。

    程式碼:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int maxn = 3e6 + 10; 
long long inv[maxn], n, p;

int main()
{
    scanf("%lld%lld", &n, &p);
    inv[0] = 0, inv[1] = 1;//初始化1的逆元為1
    printf("%lld\n", inv[1]);
    for (int i = 2; i <= n; ++i)
    {
        inv[i] = (p - p / i) * inv[p % i] % p;//上文中的式子
        printf("%lld\n", inv[i]);
    }
    return 0;
}

  5、離線線性求逆:(算出整體逆元后到著一步步算出個體)

    線性時間求n個給定較大的整數a1,a2,...,an模p意義下的逆元。一般保證全部有逆元,或說 p是質數且a中無p的倍數。

    由於值域很大,不能用普通的線性求逆。

    嘗試字首積。由逆元的完全積性,字首積的逆元等於逆元的字首積

      設pre[i]為字首積,即為 a1*a2*...*ai;inv_pre[i]為pre[i]的逆元,即為a1^(-1) *a2^(-1)*...*ai^(-1) ;inv[i]為ai的逆元

      可以發現幾個關係:inv[i]=inv_pre[i]*pre[i-1]  inv_pre[i-1]=inv_pre[i]*ai。

      這樣,算出所有字首積後,可以先用某個log演算法算一個inv_pre[n],然後逐漸算出inv[n],inv_pre[n-1],inv[n-1],....就得到答案了。

    

    瞭解演算法後應該能看出一個問題。字首積中一旦有一個a沒有逆元,即不與p互質,演算法就無法算逆元了。所以用這種演算法的話,p一般都是質數且a中無p的倍數,否則要將沒有逆元的a挑出,但這樣就難用線性演算法辦了。

    巧妙利用了逆元對原數的消去作用與完全積性,算出整體後再逐一消去算出個體。

    時間複雜度O (n +logp)

    核心程式碼:

    pre[0] = 1;//初始化
    for (register int i = 1; i <= n; ++i)
    {
        read(a[i]);
        pre[i] = 1ll * pre[i - 1] * a[i] % p;//計算字首積
    }
    inv_pre[n] = power(pre[n], p - 2, p);//求出字首積的逆元
    for (register int i = n; i > 0; --i)//遞推求逆元
    {
        inv[i] = 1ll * pre[i - 1] * inv_pre[i] % p;
        inv_pre[i - 1] = 1ll * inv_pre[i] * a[i] % p;
    }