acwing 算法基础课(第四章完整版 c++详解)

 四、数学知识

(一).数论

1.质数判定-试除法 (AcWing 866. 试除法判定质数)

(1)算法思想:

        由于一个数的约数是成对出现的,因此若一个数n被d整除,则n也会被n/d整除,由于d从最小开始枚举,因此有,若可整除,则d小于另一个约数,即d<=n/d(比sqrt(n)快,比i*i<=n不会出现越界)

(2)代码实现思路:

        同上

(3)代码实现:

#include <iostream>
using namespace std;
bool is_prime(int x)
{
    if(x<=1) return false;
    for(int i=2;i<=x/i;i++)
        if(x%i==0) return false;
    return true;
}
int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        int x;
        cin>>x;
        if(is_prime(x)) puts("Yes");
        else puts("No");
    }
    return 0;
}

2.质因数分解-试除法(AcWing 867. 分解质因数)

(1)算法思想:

        题目中底数即为质因数,指数是指该质因数被当作因数的次数。只需判断该数x是否被i整除即可,若被整除,则这个从2开始枚举的i一定是质数(因为若i为合数,则在2~i-1中必有一个数可以整除i,那么先枚举到的一定是这个合数的因子,依次递推最终会先枚举到一个没有因子的数,即为质数),由于存在大于sqrt(x)的质因数,此时的质因数就是x被整除完剩余的数,且只有一个,即指数为1(如:51,最初能整除该数的质数为3,而后在循环中就寻找不到下一个质因数,但显然能被17整除,且指数为1,且该数一定为质数,原因与上面的推理相同)。

(2)代码实现思路:

        同上

(3)代码实现:

#include <iostream>
using namespace std;
void divide(int x)
{
    for(int i=2;i<=x/i;i++)
    {
        if(x%i==0)
        {
            int s=0;
            while(x%i==0)
            {
                x/=i;
                s++;
            }
            printf("%d %d\n",i,s);
        }
    }
    if(x>1) printf("%d %d\n",x,1);
    puts("");
}
int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        int x;
        cin>>x;
        divide(x);
    }
}

3.朴素筛法(AcWing 868. 筛质数)

(1)算法思想:

        求1~n范围内所有的素数可以通过筛法来求解。将i从2开始枚举,每次枚举若i未被标记,则说明他是质数,则将其加入到质数数组中,同时对i的倍数标记为合数。

(2)代码实现思路:

        同上

(3)代码实现:

#include <iostream>
using namespace std;
const int N=1000010;
int n,prime[N],cnt;
bool st[N];
void get_prime(int x)
{
    for(int i=2;i<=x;i++)
    {
        if(!st[i]) prime[cnt++]=i;
        for(int j=2*i;j<=x;j+=i) st[j]=true;
    }
}
int main()
{
    cin>>n;
    get_prime(n);
    cout<<cnt;
    return 0;
}

4.埃氏筛法

(1)算法思想:

        在朴素筛法的基础上不在对每个数进行倍数删除,而是只对质数进行倍数删除,因为所有的合数都是更小的质数的倍数

(2)代码实现思路:

        同上

(3)代码实现:

//埃氏筛
#include <iostream>
using namespace std;
const int N=100010;
int n,cnt;
int prime[N];
bool st[N];
void get_prime(int x)
{
    for (int i = 2; i <= x; i++)
    {
        if(!st[i])
        {
            prime[cnt++] = i;
            for (int j = 2 * i; j <= x; j += i)
                st[j] = true;
        }
    }
}
int main() {
    cin>>n;
    get_prime(n);
    cout<<cnt;
    return 0;
}

5.线性筛法

(1)算法思想:

        用每个数的最小质因数来筛除,保证了每一个数只被筛除1次,因此为线性(即每一轮i乘以当前所有找到的质数,所以不会重复)。先对未筛除的标记为质数,然后遍历现有的质数数组,将里面每一个元素乘以当前的i,将这个数字筛除,当该元素整除i时跳出循环,即说明此时的元素为i的最小质因数。(为什么该元素为最小质因数时要跳出循环,因为在达到这个条件之前,筛除的每一个数都以当前元素为最小质因数,当达到这个条件由于此时为i的最小质因数,要利用大于i的数的最小质因数筛除那些大于i的数就必须保证p[j]*i中p[j]为最小质因子,但是由于继续进行的话p[j]大于i的最小质因子,因此p[j]*i的最小质因子不在是p[j],所以要跳出循环)

(2)代码实现思路:

        同上

(3)代码实现:

#include <iostream>
using namespace std;
const int N=100010;
int n,cnt;
int prime[N];
bool st[N];
void get_prime(int x)
{
    for (int i = 2; i <= x; i++)
    {
        if(!st[i])
        {
            prime[cnt++] = i;
        }
        for(int j=0;prime[j]<=x/i;j++)
        {
            st[prime[j]*i]=true;
            if(i%prime[j]==0) break;//保证只筛选一次,不重复筛除一个数
        }
    }
}
int main() {
    cin>>n;
    get_prime(n);
    cout<<cnt;
    return 0;
}

6.试除法求约数(AcWing 869. 试除法求约数)

