算法竞赛数学知识大全

质数

质数定义:一个大于1的自然数,除了1和它本身外,不能被其他自然数整除,换句话说就是该数除了1和它本身以外不再有其他的因数,这个数就是质数。

试除法判断质数

时间复杂度O(n)O(\sqrt n)O(n )

一个数 xxx 分解成两个数的乘积,则这两个数中,一定有一个数大于x\sqrt xx ,一个数小于根号x\sqrt xx 。

所以,可以用 xxx 除以 2~x2 ~\sqrt x2~x 中的每个数,如果出现了余数为 0,则这个数不是质数,如果没有出现余数为 0,则这个数是质数。

cpp 复制代码
bool is_prime(int x)
{
    if (x < 2) return false;
    int k = sqrt(x);
    for (int i = 2; i <= k; i ++ )
    {
        if (x % i == 0) return false;
    }
    return true;
}

分解质因数

时间复杂度O(n)O(\sqrt n)O(n )

把一个数nnn分解质因数之后,大于n\sqrt nn 的质因数最多有一个,也正因此我们可以只考虑≤n\leq \sqrt n≤n 的数,最后再做特判。

cpp 复制代码
void divide(int n)
{
    int x = sqrt(n);
    for (int i = 2; i <= x; i ++ )
    {
        int s = 0;
        while (n % i == 0)
        {
            n /= i;
            s += 1;
        }
        if (s) printf("%d %d\n", i, s);
    }
    if (n > 1) printf("%d %d\n", n, 1);
    puts("");
    return ;
}

筛质数

普通的筛法

时间复杂度O(nlogn)O(n logn)O(nlogn)

因为我们在2~n2~ n2~n内求每一个数的倍数数量,即n/2+n/3+...+n/nn/2+n/3+...+n/nn/2+n/3+...+n/n,是一个调和级数,和为nlognnlognnlogn。

cpp 复制代码
void get_primes(){
    for(int i=2;i<=n;i++){

        if(!st[i]) primes[cnt++]=i;//把素数存起来
        for(int j=i;j<=n;j+=i){//不管是合数还是质数,都用来筛掉后面它的倍数
            st[j]=true;
        }
    }
}

埃氏筛法

时间复杂度O(nloglogn)O(n log logn)O(nloglogn)

因为一个合数的倍数一定为一些质数的倍数,所以在这里我们只考虑了去枚举质数的倍数,避免了一定的重复。

cpp 复制代码
void get_primes(){
    for(int i=2;i<=n;i++){
        if(!st[i]){
            primes[cnt++]=i;
            for(int j=i;j<=n;j+=i) st[j]=true;//可以用质数就把所有的合数都筛掉;
        }
    }
}

★线性筛法

时间复杂度为O(n)O(n)O(n)

这种筛法避免了重复去筛掉一个合数。

我们从小到大枚举所有质因子primes[j]primes[j]primes[j]。

1、当出现 i%primes[j]==0i \% primes[j] == 0i%primes[j]==0 时,primes[j]primes[j]primes[j] 一定是iii的最小质因子,因此也一定是 primes[j]∗iprimes[j] * iprimes[j]∗i 的最小质因子。

2、当出现 i%primes[j]!=0i \% primes[j] != 0i%primes[j]!=0 时,说明我们还尚未枚举到iii的任何一个质因子,也就表示primes[j]primes[j]primes[j]小于iii的任何一个质因子,这时 primes[j]primes[j]primes[j] 就一定是 primes[j]∗iprimes[j] * iprimes[j]∗i 的最小质因子。

可以发现无论如何,primes[j]primes[j]primes[j] 都一定是 primes[j]∗iprimes[j] * iprimes[j]∗i 的最小质因子,这样就保证了每一个合数只会被它的最小质因子筛掉一次,避免了重复。

cpp 复制代码
void get_primes()
{
    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;
        }
    }
    return ;
}

约数

如果一个数aaa除以另一个数bbb的余数为0,即 a%ba\%ba%b == 0, 则bbb是aaa的约数。

试除法判断约数

时间复杂度O(n)O(\sqrt n)O(n )

一个数的约数是成对出现的,并且一个在n\sqrt nn 左边,一个在右边,所以只需要枚举到n\sqrt nn 即可。

cpp 复制代码
void get_divisors(int x)
{
    vector<int> res;
    for (int i = 1; i <= x / i; i ++ )
    {
        if (x % i == 0)
        {
            res.push_back(i);
            if (i != x / i) res.push_back(x / i);
        }
    }
    sort(res.begin(), res.end());
    for (auto item : res) cout << item << " ";
    cout << endl;
    return ;
}

约数个数

有一个数NNN,可以进行质因数分解:N=p1α1p2α2p3α3...pkαkN=p_1^{\alpha_1}p_2^{\alpha_2}p_3^{\alpha_3}...p_k^{\alpha_k}N=p1α1p2α2p3α3...pkαk。任何一个约数可以表示成d=p1β1p2β2p3β3...pkβkβi≤αid=p_1^{\beta_1}p_2^{\beta_2}p_3^{\beta_3}...p_k^{\beta_k}\beta_i \leq \alpha_id=p1β1p2β2p3β3...pkβkβi≤αi。β1\beta_1β1有α1+1\alpha_1 + 1α1+1种选法,β2\beta_2β2有α2+1\alpha_2 + 1α2+1种选法,βk\beta_kβk有αk+1\alpha_k+ 1αk+1种选法,所以约数个数一共有(α1+1)(α2+1)(α3+1)...(αk+1)(\alpha_1 + 1)(\alpha_2 + 1)(\alpha_3 + 1)...(\alpha_k + 1)(α1+1)(α2+1)(α3+1)...(αk+1)种。

cpp 复制代码
unordered_map<int, int> primes;
while(n--){
        cin >> x;
        for(int i = 2;i <= x/i; ++i){
            while(x % i == 0){
                x /= i;
                primes[i] ++;
            }
        }
        if(x > 1) primes[x] ++;
    }
    for(auto i : primes) ans = ans*(i.second + 1) % mod;
    cout << ans;

