数论的学习及刷题

数论

1.什么是数论

  • 数论是研究整数的性质、结构和关系的数学分支。它探讨了关于整数的基本性质,如素数因子分解整数的奇偶性

2.常用的数论定理

贝祖定理

  • 当两个 n,m 是互质的,则 n*x + m*y 不能组成的最大正整数为 (n-1)(m-1) - 1

算术基本定理

(这个也叫因式分解定理,但这是一条公理,它数论系统构成的基本定理,是数论的基础

  • 所有的正整数 N 一定可以分解问若干个质因子乘积。
    N = p 1 α 1 . p 2 α 2 . . . p k α k ( p i 为质数, α i > 0 ) N=p_1^{α_1}.p_2^{α_2}...p_k^{α_k}(p_i为质数,α_i>0) N=p1α1.p2α2...pkαk(pi为质数,αi>0)

3.简单数论算法

最大公约数

  • 已知两数 a,b 求它们的最大公约数 (欧几里得算法)

    #include <iostream>
    using namespace std;
    
    int gcd(int a, int b)
    {
        return b ? gcd(b, a%b) : a;
    }
    

判断质数

  • 代码:

    #include <iostream>
    using namespace std;
    
    bool isprime(int x)
    {
        if(x < 2) return false;//小于2的直接返回false
        for(int i = 2; i*i<=x; i++)//从2开始遍历到sqrt(x),查看是否有x能被i整除
            if(x%i==0)
                return false;
        return true;
    }
    

求1~n以内的质数

  • 给出一个数n,求1~n内素数的个数

  • 代码:

    基本做法 O( n 2 n^2 n2)

    #include <iostream>
    using namespace std;
    
    const int N = 1000010;
    
    int primes[N],cnt;
    
    void get_primes(int n)
    {
        for(int i = 2; i <= n; i++)
        {
            bool flag = false
            for(int j = 2; j*j<=i; j++)
            {
                if(i%j==0) flag = true;
            }
            if(flag==false) prime[cnt++] = i;
        }
    }
    

    线性筛法 O(n)

    (求1~n中所有质数,以及每个数的最小质因子)

    #include <iostream>
    using namespace std;
    const int N = 1000010;
    
    //primes[N]用于储存每一个质数,cnt用于统计质数个数,st[N]用于标记从2到n的每一个数字,是质数的话赋值为0,是合数(也就是质数的倍数)的话则赋值为1。st[i]==true表示i被筛过了是合数;st[i]==false表示i没被筛过是质数。
    int primes[N], cnt;
    bool st[N];
    
    void get_primes(int n)
    {
        for(int i = 2; i <= n; i++ )
        {
            if(!st[i])//st[i]为零说明当前i还没被筛过,因此为质数,记录下来
                primes[cnt++] = i;
            for(int j = 0; primes[j] * i <= n; j++)
            {
                st[primes[j] * i] = true;//筛掉的一定是合数
                if(i % primes[j] == 0) break;//为了避免多次筛同一个合数
            }
        }
    }
    

    线性筛法的性质

    • 每次筛掉的一定是合数,且一定是用 其最小质因子

    • 每次筛 primes[j] * iprimes[j] 一定不大于 i 的最小质因子

    • 所有的合数一定会被筛掉。原因如下:

      在这里插入图片描述

    • 时间复杂度为 O(n) ,原因是每个合数都是用它自身最小的质数进行筛除,每个合数的最小质数只有一个,因此只会被筛一次,复杂度是 n

分解质因子

  • 根据质数定理,每一个实数都可以由a个质数乘积得到,假如给定一个数n,要求你返回他的所有的质因子

  • 代码:

    #include <iostream>
    #include <vector>
    using namespace std; 
    vector<int> v;//用于存储n的所有的
    
    void divide(int n)
    {
        for(int i = 2; i*i <= n; i++)
        {
            while(n%i==0)
            {
                n /= i;//保证以后遍历的每一个i都是质数
            }
            v.push_back(i); 
        }
        if(n>1) v.push_back(n); 
    }
    

约数的个数

  • 定理:每一个数 N 的约数个数等于,每个质因子的个数加一再累乘

  • 算法分析:

    • 由因式分解定理得,N 可以拆分成多个质因子,对于每个约数来讲,都是由质因子相乘而得来的,举个例子如下图:

      在这里插入图片描述

      由上图不难看出,要求一个数的约数,只需要其质因子的排列,只要知道每个质因子的个数,我们便可以知道约数的个数总和。

  • 代码:

    #include <iostream>
    #include <unordered_map>
    using namespace std;
    typedef long long ll;
    //由于累乘后的数可能会很大,所以定义一个longlong
    
    int main()
    {
        ll ans=1;
        int n ;cin >> n;
        unordered_map<int,int> hash;
        for(int i =2;i*i<=n;i++)
        {
            while(n%i==0)
            {
                hash[i]++;
                n /= i;
            }
        }
        if(n>1) hash[n]++;
        
        for(auto &i : hash) ans *= i.second+1; 
        cout << ans;
        return 0;
    }
    

约数之和

  • 数n的约数之和的公式为:

    image-20230407151943163

    Pk表示n的第k个质因子

  • 算法分析:

    • 由上述的约数个数的算法,我们可以很轻松的得到每一项约数用质因子次方的乘积的表示,因此我们将所有约数加起来,如下图所示:

      在这里插入图片描述

      不难看出规律就是上述的公式。

  • 代码:

    #include <iostream>
    #include <unordered_map>
    using namespace std;
    typedef long long ll;
    const int N=1000010;
    
    
    int main()
    {
        unordered_map<int, int> hash;
        int n;cin >> n;
        for(int i =2;i<=n/i;i++)
        {
            while(n%i==0)
            {
                hash[i]++;
                n/=i;
            }
        }
        if(n>1) hash[n]++;
        
        ll res =1;
        for(auto &i : hash)
        {
            int p=i.first;//p代表的是
            int k=i.second;
            int sum=1;
            while(k--) 
            {
                sum+=p;
                p*=p;
            }
            res*=sum;
        }
        cout << res;
        return 0}
    

4.例题

做题思路:(对于不知道对应的数学定理的题型)

1、尽力分析,缩小暴搜数据范围

2、暴力枚举打表找规律

【Acwing 1205.买不到的数目】 (数论)

  • 分析

    • 根据题意,我们可以转化成以下意思,已知 n,m ,求解最大不能组合的数字 i ,使得大于 i 的数字 num 满足 num = n*x + m*y 有正整数解。(题目输入的 n,m 保证数据一定有解)

    • 首先我们可以分析一下 n,m 之间的关系。对于两个正整数 n,m ,它们之间的关系无非就是互质非互质。对于非互质的情况,有 gcd(n,m) = d > 1 ,则 n,m 组合出来的数字 num 一定是 d 的倍数,不满足题意,因此 n,m 之间的关系只能是互质。(互质的 n,m 一定满足题意吗,这个是一定的,具体证明自行上网搜索)

    • 我们假设由 n,m 组合的最大正整数 i 一定在 1~1000 以内,因此我们可以通过对 i1~1000 进行枚举,再对 x 从0 开始枚举,然后又接着对 y 从 0 开始枚举,最后判断一下 if(i == n*x + m*y) ,若枚举完 xy i == n*x + m*y 为false,说明当前的所遍历到的 i 不能用 nm 组合出来,因此要记录当前的 i ,接着往下遍历。

    • 我们可以通过以上思路对 nm 取较小的数值时的情况,求出对应的不能组合最大数i,代码如下:

      #include <iostream>
      using namespace std;
      
      int main()
      {
          int n, m, res = 0;
          cin >> n >> m;
          for(int i = 1; i <= 1000; i++)
          {
              if(i<n && i<m) res = i;
              else
              {
                  bool flag = false;//该标志用于判断当前数字i是否能用n,m组合出来
                  for(int j = 0; j * n <= i; j++)
                  {
                      for(int k = 0; j*n + k*m <= i; k++)
                      {
                          if(j*n + k*m == i) flag = true;
                      }
                  }
                  if(flag == false) res = i;
              }
          }
          cout << res;
          return 0;
      }
      

      具体的打表情况如下:

      在这里插入图片描述

    • 由上表不难看出一些规律:当 n=3 时, res = 2*m - 3;当 n=4 时,有 res = 3*m - 4;因此总结下来就是 res = (n-1)*m - n,即 res = (n-1)(m-1) - 1

    • 实际上,对于两个互质的 正整数 n,m ,它们的最大不能组合数为 (n-1)*(m-1) - 1

    • 对于此类题型,在不知道题目吗蕴含的定理时,我们可以通过以上打表的方式总结出规律,从而猜出定理的内容。

  • 设计思路

    • 直接输出 (n-1)(m-1) - 1
  • 代码

    • #include <iostream>
      using namespace std;
      
      int main()
      {
          int n, m;
          cin >> n >> m;
          cout << (n-1)*(m-1) -1;
          return 0;
      }
      

【Acwing 1211.蚂蚁感冒】 (数学)

  • 分析

    • 根据题意,本题要求返回总的感冒蚂蚁数目。(这是一道脑筋急转弯的题目)
    • 首先就是蚂蚁相碰的问题,根据题目意思,两个蚂蚁相碰就会使两个蚂蚁同时调转方向,所有的蚂蚁速度都相同。其实这个过程也可以等效为两个蚂蚁相碰后各自擦肩而过。这样等效后,每只蚂蚁的运动方向就是固定的了,不会变来变去,方便后续分析。
    • 然后题意规定,第一个输入的蚂蚁是感冒的,与该蚂蚁相遇的蚂蚁都会被感染。感冒和没感冒的蚂蚁相碰后,二者双双掉头,二者皆感冒。这种情况与两个蚂蚁相碰后各自擦肩而过,且相互感冒并不冲突,因此等效仍然成立。
    • 我们以第一个蚂蚁作为分界线,分析左右两边的蚂蚁哪些会被感染。
    • 当第一个蚂蚁向右运动时,在其右边向右运动的蚂蚁一定不会被感染,右边向左运动的一定会感染;在其左边向左运动的蚂蚁一定不会被感染,左边向右运动的可能会感染:1.右边向左运动的蚂蚁数目为零,不感染。2.右边向左运动的蚂蚁数目不为零,一定感染。
    • 同理,当第一个蚂蚁向左运动时,可以同上分析。
    • 因此我们要求感染的蚂蚁总数,只需统计第一个蚂蚁右边向左运动的蚂蚁个数和左边向右运动的蚂蚁个数。
  • 设计思路

    • 首先定义一个数组 arr[N] 用于存储每个蚂蚁的位置和方向。
    • 接着定义两个变量 left rightleft 用于统计第一个蚂蚁左边向右移动的蚂蚁个数, right 用于统计第一个蚂蚁右边向左移动的蚂蚁个数。
    • 当第一个蚂蚁运动的方向向右时,若右边向左的蚂蚁个数为零,则总的感染个数为 1 ;若右边向左的蚂蚁个数不为零,则总的感染个数为 left + right + 1
    • 当第一个蚂蚁运动的方向向左时,若左边向右的蚂蚁个数为零,则总的感染个数为 1 ;若左边向右的蚂蚁个数不为零,则总的感染个数为 left + right + 1
  • 代码

    • #include <iostream>
      #include <cstdio>
      using namespace std;
      
      const int N = 60;
      
      int arr[N];
      int n;
      
      int main()
      {
          cin >> n;
          for(int i = 0; i < n; i++) cin >> arr[i];
          
          int left = 0, right = 0;
          for(int i = 1; i < n; i++)
          {
              if(abs(arr[0]) > abs(arr[i]) && arr[i] > 0) 
                  left++;
              if(abs(arr[0]) < abs(arr[i]) && arr[i] < 0)
                  right++;
          }
          
          if((arr[0] > 0 && right == 0) || (arr[0] < 0 && left == 0)) cout << "1" << endl;
          else cout << left + right + 1 << endl;
          return 0;
      }
      

【Acwing 1216.饮料换购】 (数学)

  • 分析

    • 给定 n 个瓶饮料,规定3个瓶盖换一瓶饮料,问最多可以喝多少瓶饮料。
    • 我们可以先模拟分析一下。第一次喝完全部饮料,得到 n 个瓶盖,此时可以换 n/3 瓶饮料,剩 n%3 个瓶盖;第二次喝完全部饮料,得到 n/3 + n%3 个瓶盖,此时可以换 (n/3 + n%3)/3 瓶饮料,剩 (n/3 + n%3)%3 个瓶盖;以此类推,直到瓶盖个数小于3,统计所有的饮料个数。
  • 设计思路

    • 定义个答案变量 res 用于存储总的饮料瓶数。用 n 表示瓶盖的个数。
    • 进入循环体,不断更新 res 的值,同时也不断更新 n 的值,直到 n 小于 3 时,结束循环,输出答案 res
  • 代码

    • #include <iostream>
      using namespace std;
      
      int n;
      
      int main()
      {
          cin >> n;
          int res = n;
          while(n >= 3)
          {
              res += n/3;
              n = n/3 + n%3;
          }
          cout << res << endl;
          return 0;
      }
      

【Acwing 1246.等差数列】 (数论)

  • 分析

    • 根据题意,要求 N 个整数的最短的等差数列有几项。
    • 由于等差数列的性质,对于每一项 An 一定有 An = A1 + (n-1)d ,因此首项和末项之间的项数等于 (末项 - 首项)/d + 1,由于输入的数组已确定,因此要使总的项数最小,只需公差 d 最大。
    • 我们知道要满足等差数列,每一项减去第一项的差 s[i] 一定是公差 d 的倍数,所以公差 d 最大也就只能是 s[i] 的最大公约数
    • 因此本题的关键在于求解所有 s[i]最大公约数
  • 设计思路

    • 首先定义个数组 a[N] 用于存储输入的等差数列。
    • 对数组进行排序(按照等差数组大小排序)
    • 书写欧几里算法,设计求最大公约数的函数
    • 初始化公差 d 为零,依次求解 gcd(d, a[i] - a[0]) 再重新赋值给 d
    • 最后判断 d ,若 d 为零,则输出元素的总个数 n ;若 d 不为零,则输出 (a[n-1] - a[0])/d + 1
  • 代码

    • #include<iostream>
      #include <algorithm>
      using namespace std;
      
      const int N = 100010;
      
      int n, a[N];
      
      int gcd(int a, int b)
      {
          return b ? gcd(b, a%b) : a;
      }
      
      int main()
      {
          cin >> n;
          for(int i = 0; i < n; i++) scanf("%d", &a[i]);
          sort(a, a+n);
          
          for(int i = 1; i < n; i++)
              d = gcd(d, a[i-1] - a[0]);
          
          if(!d) printf("%d", n);
          else printf("%d", (a[n-1]-a[0])/d + 1);
          return 0;
      }
      

【Acwing 1295.X的因子链】 (数论)

  • 分析

    • 根据题意,我们要 X 最长的因子链的长度,同时返回符合该长度的因子方案数。

    • 根据要求,我们要分析三个问题:1、如何使因子链最长2、如何求出最大长度3、符合该长度的方案数是多少

    • 首先是如何使因子链最长。根据题意,我们要从 X 的所有因子找出一组数据构成数列 a[i] ,若 a[i] 满足 a i + 1 _{i+1} i+1 / a i _{i} i = k (k是大于2的整数),即后一项是前一项的倍数且数列递增,则该数列被称为 x 的一条因子链。

      要使链子最长,我们就要 k 的值尽量小,这样放进来的因子个数就多了。由于 a i + 1 _{i+1} i+1 和 a i _{i} i 都是 X 的因子,因此 k 一定是 X 的约数。当 k 为合数的时候,k 可以继续拆分使得因子链增长,因此 k 只能是质数。总结下来,要使链子最长,k要尽可能多,k 尽可能多,则 k 只能是 X 的质因子。

    • 接着就是求最长长度。由因式分解定理我们知道,X = p 1 α 1 p_1^{α_1} p1α1 x p 2 α 2 p_2^{α_2} p2α2 p k α k p_k^{α_k} pkαk ( p i p_i pi为质数, α i α_i αi>0),由于 k 只能取 p i p_i pi p i p_i pi的总个数为 total = α 1 {α_1} α1 + α 2 {α_2} α2 + … + α k {α_k} αk,所以 k 要取最大值时,等于 total

    • 最后就是求所有方案数。首先我们可以画一个映射表如下:

      在这里插入图片描述

      因子链每个元素的大小取决于 k 的元素的排列组合,已知在 [k] p 1 {p_1} p1 的个数有 α 1 {α_1} α1 个, p 2 {p_2} p2 的个数有 α 2 {α_2} α2 个,···, p k {p_k} pk 的个数有 α k {α_k} αk 个。[k] 的总的排列组合数为
      ( α 1 + α 2 + . . . + α k ) / α 1 ! ∗ α 2 ! ∗ . . . ∗ α k ! (α_1+α_2+...+α_k)/α_1!*α_2!*...*α_k! (α1+α2+...+αk)/α1!α2!...αk!

  • 设计思路

    • 先用线性筛法求解 1~2^n 所包含的所有质数,以及每个数的最小质因子。
    • 接着就是对输入的 X 进行质因子分解,从其最小质因子开始分解(由于测试数据很多,直接质因子分解肯定会超时,用最小质因子分解不用遍历合数),分解的同时记录质因子的总个数 total,同时记录同一个质因子的个数 sum[N],不同质因子的个数 cnt
    • 计算倍率 k 有多少种方案。total! / (sum[1]*sum[2]*...sum[cnt-1])
  • 代码

    • #include <iostream>
      using namespace std;
      
      const int N = (1 << 20) + 10;
      
      int primes[N], cnt;//primes[N]记录1~N-1的所有质数,cnt表示总的质数个数
      int minp[N];//存储每个数的最小质因子
      bool st[N];//用于判断当前数是否被筛过
      
      void get_primes(int n)
      {
          for(int i = 2; i <= n; i++)
          {
              if(!st[i]) 
              {
                  primes[cnt++] = i;
                  minp[i] = i;
              }
              for(int j = 0; primes[j] * i <= n; j++)
              {
                  int t = primes[j]*i;
                  st[t] = true;
                  minp[t] = primes[j];
                  if(i % primes[j] == 0) break;
              }
          }
      }
      
      int sum[N];//用于统计每个质因子出现的次数
      
      int main()
      {
          get_primes(N);
          
          int x;
          while( scanf("%d", &x) != EOF )
          {
              int total = 0, cnt = 0;
              while(x>1)
              {
                  int p = minp[x];
                  sum[cnt] = 0;
                  while(x%p==0)
                  {
                      x /= p;
                      total ++;
                      sum[cnt]++;
                  }
                  cnt++;
              }
              
              long long res = 1;
              for(int i = 1; i <= total; i++) res *= i;
              for(int i = 0; i < cnt; i++)
                  for(int j = 1; j <= sum[i]; j++)
                      res /= j;
              printf("%d %lld\n", total, res);
          }
          return 0;
      }
      
      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值