(1)算法思想:

        与求质数类似,不过要同时将另一个约数加入数组中,同时注意不要将同一个约数加入两次

(2)代码实现思路:

        同上

(3)代码实现:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int n;
vector<int> 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());
    return res;
}
int main() {
    cin>>n;
    while(n--)
    {
        int x;
        cin>>x;
        vector<int> res=get_divisors(x);
        for(auto t:res) cout<<t<<" ";
        puts("");
    }

    return 0;
}

7.约数个数(AcWing 870. 约数个数)

(1)算法思想:

        N为该数字,d是N的约数,由算数基本定理可知对于任意一个数N,若N不为质数,那么N可以唯一分解成有限个质数的乘积N=p1^{\alpha 1}*p2^{\alpha2}*...*pn^{\alpha n},这里P1<P2<P3......<Pn均为质数,其中指数ai是正整数。又N的每一个约数d都相当于在这n个pi当中选取不同的个数(\beta i),每一种选法都唯一对应一个约数,又因为对于每一个\beta i取值范围是0~\alpha i,共有\alpha i+1个方案,因此对于所有的\beta i共有\Pi 1+ai种选法,每种选法对应一个约数,因此约数个数为该公式。(int 范围内约数最多1500个)

        引自算法基础课视频

        题目要求n个数乘积s的约数个数,可以将每一个数分解成上面的形式,再将每一个相同的质因数的指数不断累加,最终将所有的质因数和指数对应起来就是s,最终对所有的指数加1相乘就是最终答案。

(2)代码实现思路:

        利用哈希表来存储每一个质因数的指数,对于每一个数,类似于质因数分解的方法,将指数累加到对应的哈希表中。

(3)代码实现:

#include <iostream>
#include <unordered_map>
using namespace std;
typedef long long LL;
const LL mod =1e9+7;
int main()
{
    int n;
    cin>>n;
    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 prime:primes)
    {
        res*=(prime.second+1)%mod;
    }
    cout<<res;
}

8.约数之和(AcWing 871. 约数之和)

(1)算法思想:

        见下图,将每项相乘展开后就对应所有约数。

      

        引自算法基础课视频

        思路与上题相似,都需要将每一个质因数对应的指数存在哈希表中,之后需要将每一个质因数的幂次从零依次增加到他的指数,将这些结果累加为t,最终将t相乘。

(2)代码实现思路:

        质因数为p,指数为a,t初始为1,实现求括号内的值可通过....p(p(p+1)+1)+1....因此令t=p*t+1即可。

(3)代码实现:

#include <iostream>
#include <unordered_map>
using namespace std;
typedef long long LL;
const LL mod =1e9+7;
int main()
{
    int n;
    cin>>n;
    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 prime:primes)
    {
        int p=prime.first, a=prime.second;
        LL t=1;
        while(a--) t=(p*t+1)%mod;
        res*=t%mod;
    }
    cout<<res;
}

9.欧几里得算法 / 辗转相除法(AcWing 872. 最大公约数)

(1)算法思想:

        a和b的最大公约数等于b和a mod b的最大公约数,因为左边任何公约数都是右边的公约数,反之同样。当b不为0时返回b,a mod b,否则由于a,0的最大公约数为a,因此返回a

(2)代码实现思路:

        同上

(3)代码实现:

#include <iostream>
using namespace std;
int gcd(int a,int b)
{
    return b? gcd(b,a%b):a;
}
int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        int x,y;
        cin>>x>>y;
        cout<<gcd(x,y)<<endl;
    }
}

10.欧拉函数(AcWing 873. 欧拉函数)

欧拉定理

(1)算法思想:

       

        具体证明可以通过容斥原理证明,简单来说就是若有一个数N,它的所有质因子为p1,p2...pn,且有算术基本定理可知N被所有质因子乘积唯一表示,寻找N之前的与它互质的自然数,就是将带有共同质因子的数筛掉,因此先将N以内的每一个质因子的倍数的数删掉,又由于这样会多删掉两个质因数乘积的倍数,因此又要加上两个质因数乘积的倍数的数,又由于这样会多加了三个质因数乘积的倍数的数,因此又要删掉三个........依此类推,最终迭代到N/p1p2...pn。最终累加后的结果进行合并就是上图中的式子。

(2)代码实现思路:

        同上,要注意避免小数。

(3)代码实现:

#include <iostream>
using namespace std;
int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        int a;
        cin>>a;
        int res=a;
        for(int i=2;i<=a/i;i++)
        {
            if(a%i==0)
            {
                res=res/i*(i-1);
                while(a%i==0) a/=i;
            }
        }
        if(a>1) res=res/a*(a-1);
        cout<<res<<endl;
    }
}

11.筛法求欧拉函数(AcWing 874. 筛法求欧拉函数)

(1)算法思想:

        利用线性筛来求解前N个数每一个数的欧拉函数,当一个数x为质数时,它的欧拉函数就是x-1,即除掉自身之外,前面都与他互质。当一个数是pj*i时,即线性筛的筛掉部分:当pj能整除i时,由于pj<=pi<i且pj为pj*i的质因数,因此pj*i的质因数中所有质因数都包含在i的所有质因数内了,因此再求pj*i的欧拉函数时只需要将i的欧拉函数乘以pj即可;当pj不能整除i时,说明pj不是i的质因数,因此pj*i的质因数比i多了一个pj,只需将i的欧拉函数乘以pj/pj*(pj-1)即可。最终将所有的数的欧拉函数求和即可。