约数之和

有一个数NNN,可以进行质因数分解:N=p1α1p2α2p3α3...pkαkN=p_1^{\alpha_1}p_2^{\alpha_2}p_3^{\alpha_3}...p_k^{\alpha_k}N=p1α1p2α2p3α3...pkαk。任何一个约数可以表示成d=p1β1p2β2p3β3...pkβkβi≤αid=p_1^{\beta_1}p_2^{\beta_2}p_3^{\beta_3}...p_k^{\beta_k}\beta_i \leq \alpha_id=p1β1p2β2p3β3...pkβkβi≤αi。约数之和为(1+p11+p12+...+p1α1)(1+p21+p22+...+p2α2)...(1+pk1+pk2+...+pkαk)(1+p_1^1+p_1^2+...+p_1^{\alpha_1})(1+p_2^1+p_2^2+...+p_2^{\alpha_2})...(1+p_k^1+p_k^2+...+p_k^{\alpha_k})(1+p11+p12+...+p1α1)(1+p21+p22+...+p2α2)...(1+pk1+pk2+...+pkαk)。

cpp 复制代码
unordered_map<int, int> primes;

    while (n -- )
    {
        int x;
        cin >> x;

        for (int i = 2; i <= x / i; i ++ )
            while (x % i == 0)
            {
                x /= i;
                primes[i] ++ ;
            }

        if (x > 1) primes[x] ++ ;
    }

    LL res = 1;
    for (auto p : primes)
    {
        LL a = p.first, b = p.second;
        LL t = 1;
        while (b -- ) t = (t * a + 1) % mod;
        res = res * t % mod;
    }

    cout << res << endl;

最大公约数

求两个正整数 aaa 和 bbb 的 最大公约数 ddd

则有 gcd(a,b)=gcd(b,a%b)gcd(a,b) = gcd(b,a\%b)gcd(a,b)=gcd(b,a%b)

证明:

设a%b=a−k∗ba\%b = a - k*ba%b=a−k∗b 其中k=a/bk = a/bk=a/b(向下取整)

若ddd是(a,b)(a,b)(a,b)的公约数 则知 d∣ad|ad∣a 且 d∣bd|bd∣b 则易知 d∣a−k∗bd|a-k*bd∣a−k∗b 故ddd也是(b,a%b)(b,a\%b)(b,a%b) 的公约数

若ddd是(b,a%b)(b,a\%b)(b,a%b)的公约数 则知 d∣bd|bd∣b 且 d∣a−k∗bd|a-k*bd∣a−k∗b 则 d∣a−k∗b+k∗b=d∣ad|a-k*b+k*b = d|ad∣a−k∗b+k∗b=d∣a 故而ddd同时整除aaa和bbb 所以ddd也是(a,b)(a,b)(a,b)的公约数

因此(a,b)(a,b)(a,b)的公约数集合和(b,a%b)(b,a\%b)(b,a%b)的公约数集合相同 所以他们的最大公约数也相同

时间复杂度O(log(a+b))O(log(a+b))O(log(a+b))

cpp 复制代码
int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}

欧拉函数

1∼N1∼N1∼N 中与NNN互质的数的个数被称为欧拉函数,记为 ϕ(N)\phi(N)ϕ(N)。

欧拉函数

对于N=p1α1p2α2p3α3...pkαkN=p_1^{\alpha_1}p_2^{\alpha_2}p_3^{\alpha_3}...p_k^{\alpha_k}N=p1α1p2α2p3α3...pkαk,有ϕ(N)=N×p1−1p1×p2−1p2...×pk−1pk\phi(N)=N×\frac{p_1-1}{p_1}×\frac{p_2-1}{p_2}...×\frac{p_k-1}{p_k}ϕ(N)=N×p1p1−1×p2p2−1...×pkpk−1

证明:

根据容斥原理,与NNN互质的数的个数为N−Np1−Np2−Np3...+Np1p2+Np1p3+...−Np1p2p3−Np1p2p4−...N-\frac{N}{p_1}-\frac{N}{p_2}-\frac{N}{p_3}...+\frac{N}{p_1p_2}+\frac{N}{p_1p_3}+...-\frac{N}{p_1p_2p_3}-\frac{N}{p_1p_2p_4}-...N−p1N−p2N−p3N...+p1p2N+p1p3N+...−p1p2p3N−p1p2p4N−...

其中每一项都含有NNN,可以把NNN提出,含有pip_ipi数为奇数的项前符号为−-−,为偶数的项前面的符号为+++,即上式可以化简为N(1−1p1)(1−1p2)...(1−1pk)N(1-\frac{1}{p_1})(1-\frac{1}{p_2})...(1-\frac{1}{p_k})N(1−p11)(1−p21)...(1−pk1)。

时间复杂度为O(N)O(\sqrt N)O(N )

cpp 复制代码
for (int i = 2; i <= a / i; i ++ )
{
    if (a % i == 0)
    {
        while (a % i == 0) a /= i;
        res = res * (i - 1) / i;
    }
}
if (a > 1) res = res * (a - 1) / a;
cout << res << endl;

筛法求欧拉函数

我们可以在线性筛的基础上完成欧拉函数的求解。

1.ϕ(1)=1\phi(1)=1ϕ(1)=1

2.如果一个数NNN是质数,那么ϕ(N)=N−1\phi(N)=N-1ϕ(N)=N−1。

3.ϕ(primes[j]∗i)\phi(primes[j]*i)ϕ(primes[j]∗i)分两种情况:

(1).i%primes[j]==0i\%primes[j]==0i%primes[j]==0时,primes[j]primes[j]primes[j]是iii的最小质因子,所以在ϕ(i)\phi(i)ϕ(i)中,(1−1primes[j])(1-\frac{1}{primes[j]})(1−primes[j]1)这一项已经考虑过了,只需要将NNN修正为primes[j]primes[j]primes[j]倍,最终结果为phi[i]∗primes[j]phi[i] * primes[j]phi[i]∗primes[j]。

(2).i%primes[j]!=0i\%primes[j]!=0i%primes[j]!=0时,primes[j]primes[j]primes[j]不是iii的质因子,只是primes[j]∗iprimes[j] * iprimes[j]∗i的最小质因子,因此不仅需要将基数NNN修正为primes[j]primes[j]primes[j]倍,还需要补上1−1/primes[j]1 - 1 / primes[j]1−1/primes[j]这一项,因此最终结果phi[i]∗(primes[j]−1)phi[i] * (primes[j] - 1)phi[i]∗(primes[j]−1)。

cpp 复制代码
void get_eulers(int n)
{
    phi[1] = 1;
    for (int i = 2; i <= n; i++)
    {
        if (!st[i])
        {
            primes[cnt++] = i;
            phi[i] = i - 1; 
        }
        for (int j = 0; primes[j] <= n / i; j++)
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0)
            {
                phi[primes[j] * i] = phi[i] * primes[j]; 
                break;
            }
            phi[primes[j] * i] = phi[i] * (primes[j] - 1);
        }
    }
}

快速幂

快速幂

因为每一个整数都可以唯一表示为若干指数不重复的2的次幂的和,对于aba^bab来说,我们可以先预处理出a20,a21,a22,...,a2logba^{2^{0}},a^{2^{1}},a^{2^{2}},...,a^{2^{logb}}a20,a21,a22,...,a2logb,aba^bab可以由这些数组合而来,即ab=a2x1a2x2...a2xta^b=a^{2^{x_1}}a^{2^{x_2}}...a^{2^{x_t}}ab=a2x1a2x2...a2xt。因此,可以把本题时间复杂度从O(b)O(b)O(b)降低为O(logb)O(logb)O(logb)。

cpp 复制代码
int qmi(ll a, int b, int p)
{
    ll res = 1;
    while (b)
    {
        if (b & 1) res = res * a % p;
        b >>= 1;
        a = a * a % p;
    }
    return res;
}

快速幂求逆元

1.欧拉定理:对任意两个正整数 a,na, na,n,如果两者互质,那么有aϕ(n)≡1(mod n)a^{\phi(n)}≡1(mod\ n)aϕ(n)≡1(mod n)

证明:

在1~n1~n1~n内有a1,a2,a3,...,aϕ(n)a_1,a_2,a_3,...,a_{\phi(n)}a1,a2,a3,...,aϕ(n)与nnn互质,又aaa与nnn互质,所以aa1%n,aa2%n,aa3%n,...,aaϕ(n)%naa_1\%n,aa_2\%n,aa_3\%n,...,aa_{\phi(n)}\%naa1%n,aa2%n,aa3%n,...,aaϕ(n)%n也与nnn互质。

下面用反证法证明该集合内任意两个数互不相等:

如果其中两项相等即aai%n=aaj%n=baa_i\%n=aa_j\%n=baai%n=aaj%n=b,有aai=k1n+b,aaj=k2n+baa_i=k_1n+b,aa_j=k_2n+baai=k1n+b,aaj=k2n+b。
a(ai−aj)=(ki−kj)na(a_i-a_j)=(k_i-k_j)na(ai−aj)=(ki−kj)n,
a(ai−aj)≡0(mod n)a(a_i-a_j)≡0(mod\ n)a(ai−aj)≡0(mod n),

所以ai−aj=k∗na_i-a_j=k*nai−aj=k∗n,kkk是非0整数

所以ai,aja_i,a_jai,aj至少相差一个nnn

又因为ai,aja_i,a_jai,aj都在1~n1~n1~n范围内,所以差值只会小于nnn

所以假设不成立,集合内任意两项都不相等

所以a1,a2,a3,...,aϕ(n)a_1,a_2,a_3,...,a_{\phi(n)}a1,a2,a3,...,aϕ(n)和aa1%n,aa2%n,aa3%n,...,aaϕ(n)%naa_1\%n,aa_2\%n,aa_3\%n,...,aa_{\phi(n)}\%naa1%n,aa2%n,aa3%n,...,aaϕ(n)%n两个集合一定是完全相同的。

有a1×a2×a3×...×aϕ(n)≡aa1×aa2×aa3×...×aaϕ(n)(mod n)a_1×a_2×a_3×...×a_{\phi(n)}≡aa_1×aa_2×aa_3×...×aa_{\phi(n)}(mod\ n)a1×a2×a3×...×aϕ(n)≡aa1×aa2×aa3×...×aaϕ(n)(mod n)

化简为:aϕ(n)≡1(mod n)a^{\phi(n)}≡1(mod\ n)aϕ(n)≡1(mod n),得证
2.费马小定理

对任意两个正整数 a,na, na,n,如果a不是n的倍数,且n是质数,那么有an−1≡1(mod n)a^{n-1}≡1(mod\ n)an−1≡1(mod n)

费马小定理是欧拉定理在n为质数时的特殊情况,因为ϕ(n)=n−1\phi(n)=n-1ϕ(n)=n−1,所以易证。

乘法逆元 :若整数 b,mb,mb,m 互质,并且对于任意的整数 aaa,如果满足 b∣ab|ab∣a,则存在一个整数 xxx,使得 a/b≡a×x(mod m)a/b≡a×x(mod\ m)a/b≡a×x(mod m),则称 xxx 为 bbb 的模 mmm 乘法逆元,记为 b−1(mod m)b^{−1}(mod\ m)b−1(mod m)
bbb存在乘法逆元的充要条件是 bbb 与模数 mmm 互质。当模数 mmm 为质数时,bm−2b^{m−2}bm−2 即为 bbb 的乘法逆元。

证明:

当mmm为质数时

根据a/b≡a×x(mod m)a/b≡a×x(mod\ m)a/b≡a×x(mod m),有bx≡1(mod m)bx≡1(mod\ m)bx≡1(mod m)。