(2)代码实现思路:

        同上。

(3)代码实现:

#include <iostream>
using namespace std;
const int N=1000010;
int phi[N],prime[N],cnt;
bool st[N];
long long get_euler(int n)
{
    phi[1]=1;
    long long res=0;
    for(int i=2;i<=n;i++)
    {
        if(!st[i])
        {
            prime[cnt++]=i;
            phi[i]=i-1;
        }
        for(int j=0;prime[j]<=n/i;j++)
        {
            st[prime[j]*i]=true;
            if(i%prime[j]==0)
            {
                phi[prime[j]*i]=prime[j]*phi[i];
                break;
            }
            phi[prime[j]*i]=phi[i]*(prime[j]-1);
        }
    }
    for(int i=1;i<=n;i++)
        res+=phi[i];
    return res;
}
int main()
{
    int n;
    cin>>n;
    cout<<get_euler(n);
}

12.快速幂(AcWing 875. 快速幂)

(1)算法思想:

        利用指数的二进制表示形式来减少乘法和幂运算的次数。求(a^k) mod p,可以将k转化成二进制,从而将a以2的指数为幂快速计算出a^k,如5^7:k=111,即k=2^0+2^1+2^2,则可以通过a^(2^0+2^1+2^2),即初始a=5对应于a^(2^0),下一次a=25对应于a^(2^1),下一次a=225,对应于a^(2^2),即每次将a扩大a倍,速度提高很多。

(2)代码实现思路:

        同上

(3)代码实现:

#include <iostream>
using namespace std;
typedef long long LL;
LL qmi(LL a,int k,int p)
{
    LL res=1;
    while(k)
    {
        if(k & 1) res=(LL)res*a%p;
        k >>= 1;
        a=(LL)a*a%p;
    }
    return res;
}
int main() {
    int n;
    cin>>n;
    while(n--)
    {
        int a,k,p;
        cin>>a>>k>>p;
        cout<<qmi(a,k,p)<<endl;
    }
    return 0;
}

13.快速幂求逆元(AcWing 876. 快速幂求逆元)

(1)算法思想:

        当遇到a/b mod m 时,为简化计算并避免小数考虑将其转化为a*b逆 ,使得a*b逆 mod m 与 a/b mod m相等,通过恒等变形可以得到,b逆*b mod m为1,通过这个性质就可以计算出b逆。由于本题规定m为质数,根据费马定理(欧拉定理的特例)有b^(m-1) mod m 为1,因此b逆就是b^(m-2)。在通过快速幂就可以求解出来。

(2)代码实现思路:

        同上

(3)代码实现:

#include <iostream>
using namespace std;
typedef long long LL;
LL qmi(LL a,int k,int p)
{
    LL res=1;
    while(k)
    {
        if(k & 1) res=(LL)res*a%p;
        k >>= 1;
        a=(LL)a*a%p;
    }
    return res;
}
int main() {
    int n;
    cin>>n;
    while(n--)
    {
        int a,p;
        cin>>a>>p;
        int res=qmi(a,p-2,p);
        if(a%p) cout<<res<<endl;
        else puts("impossible");
    }
    return 0;
}

14.扩展欧几里得算法(用于裴蜀定理)(AcWing 877. 扩展欧几里得算法)

(1)算法思想:

        正整数a,b,由于对于任何一个整数x,y,有xa+yb为gcd(a,b)的整数倍,因此可以构造存在整数x,y使得xa+yb为a,b的最大公约数同时也是最小的正整数,经验证成立。因此利用欧几里得算法加上x,y参数时只需要将每次递归的x,y系数修改即可。若b为0,则x为1,y=0。若b不为0,由于gcd(a,b,x,y)=gcd(b,b%a,y,x),通过合并系数,可以得到x系数不变,而y变为y-a/b*x,注意x,y不唯一。

(2)代码实现思路:

        同上

(3)代码实现:

#include <iostream>
using namespace std;
int  exgcd(int a,int b,int &x,int &y)
{
    if(!b)
    {
        x=1,y=0;
        return a;
    }
    int d= exgcd(b,a%b,y,x);
    y-=a/b*x;
    return d;
}
int main() {
    int n;
    cin>>n;
    while(n--)
    {
        int a,b,x,y;
        cin>>a>>b;
        exgcd(a,b,x,y);
        cout<<x<<" "<<y<<endl;
    }
    return 0;
}

15.线性同余方程(AcWing 878. 线性同余方程)

(1)算法思想:

        有a*x mod m 等于b,求x,通过恒等变形可以得到ax+my=b,由扩展欧几里得算法可以得到一个x,y使得ax+my为(x,m)的最大公因数d,若b%d不为0,则无解,若有解,此时将等式两边同时乘以b/d,此时a的系数就是答案。

(2)代码实现思路:

        同上