根据费马小定理,有
bm−1≡1(mod m)b^{m-1}≡1(mod\ m)bm−1≡1(mod m),
bbm−2≡1(mod m)bb^{m-2}≡1(mod\ m)bbm−2≡1(mod m),
x=bm−2x=b^{m-2}x=bm−2。可以使用快速幂来对=bm−2=b^{m-2}=bm−2进行求解。

扩展欧几里得算法

扩展欧几里得算法

裴蜀定理:对于任意正整数a,ba,ba,b,一定存在非零整数x,yx,yx,y,使得ax+by=(a,b)ax+by=(a,b)ax+by=(a,b)

证明:在我们使用欧几里得算法求a,ba,ba,b最大公约数的过程中:

1.b=0b=0b=0时,可以取x=1,y=0x=1,y=0x=1,y=0,使得a=gcd(a,0)a=gcd(a,0)a=gcd(a,0),有解;

2.b≠0b≠0b=0时,有gcd(a,b)=gcd(b,a%b)gcd(a,b)=gcd(b,a\%b)gcd(a,b)=gcd(b,a%b),假设存在x,yx,yx,y使得bx+a%by=(a,b)bx+a\%by=(a,b)bx+a%by=(a,b),则有bx+(a−⌊ab⌋b)y=ay+(x−⌊ab⌋y)bbx+(a-\lfloor \frac{a}{b}\rfloor b)y=ay+(x-\lfloor \frac{a}{b}\rfloor y)bbx+(a−⌊ba⌋b)y=ay+(x−⌊ba⌋y)b

因此x′=y,y′=(x−⌊ab⌋)x^{'}=y,y^{'}=(x-\lfloor \frac{a}{b}\rfloor)x′=y,y′=(x−⌊ba⌋)就是一组可行解。

在欧几里得算法的递归过程中应用数学归纳法,可知裴蜀定理成立。

以上过程不仅证明了裴蜀定理的正确性,也给出了计算x,yx,yx,y的方法,这个算法就叫做扩展欧几里得算法

cpp 复制代码
int exgcd(int a, int b, int &x, int &y)
{
    if (!b)
    {
        x = 1, y = 0;
        return a;
    }
    int t = exgcd(b, a % b, y, x);
    y = y - a / b * x;
    return t;
}

线性同余方程

对于一组a,b,ma,b,ma,b,m,我们想要求出一个xxx,使得ax≡b(mod m)ax≡b(mod\ m)ax≡b(mod m)。
ax≡b(mod m)ax≡b(mod\ m)ax≡b(mod m)
ax=km+bax=km+bax=km+b
ax−km=bax-km=bax−km=b