(3)代码实现:

#include <iostream>
using namespace std;
typedef long long LL;
int  exgcd(int a,int b,int &x,int &y)
{
    if(!b)
    {
        x=1,y=0;
        return a;
    }
    int d= exgcd(b,a%b,y,x);
    y-=a/b*x;
    return d;
}
int main() {
    int n;
    cin>>n;
    while(n--)
    {
        int a,b,x,y,m;
        cin>>a>>b>>m;
        int d=exgcd(a,m,x,y);
        if(b%d) puts("impossible");
        else cout<<(LL)x*(b/d)%m<<endl;
    }
    return 0;
}

16.中国剩余定理 (AcWing 204.         表达整数的奇怪方式)

(1)算法思想:

        中国剩余定理:对于n个互质的数m1,m2....mn,若存在一个数x,使得x mod m1 =a1,x mod m2=a2 , ... , x mod mn =an,假设M=m1*m2* ... *mn,Mi=M/mi,Mi的逆表示Mi mod mi 为1,可以通过扩展欧几里得算法来求解Mi的逆,又因为求解x可以通过a1*M1*(M1逆)+a2*M2*(M2逆)+... +an*Mn(Mn逆),(构造证明)。

        对于本题而言,由于没有限制m1,m2...mn互质,因此要对上述定理进行推广,具体步骤见上图,大致是先将前两个方程合并为一个方程,即通过恒等变形,将前两个方程表示x,再进行恒等变形,根据扩展欧几里得算法计算出k1的解,再将结果转换为最小正整数解,再用该解就可以将两个方程合并为同一个方程,再合并式子中多余的形式,最终就转化为最初的x=k*ai+mi的形式,即实现将两个方程合并,以此类推将这个结果与后面的式子依次合并,一共迭代n-1次,最终获得x的通解。通过(x0%b+b)%b来计算x=kb+x0->x mod b = x0。

(2)代码实现思路:

        同上

(3)代码实现:

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL  exgcd(LL a,LL b,LL &x,LL &y)
{
    if(!b)
    {
        x=1,y=0;
        return a;
    }
    LL d= exgcd(b,a%b,y,x);
    y-=a/b*x;
    return d;
}
LL mod(LL a,LL b)
{
    return (a%b+b)%b;//求a mob b=a的最小正整数解
}
int main() {
    LL n,a1,a2,m1,m2;
    cin>>n;
    cin>>a1>>m1;
    for(int i=1;i<=n-1;i++)
    {
        LL k1,k2;
        cin>>a2>>m2;
        LL d=exgcd(a1,a2,k1,k2);
        if((m1-m2)%d)
        {
            puts("-1");
            return 0;
        }
        k1=mod (k1*(m2-m1)/d,abs(a2/d));
        m1=m1+k1*a1;
        a1=abs(a1*a2/d);
    }
    cout<<m1;
    return 0;
}
//y总代码
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL exgcd(LL a,LL b,LL &x,LL &y)
{
    if(!b)
    {
        x=1,y=0;
        return a;
    }
    LL d=exgcd(b,a%b,y,x);
    y-=a/b*x;
    return d;
}
int main()
{
    int n;
    cin>>n;
    LL a1,m1;
    cin>>a1>>m1;
    bool flag=true;
    for(int i=0;i<n-1;i++)
    {
        LL a2,m2,k1,k2;
        cin>>a2>>m2;
        LL d=exgcd(a1,a2,k1,k2);
        if((m2-m1)%d)
        {
            flag=false;
            break;
        }
        LL t=a2/d;
        k1*=(m2-m1)/d;
        k1=(k1%t+t)%t;
        m1=a1*k1+m1;
        a1=abs(a1/d*a2);
    }
    if(flag) cout<<(m1%a1+a1)%a1;
    else cout<<-1;
    return 0;
}

(二).高斯消元

1.高斯消元求解方程组 (AcWing883. 高斯消元解线性方程组)

(1)算法思想:

        主要分为两步:一、将方程化为上三角形式,且每行第一个非零系数为1,该部分可分为以下步骤解决:1.枚举每一列   2.找到该列系数最大的行   3.将该行与第一行交换   4.将该行第一个系数变为1   5.将下面所有行的该列系数利用第一行该列系数变为0,最终就化为了上三角形式;二、从最后一行开始向上依次消末尾的系数,如倒数第二行消掉最后一个变量系数,倒数第三行消掉后两个变量系数,...,第一行只保留第一个变量系数,最终每一行i的常数系数就是该变量xi的解。若最后几行为0=0则说明方程不具有唯一解,若存在某一行使得等式左侧为0右侧为非零常数系数,则说明无解,其他情况为无穷解。