令k′=−kk^{'}=-kk′=−k
ax+k′m=bax+k^{'}m=bax+k′m=b

如果bbb不是(a,m)(a,m)(a,m)的倍数,那么一定无解,因为ax+k′max+k^{'}max+k′m一定是(a,m)(a,m)(a,m)的倍数;

如果bbb是(a,m)(a,m)(a,m)的倍数,那么一定有解,因为对于ax+k′max+k^{'}max+k′m,我们可以使用扩展欧几里得算法求出来满足ax+k′m=(a,m)ax+k^{'}m=(a,m)ax+k′m=(a,m)的解xxx,再把xxx扩大到b/(a,m)b/(a,m)b/(a,m)倍就是答案。

中国剩余定理

m1,m2,...,mkm_1,m_2,...,m_km1,m2,...,mk两两互质,对任意整数a1,a2,...,aka_1,a_2,...,a_ka1,a2,...,ak下面方程组都存在整数解。
{x≡a1 (mod m1)x≡a2 (mod m2)...x≡ak (mod mk) \begin{cases} x≡a_1\ (mod\ m_1) \\ x≡a_2\ (mod\ m_2) \\ ...\\ x≡a_k\ (mod\ m_k) \\ \end{cases} ⎩ ⎨ ⎧x≡a1 (mod m1)x≡a2 (mod m2)...x≡ak (mod mk)
M=∏i=1kmiM=\prod_{i=1}^km_iM=∏i=1kmi,Mi=MmiM_i=\frac{M}{mi}Mi=miM

那么x≡∑i=1kaiMiMi−1x≡\sum_{i=1}^ka_iM_iM_i^{-1}x≡∑i=1kaiMiMi−1 是一组可行的解。
∀k≠i\forall k≠i∀k=i, akMkMk−1≡0 (mod mi)\ a_kM_kM_k^{-1}≡0\ (mod\ m_i) akMkMk−1≡0 (mod mi)

又aiMiMi−1≡ai (mod mi)a_iM_iM_i^{-1}≡a_i\ (mod\ m_i)aiMiMi−1≡ai (mod mi)

所以x≡∑i=1kaiMiMi−1x≡\sum_{i=1}^ka_iM_iM_i^{-1}x≡∑i=1kaiMiMi−1 是一组可行的解。

另外xxx的通解为∑i=1kaiMiMi−1+kM\sum_{i=1}^ka_iM_iM_i^{-1}+kM∑i=1kaiMiMi−1+kM。

中国剩余定理要求m1,m2,...,mkm_1,m_2,...,m_km1,m2,...,mk两两互质,在没有这个条件限制的情况下,也有解决办法。

先分析前两个方程:
x=k1a1+m1x=k_1a_1+m_1x=k1a1+m1
x=k2a2+m2x=k_2a_2+m_2x=k2a2+m2

联立得:k1a1+m1=k2a2+m2k_1a_1+m_1=k_2a_2+m_2k1a1+m1=k2a2+m2
k1a1−k2a2=m2−m1k_1a_1-k_2a_2=m_2-m_1k1a1−k2a2=m2−m1

如果(a1,a2)∣(m2−m1)(a_1,a_2)|(m_2-m_1)(a1,a2)∣(m2−m1)一定有解,反之一定没解。

先用exgcdexgcdexgcd求k1,k2,dk1,k2,dk1,k2,d,得到k1,k2k_1,k_2k1,k2的通解如下:
{k1=m2−m1dk1+ka2dk2=m2−m1dk2+ka1d \begin{cases} k_1=\frac{m_2-m_1}{d}k_1+k\frac{a2}{d} \\ k_2=\frac{m_2-m_1}{d}k_2+k\frac{a1}{d} \\ \end{cases} {k1=dm2−m1k1+kda2k2=dm2−m1k2+kda1

带入式子1中,有x=(m2−m1dk1+ka2d)a1+m1x=(\frac{m_2-m_1}{d}k_1+k\frac{a2}{d})a_1+m_1x=(dm2−m1k1+kda2)a1+m1
x=m2−m1dk1a1+ka1a2d+m1x=\frac{m_2-m_1}{d}k_1a_1+k\frac{a_1a_2}{d}+m_1x=dm2−m1k1a1+kda1a2+m1
x=k[a1,a2]+m2−m1dk1a1+m1x=k[a_1,a_2]+\frac{m_2-m_1}{d}k_1a_1+m_1x=k[a1,a2]+dm2−m1k1a1+m1

可以发现新得到的式子和原先式子1形式是相同的,在新式子中,[a1,a2][a_1,a_2][a1,a2]是原先式子的a1a_1a1,m2−m1dk1a1+m1\frac{m_2-m_1}{d}k_1a_1+m_1dm2−m1k1a1+m1是原先式子的m1m_1m1,这样一来我们就把式子1,2融合成了一个新的式子,并且形式和原先式子相同,以此类推,考虑完所有式子后,就能求解得到答案。

cpp 复制代码
#include <iostream>
using namespace std;
typedef long long ll;
int n;
ll exgcd(ll a1, ll a2, ll &k1, ll &k2)
{
    if (!a2)
    {
        k1 = 1, k2 = 0;
        return a1;
    }
    ll t = exgcd(a2, a1 % a2, k2, k1);
    k2 = k2 - a1 / a2 * k1;
    return t;
}
int main()
{
    cin >> n;
    ll a1, m1, x;
    cin >> a1 >> m1;
    for (int i = 1; i < n; i ++ )
    {
        ll a2, m2;
        cin >> a2 >> m2;
        ll k1, k2;
        ll d = exgcd(a1, a2, k1, k2);
        if ((m2 - m1) % d)
        {
            x = -1;
            break;
        }
        int t = a2 / d;
        k1 *= (m2 - m1) / d;
        k1 = (k1 % t + t) % t;
        m1 += k1 * a1;
        a1 = a1 * a2 / d;
    }
    if (x != -1) x = (m1 % a1 + a1) % a1;
    cout << x << endl;
    return 0;
}

使用矩阵快速幂优化递推问题

对于一个递推问题,如递推式的每一项系数都为常数,我们可以使用矩阵快速幂来对算法进行优化。

一般形式为:
Fn=F1×An−1F_n=F_1×A^{n-1}Fn=F1×An−1

由于递推式的每一项系数都为常数,因此对于每一步的递推,AAA里面都是相同的常数。

对于An−1A^{n-1}An−1部分使用快速幂求解,根据FnF_nFn就能得到最终答案。

矩阵快速幂:

cpp 复制代码
void mul(int c[][N], int a[][N], int b[][N])
{
    int temp[N][N] = {0};
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j < N; j ++ )
            for (int k = 0; k < N; k ++ )
                temp[i][j] = (temp[i][j] + (ll)a[i][k] * b[k][j] % m) % m;
    memcpy(c, temp, sizeof temp);
}
while (k)
    {
        if (k & 1) mul(f, f, a);
        mul(a, a, a);
        k >>= 1;
    }

高斯消元

前置知识:初等行(列)变换

1.把某一行乘一个非0的数 (方程的两边同时乘上一个非0数不改变方程的解)

2.交换某两行 (交换两个方程的位置)

3.把某行的若干倍加到另一行上去 (把一个方程的若干倍加到另一个方程上去)

算法步骤

枚举每一列c,

1.找到当前列绝对值最大的一行

2.用初等行变换(2) 把这一行换到最上面(未确定阶梯型的行,并不是第一行)

3.用初等行变换(1) 将该行的第一个数变成 1(其余所有的数字依次跟着变化)

4.用初等行变换(3) 将下面所有行的当且列的值变成 0

cpp 复制代码
int gauss()
{
    int r, c;
    // 得到上三角矩阵
    for (r = 0, c = 0; c < n; c ++ )
    {
        int t = r;
        // 找主元
        for (int i = r; i < n; i ++ )
        {
            if (fabs(a[i][c]) > fabs(a[t][c])) 
                t = i;
        }
        if (fabs(a[t][c]) < eps) continue;
        // 交换两行
        for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]);
        // 归一化
        for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];
        // 消
        for (int i = r + 1; i < n; i ++ )
        {
            if (fabs(a[i][c]) < eps) continue;
            for (int j = n; j >= c; j -- ) a[i][j] -= a[i][c] * a[r][j];
        }
        r ++ ;
    }
    if (r < n)
    {
        for (int i = r; i < n; i ++ ) 
            if (fabs(a[i][n]) > eps) return 2;
        return 1;
    }
    // 得到对角矩阵
    for (int i = n - 1; i >= 0; i -- )
    {
        for (int j = i + 1; j < n; j ++ )
        {
            a[i][n] -= a[i][j] * a[j][n];
        }
    }
    return 0;
}

求组合数

根据数据范围不同,求组合数有四种方法。

递推

Cab=Ca−1b+Ca−1b−1C_a^b=C_{a-1}^b+C_{a-1}^{b-1}Cab=Ca−1b+Ca−1b−1 证明:

我们可以把选择的方案分成两大类,对于aaa个数里面的某个数,我可能选出来了,也可能没选出来

选出来了:Ca−1b−1C_{a-1}^{b-1}Ca−1b−1,还需要在剩下的a−1a-1a−1个数选b−1b-1b−1个。

没选出来:Ca−1bC_{a-1}^{b}Ca−1b需要在剩下的a−1a-1a−1个数选bbb个。

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

1≤n≤10000,1≤n≤10000,1≤n≤10000,
1≤b≤a≤20001≤b≤a≤20001≤b≤a≤2000

先利用递推求出所有的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)。

cpp 复制代码
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!(a−b)!a!

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

1≤n≤10000,1≤n≤10000,1≤n≤10000,
1≤b≤a≤1051≤b≤a≤10^51≤b≤a≤105

用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[i−1]∗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[i−1]∗qmi(i,p−2,p)%p

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

cpp 复制代码
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)Cab≡Ca%pb%pCa/pb/p (mod p)