(2)代码实现思路:

        首先读入每一行每一列的系数,用一个二维数组存储。

        gauss函数:枚举每一列c,同时初始行数r为0,然后通过枚举该行以后得每一行找到该列c的最大值,令该行为t,若该行的最大值为0,则跳过该次循环(说明方程不具有唯一解),再交换t行与r行的每一列元素,实现交换两行,此时第r行元素就是选出的列最大的行,将其第c列元素变为1,即对该行每一个元素除以a[r][c](注意从最后一列开始改变,因为需要用到最前面的列),然后再利用该行将下面所有行的c列变为0,由每一行的第c列是第r行的a[i][c]倍,可以得到,将每一行的每一个元素减去a[i][c]*a[r][j]即可(注意从最后一列开始改变),最后在本次枚举第c列结束之前将r移动到下一行,结束本次枚举。最终若r<n,则说明枚举中出现跳过循环,即说明该方程不具有唯一解,则判断r行之后的常数系数是否为0,若不为0,则返回无解,否则返回无穷解。若r=n,则说明具有唯一解,然后从最后一行开始向上修正系数,即初始行i为n-1,列j从i+1到n-1,将每一行的常数系数依次减去当前列j的该行元素乘以j行所在的常数系数,最终到最后一行。即减去a*[i][j]*a[j][n]。

(3)代码实现:

#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 110;
const double eps = 1e-6;
int n;
double a[N][N];
int gauss()
{
    int c,r;
    for(c=0,r=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 j=c;j<=n;j++)
            swap(a[r][j],a[t][j]);
        for(int j=n;j>=c;j--)
            a[r][j]/=a[r][c];
        for(int i=r+1;i<n;i++)
            if(fabs(a[i][c])>eps) //减少修改次数,删去亦可
                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;i>=0;i--)
        for(int j=i+1;j<=n-1;j++)
            a[i][n]-=a[j][n]*a[i][j];
    return 0;
}
int main() {
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<=n;j++)
            cin>>a[i][j];
    int t=gauss();
    if(t==0)
        for(int i=0;i<n;i++)
            printf("%.2f\n",a[i][n]);
    else if(t==1) puts("Infinite group solutions");
    else puts("No solutions");
    return 0;
}

2.AcWing884. 高斯消元解异或线性方程组

(1)算法思想:

       与上题思路相同。不再将本行进行除以第一个非0元素的操作。

(2)代码实现思路:

       无。

(3)代码实现:

#include <iostream>
#include <algorithm>
using namespace std;
const int N=110;
int a[N][N],n;
int gauss()
{
    int c,r;
    for(c=0,r=0;c<n;c++)
    {
        int t=r;
        for(int i=r;i<n;i++)
            if(a[i][c])
            {
                t=i;
                break;
            }
        if(!a[t][c]) continue;
        for(int j=c;j<=n;j++)
            swap(a[t][j],a[r][j]);
        for(int i=r+1;i<n;i++)
            if(a[i][c])
                for(int j=c;j<=n;j++)
                    a[i][j]^=a[r][j];
        r++;
    }
    if(r<n)
    {
        for(int i=r;i<n;i++)
            if(a[i][n]) 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;
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<=n;j++)
            cin>>a[i][j];
    int res=gauss();
    if(res==0)
    {
        for(int i=0;i<n;i++)
            cout<<a[i][n]<<endl;
    }
    else if(res==1)
        puts("Infinite group solutions");
    else 
        puts("No solutions");
    return 0;
}

(三).组合计数

1.求组合数方法一(询问10000次,数字范围2000)(AcWing 885. 求组合数 I)

(1)算法思想:

        若采用简单的求阶乘方式,则会超时,因此可以先计算出所有的组合数的值。通过公式C_{a}^{b}=C_{a-1}^{b}+C_{a-1}^{b-1}可以递推出所有组合数的值,即先对每一个C_{i}^{0}赋值为1,对于上面的数b大于下面的数a的情况,不赋值就默认为0,通过该递推公式就可以得到所有的组合数。

(2)代码实现思路:

        同上;

(3)代码实现:

#include <iostream>
using namespace std;
const int N = 2010,mod=1e9+7;
int c[N][N];
void init()
{
    for(int i=0;i<N;i++)
        for(int j=0;j<=i;j++)
            if(!j) c[i][j]=1;
            else c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;
}
int main() {
    int n;
    cin>>n;
    init();
    while(n--)
    {
        int a,b;
        cin>>a>>b;
        cout<<c[a][b]<<endl;
    }
    return 0;
}

2.求组合数方法二(询问10000次,数字范围100000)(AcWing 886. 求组合数 II)

(1)算法思想:

        若该题采用上面的方式循环总次数大概在1e10次左右,因此会超时。因此可以通过原始的公式^{C_{a}^{b}}=\tfrac{a!}{(a-b)!b!}先计算出每一个阶乘的值,再运算,又因为对结果取模,因此等价于对分子取模再乘以分母的逆元,由于该题的mod为一个质数,因此可以用快速幂来计算逆元,最终组合数可以通过计算fact[a]*infact[b]*infact[b-a]得出,fact[a]表示求a的阶乘取模的结果,infact[b]表示求b的阶乘的逆元取模的结果(注意b!的逆可以通过b逆乘以(b-1)!逆来求)。

(2)代码实现思路:

        同上;