证明:

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

1≤n≤201≤n≤201≤n≤20
1≤b≤a≤10181≤b≤a≤10^{18}1≤b≤a≤1018
1≤p≤1051≤p≤10^51≤p≤105

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

cpp 复制代码
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,ba,b,请你输出 CbaC_b^aCba的值。

注意结果可能很大,需要使用高精度计算。

1≤b≤a≤50001≤b≤a≤50001≤b≤a≤5000

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)!(a−b)!中这个质数的个数。

3.我们得到了CabC_a^bCab每个质因数的个数,可以直接使用高精度乘法求解。

cpp 复制代码
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;
}

卡特兰数

在一个二维平面内,从(0, 0)出发到达(n, n),每次可以向上或者向右走一格,0代表向右走一个,1代表向上走一格,则每条路径都会代表一个01序列,则满足任意前缀中0的个数不少于1个数序列对应的路径则右下侧

符合要求的路径必须严格在上图中红色线的下面(不可以碰到图中的红线,可以碰到绿线)。则我们考虑任意一条不合法路径,例如下图:

所有路径的条数为 C2nnC_{2n}^{n}C2nn,其中不合法的路径有 C2nn−1C_{2n}^{n-1}C2nn−1 条,因此合法路径有:
C2nn−C2nn−1C_{2n}^{n}-C_{2n}^{n-1}C2nn−C2nn−1
=(2n)!n!n!−(2n)!(n−1)!(n+1)!=\frac{(2n)!}{n!n!}-\frac{(2n)!}{(n-1)!(n+1)!}=n!n!(2n)!−(n−1)!(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

容斥原理

∣⋃i=1nAi∣=∑i=1n∣Ai∣−∑1≤i<j≤n∣Ai∩Aj∣+∑1≤i<j≤k≤n∣Ai∩Aj∩Ak∣−...+(−1)n−1∣A1∩...∩An∣|\mathop{\bigcup}\limits_{i=1}^n A_i|=\sum\limits_{i=1}^{n} |A_i|-\sum\limits_{1\leq i<j\leq n}|A_i\cap A_j|+\sum\limits_{1\leq i<j\leq k\leq n}|A_i\cap A_j\cap A_k|-...+(-1)^{n-1}|A_1\cap...\cap A_n|∣i=1⋃nAi∣=i=1∑n∣Ai∣−1≤i<j≤n∑∣Ai∩Aj∣+1≤i<j≤k≤n∑∣Ai∩Aj∩Ak∣−...+(−1)n−1∣A1∩...∩An∣

两个集合的容斥原理
A∪B=A+B−A∩BA\cup B=A+B-A\cap BA∪B=A+B−A∩B

三个集合的容斥原理
A∪B∪C=A+B+C−A∩B−A∩C−B∩C+A∩B∩CA\cup B \cup C=A+B+C-A\cap B-A\cap C-B\cap C+A\cap B\cap CA∪B∪C=A+B+C−A∩B−A∩C−B∩C+A∩B∩C

正确性证明:

Cn0−Cn1+Cn2−...+(−1)nCnn=0C_n^0-C_n^1+C_n^2-...+(-1)^{n}C_n^n=0Cn0−Cn1+Cn2−...+(−1)nCnn=0

因为对于(−1+1)n(-1+1)^n(−1+1)n,很显然结果为0,展开之后的式子Cn0−Cn1+Cn2−...+(−1)n−1CnnC_n^0-C_n^1+C_n^2-...+(-1)^{n-1}C_n^nCn0−Cn1+Cn2−...+(−1)n−1Cnn也就为0了。

对于一个元素xxx,它属于kkk个集合。

因为Ck1−Ck2+Ck3−...+(−1)k−1Ckk=1C_k^1-C_k^2+C_k^3-...+(-1)^{k-1}C_k^k=1Ck1−Ck2+Ck3−...+(−1)k−1Ckk=1,所以说在上面的这个公式中,xxx正好被包含了一次,所以是正确的。

另外对于nnn个集合的并,按照容斥原理拆开后一共有2n−12^n-12n−1项。

Cn0+Cn1+Cn2−...+Cnn=0C_n^0+C_n^1+C_n^2-...+C_n^n=0Cn0+Cn1+Cn2−...+Cnn=0

因为对于(1+1)n(1+1)^n(1+1)n,展开之后的式子Cn0+Cn1+Cn2−...+CnnC_n^0+C_n^1+C_n^2-...+C_n^nCn0+Cn1+Cn2−...+Cnn也就为2n2^n2n了。

博弈论

若一个游戏满足:

1.由两名玩家交替行动

2.在游戏进行的任意时刻,可以执行的合法行动与轮到哪位玩家无关

3.不能行动的玩家判负

则称该游戏为一个公平组合游戏

则称该游戏为一个公平组合游戏。

NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。

给定一个有向无环图,图中有一个唯一的起点,在起点放有一枚棋子。两名玩家交替把这枚棋子沿着有向边进行移动,每次可以移动一步,无法移动判负。这类游戏被称为有向图游戏

任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连接有向边。

NIM游戏

给定nnn堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

必胜状态 :先手进行某一个操作,留给后手是一个必败状态时,对于先手来说是一个必胜状态。即先手可以走到某一个必败状态。
必败状态:先手无论如何操作,留给后手都是一个必胜状态时,对于先手来说是一个必败状态。即先手走不到任何一个必败状态。

结论:

假设nnn堆石子,石子数目分别是a1,a2,...,ana_1,a_2,...,a_na1,a2,...,an,如果a1⊕a2⊕...⊕an≠0a_1⊕a_2⊕...⊕a_n≠0a1⊕a2⊕...⊕an=0,先手必胜;否则先手必败。

证明:

1.结束状态每一堆都是0个石子,0⊕0⊕...⊕0=00⊕0⊕...⊕0=00⊕0⊕...⊕0=0

2.如果a1⊕a2⊕...⊕an=xa_1⊕a_2⊕...⊕a_n=xa1⊕a2⊕...⊕an=x,x≠0x≠0x=0,设xxx在二进制表示下,最高位的1在第kkk位,那么在a1~ana_1~a_na1~an中一定有一个aia_iai,它的第kkk位也是1。显然ai⊕x<aia_i⊕x<a_iai⊕x<ai,我们在aia_iai中取出来ai⊕xa_i⊕xai⊕x个石子,这样aia_iai中还剩下ai−(ai−ai⊕x)=ai⊕xa_i-(a_i-a_i⊕x)=a_i⊕xai−(ai−ai⊕x)=ai⊕x个。a1⊕a2⊕...⊕ai⊕x...⊕an=x⊕x=0a_1⊕a_2⊕...⊕a_i⊕x...⊕a_n=x⊕x=0a1⊕a2⊕...⊕ai⊕x...⊕an=x⊕x=0,这样就得到了各堆石子数异或为0的局面。

3.如果a1⊕a2⊕...⊕an=0a_1⊕a_2⊕...⊕a_n=0a1⊕a2⊕...⊕an=0,那么不管我们怎么取石子,最后都会得到各堆石子数异或不为0的局面。反证法:

假设在第iii堆中取石子,把aia_iai取成ai′a_i^{'}ai′,可以得到a1⊕a2⊕...⊕an=0a_1⊕a_2⊕...⊕a_n=0a1⊕a2⊕...⊕an=0,那么就会有a1⊕a2⊕...⊕ai⊕...⊕an⊕a1⊕a2⊕...⊕ai′⊕...⊕an=ai⊕ai′=0a_1⊕a_2⊕...⊕a_i⊕...⊕a_n⊕a_1⊕a_2⊕...⊕a_i^{'}⊕...⊕a_n=a_i⊕a_i^{'}=0a1⊕a2⊕...⊕ai⊕...⊕an⊕a1⊕a2⊕...⊕ai′⊕...⊕an=ai⊕ai′=0,所以aia_iai和ai′a_i^{'}ai′相同,但是实际上ai>ai′a_i>a_i^{'}ai>ai′,产生矛盾,假设不成立。

综上,如果当前状态异或和不为0,那么就会让对手陷入到石子数异或为0的局面;而对手不管如何操作,我们面临的状态依然是异或和不为0。以此类推,最后我们一定可以把对手逼到各堆石子都为0的局面。

台阶------NIM游戏

现在,有一个 nnn 级台阶的楼梯,每级台阶上都有若干个石子,其中第 i

级台阶上有 aia_iai 个石子(i≥1)(i≥1)(i≥1)。

两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。

已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

假设nnn堆石子,石子数目分别是a1,a2,...,ana_1,a_2,...,a_na1,a2,...,an,如果a1⊕a3⊕a5⊕...≠0a_1⊕a_3⊕a_5⊕...≠0a1⊕a3⊕a5⊕...=0,先手必胜;否则先手必败。

证明:

1.结束状态每一堆都是0个石子,0⊕0⊕...⊕0=00⊕0⊕...⊕0=00⊕0⊕...⊕0=0

2.如果a1⊕a3⊕...=xa_1⊕a_3⊕...=xa1⊕a3⊕...=x,x≠0x≠0x=0,设xxx在二进制表示下,最高位的1在第kkk位,那么在a1,a3,...a_1,a_3,...a1,a3,...中一定有一个aia_iai,它的第kkk位也是1。显然ai⊕x<aia_i⊕x<a_iai⊕x<ai,我们在aia_iai中取出来ai⊕xa_i⊕xai⊕x个石子,这样aia_iai中还剩下ai−(ai−ai⊕x)=ai⊕xa_i-(a_i-a_i⊕x)=a_i⊕xai−(ai−ai⊕x)=ai⊕x个。a1⊕a2⊕...⊕ai⊕x...⊕an=x⊕x=0a_1⊕a_2⊕...⊕a_i⊕x...⊕a_n=x⊕x=0a1⊕a2⊕...⊕ai⊕x...⊕an=x⊕x=0,这样就得到了各堆石子数异或为0的局面。

3.如果a1⊕a3⊕...=0a_1⊕a_3⊕...=0a1⊕a3⊕...=0,那么如果我们取的是偶数堆的石子放到他的下一个奇数堆里面,那么下一个人可以把这个奇数堆里面刚刚被放的石子取出来放到下一个偶数堆,这样就保证了每一个奇数堆的石子总数和在这两个人操作之前一模一样,异或依然是0;如果取得是奇数堆石子,不管我们怎么取石子,最后都会得到各堆石子数异或不为0的局面。反证法:

假设在第iii堆中取石子,把aia_iai取成ai′a_i^{'}ai′,可以得到a1⊕a3⊕...=0a_1⊕a_3⊕...=0a1⊕a3⊕...=0,那么就会有a1⊕a3⊕...⊕ai⊕...⊕a1⊕a3⊕...⊕ai′⊕...=ai⊕ai′=0a_1⊕a_3⊕...⊕a_i⊕...⊕a_1⊕a_3⊕...⊕a_i^{'}⊕...=a_i⊕a_i^{'}=0a1⊕a3⊕...⊕ai⊕...⊕a1⊕a3⊕...⊕ai′⊕...=ai⊕ai′=0,所以aia_iai和ai′a_i^{'}ai′相同,但是实际上ai>ai′a_i>a_i^{'}ai>ai′,产生矛盾,假设不成立。

集合------NIM游戏

Mex运算

设SSS表示一个非负整数集合。定义mex(S)mex(S)mex(S)为求出不属于集合S的最小非负整数的运算,即:
mex(S)=minxmex(S) = min{x}mex(S)=minx, xxx属于自然数,且xxx不属于SSS

SG函数

在有向图游戏中,对于每个节点xxx,设从xxx出发共有kkk条有向边,分别到达节点y1,y2,...,yky_1, y_2, ..., y_ky1,y2,...,yk,定义SG(x)SG(x)SG(x)为xxx的后继节点y1,y2,...,yky_1, y_2, ..., y_ky1,y2,...,yk的SG函数值构成的集合再执行mex(S)mex(S)mex(S)运算的结果,即:
SG(x)=mex(SG(y1),SG(y2),...,SG(yk))SG(x) = mex({SG(y_1), SG(y_2), ..., SG(y_k)})SG(x)=mex(SG(y1),SG(y2),...,SG(yk)),终点的SGSGSG函数值为0。

特别地,整个有向图游戏GGG的SGSGSG函数值被定义为有向图游戏起点sss的SG函数值,即SG(G)=SG(s)SG(G) = SG(s)SG(G)=SG(s)。

定理

有向图游戏的某个局面必胜,当且仅当该局面对应节点的SGSGSG函数值大于0。

有向图游戏的某个局面必败,当且仅当该局面对应节点的SGSGSG函数值等于0。

证明:

1.如果局面对应节点的SGSGSG函数值大于0,说明我是可以从当前状态到达SGSGSG函数值为0的状态的。

2.如果局面对应节点的SGSGSG函数值为0,说明我只能从当前状态到达SGSGSG函数值不为0的状态。

综上,又因为终点的SGSGSG函数值为0,所以如果当前状态SGSGSG函数值不为0那么最后我就一定能把对手逼到终点,反之同理。

有向图游戏的和

设G1,G2,...,GmG_1, G_2, ..., G_mG1,G2,...,Gm 是mmm个有向图游戏。定义有向图游戏GGG,它的行动规则是任选某个有向图游戏GiG_iGi,并在GiG_iGi上行动一步。GGG被称为有向图游戏G1,G2,...,GmG_1, G_2, ..., G_mG1,G2,...,Gm的和。

有向图游戏的和的SGSGSG函数值等于它包含的各个子游戏SGSGSG函数值的异或和,即:
SG(G)=SG(G1)⊕SG(G2)⊕...⊕SG(Gm)SG(G) = SG(G_1)⊕SG(G_2)⊕ ... ⊕SG(G_m)SG(G)=SG(G1)⊕SG(G2)⊕...⊕SG(Gm)

证明:类似于NIM游戏中的证明,此处略。

给定nnn堆石子以及一个由kkk个不同正整数构成的数字集合 SSS。

现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合SSS,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

cpp 复制代码
int sg(int x)
{
    if (f[x] != -1) return f[x];
    unordered_set<int> S;
    for (int i = 1; i <= k; i ++ )
    {
        if (x >= s[i]) S.insert(sg(x - s[i]));
        else break;
    }
    for (int i = 0; ; i ++ )
        if (!S.count(i)) return i;
}
int main()
{
    cin >> k;
    memset(f, -1, sizeof f);
    for (int i = 1; i <= k; i ++ ) cin >> s[i];
    cin >> n;
    for (int i = 1; i <= n; i ++ )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }
    if (res) puts("Yes");
    else puts("No");
    return 0;
}

拆分------NIM游戏

给定nnn堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为 0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

相比于集合-NIM,这里的每一堆可以变成小于原来那堆的任意大小的两堆

即a[i]a[i]a[i]可以拆分成(b[i],b[j])(b[i],b[j])(b[i],b[j]),为了避免重复规定b[i]>=b[j]b[i]>=b[j]b[i]>=b[j],即:a[i]>b[i]>=b[j]a[i]>b[i]>=b[j]a[i]>b[i]>=b[j]

相当于一个局面拆分成了两个局面,由SGSGSG函数理论,多个独立局面的SGSGSG值,等于这些局面SGSGSG值的异或和。

因此需要存储的状态就是sg(b[i])⊕sg(b[j])sg(b[i])⊕sg(b[j])sg(b[i])⊕sg(b[j])(与集合-Nim的唯一区别)

相关推荐
业精于勤的牙2 小时前
最长特殊序列(二)
java·开发语言·算法
yong99903 小时前
C#实现OPC客户端与S7-1200 PLC的通信
开发语言·网络·算法·c#
yaoh.wang3 小时前
力扣(LeetCode) 111: 二叉树的最小深度 - 解法思路
python·程序人生·算法·leetcode·面试·职场和发展·深度优先
啊阿狸不会拉杆3 小时前
《数字图像处理》第 11 章 - 特征提取
图像处理·人工智能·算法·计算机视觉·数字图像处理
那雨倾城3 小时前
PiscCode实现用 YOLO 给现实世界加上「NPC 血条 HUD」
图像处理·python·算法·yolo·计算机视觉·目标跟踪
夏幻灵3 小时前
C++ 中手动重载赋值运算符(operator=)时实现部分复制的思路和方法
开发语言·c++·算法
九河云3 小时前
人工智能驱动企业数字化转型:从效率工具到战略引擎
人工智能·物联网·算法·机器学习·数字化转型
王德博客3 小时前
【题解】求分数序列(C++)
算法
再__努力1点3 小时前
LBP纹理特征提取:高鲁棒性的纹理特征算法
开发语言·人工智能·python·算法·计算机视觉