(3)代码实现:

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N =1e4+10,mod=1e9+7;
LL fact[N],infact[N];
LL qmi(LL a,LL k,LL p)
{
    LL res=1;
    while(k)
    {
        if(k&1) res=(LL)res*a%p;
        k>>=1;
        a=(LL)a*a%p;
    }
    return res;
}
int main()
{
    fact[0]=infact[0]=1;
    for(int i=1;i<N;i++)
    {
        fact[i]=fact[i-1]*i%mod;
        infact[i]=infact[i-1]*qmi(i,mod-2,mod)%mod;
    }
    int n;
    cin>>n;
    while(n--)
    {
        int a,b;
        cin>>a>>b;
        cout<<(LL)fact[a]*infact[b]%mod*infact[a-b]%mod<<endl;
    }
}

3.求组合数方法三(询问20次,数字范围1e18,Lucas定理)(AcWing 887. 求组合数 III)

(1)算法思想:

        由于该题的数据范围过大,因此考虑用Lucas定理计算。公式见下图(引自算法基础课)

lucas函数:函数的参数为a,b,若a<p且b<p则说明a,b足够小可以通过组合数函数C(a,b)(类似上题)直接计算,否则返回C(a%p,b%p)*lucas(a/p,b/p)(因为a,b除p可能依然很大,当足够小时就像上述情况直接返回组合数函数)

C函数:与上题不同的是本题不需要枚举所有组合数,而是通过定义计算,即C_{a}^{b}=\tfrac{a*(a-1)*...*(a-b+1)}{b!},即枚举b次,分子从a开始依次递减,分母从1开始依次递增到b结束循环,分母依旧采用快速幂计算逆元的方式。

(2)代码实现思路:

        同上;

(3)代码实现:

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
int p;
int qmi(int a,int k)
{
    int res=1;
    while(k)
    {
        if(k&1) res=(LL)res*a%p;
        k>>=1;
        a=(LL)a*a%p;
    }
    return res;
}
int C(int a,int b)
{
    int  res=1;
    for(LL i=1,j=a;i<=b;i++,j--)
    {
        res=(LL)res*j%p;
        res=(LL)res*qmi(i,p-2)%p;
    }
    return res;
}
int lucas(LL a,LL 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 n;
    cin>>n;
    while(n--)
    {
        LL a,b;
        cin>>a>>b>>p;
        cout<<lucas(a,b)<<endl;
    }
}

4.求组合数方法四(结果不取模,因此需要用到大整数乘法)(AcWing 888. 求组合数 IV)

(1)算法思想:

        考虑公式^{C_{a}^{b}}=\tfrac{a!}{(b-a)!b!},根据算数基本定理,该数可以唯一表示成质因数分解相乘的形式,因此只需要计算出每一个质因数的幂次,最后用大整数乘法计算即可。

(2)代码实现思路:

        由于本题中最大的数为a,因此只需要利用线性筛算法筛选出a之前所有质因子即可,之后再对于每一个质因子,求出对应的幂次,对于a!中包含质因子p的幂次可以通过公式\left \lfloor \frac{a}{p} \right \rfloor+\left \lfloor \frac{a}{p^{2}} \right \rfloor+...+\left \lfloor \frac{a}{p^{n}} \right \rfloor(表示能被p除1次,除2次,除....)直到项为0为止,因此对于质因子p而言,组合数^{C_{a}^{b}}中包含p的幂次就是a!通过公式计算出来的值减去(b-a)!和b!通过公式计算出来的值。进而获得a之前每一个质因子的幂次,再通过大整数乘法对所有质因子枚举一遍,每次枚举都对质因子枚举它的幂数次。

(3)代码实现:

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N =5010;
int primes[N],cnt;
bool st[N];
void get_prime(int x)
{
    for(int i=2;i<=x;i++)
    {
        if(!st[i]) primes[cnt++]=i;
        for(int j=0;primes[j]<=x/i;j++)
        {
            st[primes[j]*i]=true;
            if(i%primes[j]==0) break;
        }
    }
}
int get(int n,int p)
{
    int res=0;
    while(n)
    {
        res=res+n/p;
        n=n/p;
    }
    return res;
}
vector<int> mul(vector<int> &res,int b)
{
    vector<int> c;
    int t=0;
    for(int i=0;i<res.size()||t;i++)
    {
        if(i<res.size()) t+=res[i]*b;
        c.push_back(t%10);
        t/=10;
    }
    return c;
}
int main()
{
    int a,b;
    cin>>a>>b;
    get_prime(a);
    int sum[N];
    for(int i=0;i<cnt;i++)
    {
        int p=primes[i];
        sum[i]=get(a,p)-get(b,p)-get(a-b,p);
    }
    vector<int> res;
    res.push_back(1);
    for(int i=0;i<cnt;i++)
        for(int j=0;j<sum[i];j++)
            res=mul(res,primes[i]);
    for(int i=res.size()-1;i>=0;i--)
        cout<<res[i];
}

5.卡特兰数(AcWing 889. 满足条件的01序列)

(1)算法思想:

        该题可以通过将排列问题转化为路径问题,最终变为求解卡特兰数。该题要求转化为路径如下图

        假设有6个0,6个1,0表示向右,1表示向上,则所有路径都是从原点出发到(6,6),题目要求前缀中0的个数大于1的个数就意味着路径在红色线之下的个数,这个个数可以通过总路径个数减去红色线之上路径的个数,求解红色线之上的路径个数可以看作当路径第一次穿过红线时,将之后的路径与红线做对称,不改变路径的个数,由于肯定会穿过红线,因此终点做对称一定在(5,7),则可以将该问题的求解转化为计算原点到(5,7)的路径个数,因此求解符合题意得个数就可以通过(0,0)到(6,6)的路径个数,即C_{12}^{6}\textrm{}减去(0,0)到(5,7)的路径个数,即C_{12}^{5}\textrm{}。推广至n个0,n个1的情况,结果就为\frac{C_{2n}^{n}\textrm{}}{n+1},即卡特兰数。

(2)代码实现思路:

        与计算组合数方法3类似

(3)代码实现:

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int mod=1e9+7;
int qmi(int a,int k,int p)
{
    int res=1;
    while(k)
    {
        if(k&1) res=(LL)res*a%p;
        a=(LL)a*a%p;
        k>>=1;
    }
    return res;
}
int main() {
    int n;
    cin>>n;
    int a=2*n,b=n;
    int res=1;
    for(int i=a;i>a-b;i--) res=(LL)res*i%mod;
    for(int i=1;i<=b;i++) res=(LL)res*qmi(i,mod-2,mod)%mod;
    res=(LL)res*qmi(n+1,mod-2,mod)%mod;
    cout<<res;
    return 0;
}

(四).容斥原理

容斥原理简单介绍:

        先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复,这种计数的方法称为容斥原理。

        以A,B,C三个集合为例,求出三个集合中包含的元素个数,可以通过韦恩图得到A∪B∪C = A+B+C - A∩B - B∩C - C∩A + A∩B∩C。通过数学归纳法可以证明,对于求A1,A2,...,Am集合中包含的元素个数,可以通过下面的公式来计算

总项数为C_{m}^{1}\textrm{}+C_{m}^{2}\textrm{}+...+C_{m}^{m}\textrm{}=2^{m}-1

1.AcWing 890.能被整除的数

(1)算法思想:

       对于本题而言,计算1~n中被m个质数中至少一个整除的次数,可以将集合Ai看作质数pi整除1~n中的数的个数,即\left \lfloor n/pi \right \rfloor,因此答案就可以通过容斥原理的公式来求解,对于集合的交集中元素的个数,由于pi互质,因此Ai\capAj可以通过pi*pj整除1~n中的数的个数表示,即\left \lfloor \frac{n}{pi*pj} \right \rfloor。整体时间复杂度为O(2^{m}m)。

(2)代码实现思路:

        用位运算来枚举集合的选法,由于总共的选法为2^{m}-1次,因此从1 枚举到2^{m}-1即可,用二进制表示每一种选法,再从1枚举到m位,对用t表示选中集合的质数的乘积,cnt表示选中集合的而个数,通过i>>j&1来判断该位置是否为1,若为1,则cnt++,同时判断t是否大于n,若大于n,说明该选法的符合题意的1~n的个数为0,否则若选择奇数个集合则加上n/t,否则减去n/t。

(3)代码实现:

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N=20;
int p[N];
int n,m;
int main() {
    cin>>n>>m;
    for(int i=0;i<m;i++) cin>>p[i];
    int res=0;
    for(int i=1;i<1<<m;i++)
    {
        int t=1,cnt=0;
        for(int j=0;j<m;j++)
            if(i>>j&1)
            {
                cnt++;
                if((LL)t*p[j]>n)
                {
                    t=-1;
                    break;
                }
                t=(LL)t*p[j];
            }
        if(t!=-1)
        {
            if(cnt%2) res+=n/t;
            else res-=n/t;
        }
    }
    cout<<res;
    return 0;
}

(五).简单博弈论

1.Nim游戏(AcWing891. Nim游戏)

(1)算法思想:

        先给出结论,设每堆石子的个数a1,a2,...,an,若a1\oplusa2\oplusa3\oplus....\oplusan=0,(\oplus表示异或)则说明先手必败,否则说明先手必胜,证明可以分为三种情况,第一种情况先得出最终态的结论,即0\oplus0\oplus...\oplus0=0,即当达到该状态时,已经是无处可动的状态,因此为先手必败,第二种情况是对于a1\oplusa2\oplusa3\oplus....\oplusan=x(x不等于0)来说,总存在一种取法,使得下一个状态等式右侧为0(具体方法略),第三种情况是对于a1\oplusa2\oplusa3\oplus....\oplusan=0,无论采取什么取法,下一个状态等式右侧都不为0(具体证明略)。因此形成当先手遇到第二种情况,则可以将下一个状态变为情况三,因此对手遇到的就是情况三,而对手只能将下一状态转换成先手方遇到的第二种情况,依次递推,最终先手方会取走所有石头,使下一状态变为情况一,使得对方达到必败的状态,因此情况二为先手必胜状态;当先手遇到第三种状态,同理可得,最终先手方会遇到情况一,达到必败的状态,因此情况三为先手必输状态。

(2)代码实现思路:

        若a1\oplusa2\oplusa3\oplus....\oplusan=0,则说明先手必败,否则说明先手必胜。即判断异或值是否为0。

(3)代码实现:

#include <iostream>
#include <algorithm>
using namespace std;
int main() {
    int n;
    cin>>n;
    int res=0;
    while(n--)
    {
        int a;
        cin>>a;
        res= res^a;
    }
    if(res) puts("Yes");
    else puts("No");
    return 0;
}

2.AcWing892. 台阶-Nim游戏

(1)算法思想:

       由于离地面最近的台阶序号为1,因此当一方拿取偶数层石头时,可以将下一层拿取同样多的石头保持状态不变,所以只需要考虑奇数层。当若a1\oplusa3\oplusa5\oplus....\oplusa(2n+1)=x,则先手必胜,否则先手必败。

(2)代码实现思路:

       同上。

(3)代码实现:

#include <iostream>
using namespace std;
int main()
{
    int n;
    cin>>n;
    int res=0;
    for(int i=1;i<=n;i++)
    {
        int a;
        cin>>a;
        if(i%2) res^=a;
    }
    if(res) puts("Yes");
    else puts("No");
    return 0;
}

3.Sg函数(AcWing 893.集合-Nim游戏)

        Sg函数简介:

        mex运算:对集合S进行mex运算,得到的结果是不包含在S的最小自然数。如mex{0,1,3}=2

        Sg函数:在有向图游戏中,每一个状态点的Sg函数值就是对它的直接后继点的Sg函数值进行mex运算,规定终点的Sg函数值为0,对于单个图的Sg函数值,定义为起点的值。

(1)算法思想:

        对于每一堆而言就是一个图,初始态的堆的数量就是起点x,因为sg(x)若不为0,则证明他的后继状态一定存在0,因此使用一种操作使得下一个状态的sg函数为0,即可维持必胜。若有一堆的数量为10,取法为2,5,则他的状态图如下所示。

        计算出每一堆的起点的Sg函数值,再通过上题的思路判断相异或的值是否为0,若为0,则必败,否则必胜。

        Sg函数实现:

        首先判断该状态(当前堆中包含石头的个数为x)是否被计算过,若没被计算过,则向下进行。定义一个哈希表S,用来记录该状态的后继状态的sg函数值。然后再枚举所有供选择的取石头个数,若该状态大于取法,则可以进入下一个状态,将下一个状态的sg函数值加入S中。最后通过Mex运算计算出该状态的St值,即i从自然数0开始枚举,若S中不存在i,则返回该状态的St值就是i。

(2)代码实现思路:

        设置一个f[]数组,记录每一个状态的sg函数值,初始化f为-1。

        Sg函数:若f不为-1,则说明已被计算过,直接返回。设置一个哈希表S,记录该状态X的所有后继状态的sg值。枚举所有取法s[i],若当前状态的石头数大于取法中的石头数,则说明该取法可行,将下一个状态,即X-s[i]的sg函数值加入S中。在对S进行mex运算,即从0开始枚举所有自然数i,若集合S中没有i,则该状态X的sg函数值就为i。

        计算出每一堆的Sg函数,再进行上题的类似操作,将每一堆的sg函数异或,若结果为0,则必败,否则必胜。

(3)代码实现:

#include <iostream>
#include <algorithm>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N=110,M=10010;
int n,m;
int s[N],f[N];
int sg(int x)
{
    if(f[x]!=-1) return f[x];
    unordered_set<int> S;
    int sum;
    for(int i=0;i<m;i++)
    {
        sum=s[i];
        if(x>=sum) S.insert(sg(x-sum));
    }
    for(int i=0;;i++)
        if(!S.count(i))
            return f[x]=i;
}
int main()
{
    cin>>m;
    for(int i=0;i<m;i++) cin>>s[i];
    cin>>n;
    memset(f,-1,sizeof f);
    int res=0;
    while(n--)
    {
        int x;
        cin>>x;
        res=res^sg(x);
    }
    if(res) puts("Yes");
    else puts("No");
}

4.AcWing894. 拆分-Nim游戏

(1)算法思想:

        该游戏一定会结束,因为每次拿走一堆后,放入的堆虽然比原来多,但是每一堆的数量变小,一直往下进行,那么迟早会遇到所有堆都为1,此时只能拿走,而无法放回。

       根据sg(x1,x2)=sg(x1)^sg(x2)即可求出任意一堆的sg函数(方法如上),最后将所有堆的sg函数异或看是否为0,若为0,则必败,否则必胜。

(2)代码实现思路:

       同上

(3)代码实现:

#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N=110;
int f[N];
int sg(int x)
{
    if(f[x]!=-1) return f[x];
    unordered_set<int> S;
    for(int i=0;i<x;i++)
        for(int j=0;j<=i;j++)//实现放入两堆,由于放入a,b和放入b,a一样,所以j不用枚举到x
            S.insert(sg(i)^sg(j));
    for(int i=0;;i++)
        if(!S.count(i))
            return f[x]=i;
}
int main()
{
    int n;
    cin>>n;
    memset(f,-1,sizeof f);
    int res=0;
    while(n--)
    {
        int x;
        cin>>x;
        res^=sg(x);
    }
    if(res) puts("Yes");
    else puts("No");
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值